Archive restoration verified annually
Why it matters
SOX §802 requires that covered companies preserve audit records for 7 years, and regulators during examinations will ask not just whether archives exist but whether they can actually be restored to a readable state. Backup processes that have never been tested are not controls — they are theoretical controls. NIST 800-53 AU-11 requires that organizations retain audit logs for a defined period and that archived records remain accessible. NIST SP 800-34 (Contingency Planning Guide) requires that recovery procedures be tested. A compressed, encrypted archive on Glacier that has never been decompressed, decrypted, and verified is an unknown quantity — and discovering at examination time that the archive is corrupted or the decryption key is lost is a SOX material weakness.
Severity rationale
Info because untested restoration is a latent risk rather than an immediate vulnerability — the failure mode only becomes critical when an actual restoration is required during an incident or examination.
Remediation
Schedule an annual restoration test via cron in src/jobs/archive-test.ts and log each run to archive_restoration_tests:
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import zlib from 'zlib';
async function testArchiveRestoration(targetYear: number) {
const s3 = new S3Client({ region: process.env.AWS_REGION });
const obj = await s3.send(new GetObjectCommand({
Bucket: process.env.AUDIT_ARCHIVE_BUCKET!,
Key: `transactions_${targetYear}.json.gz`
}));
const body = await obj.Body!.transformToByteArray();
const decompressed = zlib.gunzipSync(body);
const records: AuditRecord[] = JSON.parse(decompressed.toString());
// Sample-verify 100 random records
const sample = records.sort(() => Math.random() - 0.5).slice(0, 100);
const allValid = sample.every(r => verifyLogSignature(r, process.env.AUDIT_SIGNING_KEY!));
await db('archive_restoration_tests').insert({
test_date: new Date(), archive_year: targetYear,
records_verified: records.length, status: allValid ? 'success' : 'failed'
});
if (!allValid) await alertOps('Archive integrity check failed', { targetYear });
}
// Run once per year (first Monday of January)
cron.schedule('0 8 1-7 1 1', () => testArchiveRestoration(new Date().getFullYear() - 2));
Detection
-
ID:
archive-restoration-test -
Severity:
info -
What to look for: Count all backup/restore playbooks, scheduled restoration tests, or monitoring jobs. Look for test result logs or records of restoration attempts. Quote the actual test schedule or last-run date found. Verify restoration tests occur at least once every 365 days.
-
Pass criteria: At least 1 documented archive restoration process exists, tested at least once per year (every 365 days). Test results are logged with at least 2 fields: test date and pass/fail status. Report the count even on pass (e.g., "1 annual restoration test found, last run 2025-12-15, status: success").
-
Fail criteria: No archive restoration tests documented (0 playbooks or test records found), or last test was more than 365 days ago.
-
Skip (N/A) when: Application has been in production for fewer than 12 months (no archives old enough to test) — cite the actual deployment date found.
-
Detail on fail:
"No documentation of archive restoration tests — 0 playbooks, 0 test records found.". -
Remediation: Schedule annual restoration testing (in
docs/runbooks/archive-restore.mdorsrc/jobs/archive-test.ts):// In your operations runbook or cron job async function annualArchiveRestorationTest() { const testDate = new Date(); testDate.setFullYear(testDate.getFullYear() - 2); // Test 2-year-old archive try { // Retrieve archived logs from cold storage (S3, Glacier, etc.) const archivedLogs = await s3.getObject({ Bucket: 'audit-logs-archive', Key: `transactions_${testDate.getFullYear()}-${testDate.getMonth()}.json.gz` }); // Decompress and verify const decompressed = zlib.gunzipSync(archivedLogs.Body); const logs = JSON.parse(decompressed); // Verify integrity for (const log of logs.slice(0, 100)) { // Sample check if (!verifyLogSignature(log, AUDIT_KEY)) { throw new Error('Archive integrity check failed'); } } // Log successful test await db.archiveRestorationTests.create({ testDate: new Date(), archiveYear: testDate.getFullYear(), recordsVerified: logs.length, status: 'success' }); } catch (error) { await db.archiveRestorationTests.create({ testDate: new Date(), archiveYear: testDate.getFullYear(), status: 'failed', errorMessage: error.message }); // Alert ops await notificationService.alertOps('Archive restoration test failed', error); } }
External references
- nist:rev5 · AU-11 — Audit Record Retention — verify recoverability of retained records
- sox · Section 802 — Criminal penalties for altering documents — requires demonstrable ability to retrieve records
- nist:rev1 · SP-800-34 — Contingency Planning Guide for Federal Information Systems
Taxons
History
- 2026-04-18·v1.0.0·Initial import from finserv-audit-trail·automated