# Archive restoration verified annually

- **Pattern:** `ab-001396` (`finserv-audit-trail.tamper-evidence.archive-restoration-test`)
- **Severity:** info
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001396
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001396)

## 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`:

```ts
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.md` or `src/jobs/archive-test.ts`):

  ```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 AU-11
- sox Section 802
- nist SP-800-34

Taxons: operational-readiness, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-001396
