During a regulatory examination, an auditor will request a complete export of audit records for a specified time period — and they will expect a way to verify that the export is complete and unaltered. Without a cryptographic hash and a record count in the export metadata, the auditor cannot distinguish a complete export from one where rows were selectively omitted. NIST 800-53 AU-9 requires protection of audit information integrity, which extends to exported copies. SOX §404 internal control assessments require that the process for producing audit evidence is itself controlled and verifiable. PCI-DSS 4.0 Req-10.3 requires that audit log files are protected. FINRA Rule 4511 requires that records be available for examination — an export without a completeness proof is an unverifiable record.
Medium because an export without a cryptographic completeness proof cannot be used as admissible regulatory evidence, potentially invalidating the audit trail's legal standing during an examination.
Add a completeness metadata block to every export response in src/app/api/admin/audit-logs/export/route.ts:
import crypto from 'crypto';
export async function GET(req: Request) {
const { startDate, endDate } = Object.fromEntries(new URL(req.url).searchParams);
const rows = await db('transaction_logs')
.whereBetween('timestamp', [startDate, endDate]);
const payload = JSON.stringify(rows);
const exportHash = crypto.createHash('sha256').update(payload).digest('hex');
return new Response(payload, {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename=audit_${startDate}_${endDate}.json`,
'X-Export-Record-Count': String(rows.length),
'X-Export-Date-Range': `${startDate}/${endDate}`,
'X-Export-Hash-SHA256': exportHash,
'X-Export-Timestamp': new Date().toISOString()
}
});
}
Include the same four fields — record_count, date_range, export_hash, export_timestamp — in the response body or a sidecar manifest file so they travel with the export artifact.
ID: finserv-audit-trail.retention-compliance.export-completeness
Severity: medium
What to look for: Count all export endpoints or functions (CSV, JSON, PDF). For each, enumerate the metadata fields included in the export. Verify at least 3 completeness proof fields: record count, date range, and cryptographic hash/signature of the export. Quote the actual hash algorithm used if present. An export without at least a record count and date range does not count as pass.
Pass criteria: At least 1 audit log export function exists that includes at least 3 metadata fields proving completeness (record count, date range, hash/signature). Report the count even on pass (e.g., "1 export endpoint found with 4 completeness fields: record_count, date_range, sha256_hash, export_timestamp").
Fail criteria: No export capability exists (0 endpoints), or exports lack at least 3 completeness metadata fields.
Skip (N/A) when: Never — regulators require the ability to export complete audit trails.
Detail on fail: "No audit log export function found — 0 endpoints produce audit data exports." or "Export exists but includes only 1 of 3 required completeness fields (record count only, no hash or date range).".
Remediation: Add a secure export endpoint (in src/app/api/admin/audit-logs/export/route.ts):
import crypto from 'crypto';
import { createObjectCsvWriter } from 'csv-writer';
app.get('/api/admin/audit-logs/export', authenticate, requireRole('compliance'), async (req, res) => {
const { startDate, endDate } = req.query;
const logs = await db.transactionLogs
.find({ timestamp: { $gte: new Date(startDate), $lte: new Date(endDate) } })
.lean()
.exec();
// Create CSV
const csvPath = `/tmp/audit_export_${Date.now()}.csv`;
const writer = createObjectCsvWriter({
path: csvPath,
header: [
{ id: 'id', title: 'ID' },
{ id: 'timestamp', title: 'Timestamp' },
{ id: 'user_id', title: 'User ID' },
{ id: 'operationType', title: 'Operation' },
// ... all columns
]
});
await writer.writeRecords(logs);
// Compute completeness proof
const fileHash = crypto.createHash('sha256');
const fileContent = fs.readFileSync(csvPath);
fileHash.update(fileContent);
const exportHash = fileHash.digest('hex');
const completenessProof = {
exportDate: new Date().toISOString(),
recordCount: logs.length,
dateRange: { startDate, endDate },
exportHash,
exportHashAlgorithm: 'sha256'
};
// Send CSV with proof in header
res.setHeader('X-Export-Proof-Hash', exportHash);
res.setHeader('X-Export-Record-Count', logs.length);
res.download(csvPath, `audit_export_${startDate}_${endDate}.csv`, () => {
fs.unlinkSync(csvPath);
});
});