(log: SignedAuditLog, verdict: AuditVerifyResult)
| 162 | * Feed the blocks to pdf-lite's buildPdf. PURE (pass the verdict computed by verifyAuditLog). |
| 163 | */ |
| 164 | export function buildAuditPdfBlocks(log: SignedAuditLog, verdict: AuditVerifyResult): import('./pdf-lite.js').PdfBlock[] { |
| 165 | const kv = (k: string, v: string): import('./pdf-lite.js').PdfBlock => ({ text: `${k}: ${v}`, size: 10, indent: 10 }); |
| 166 | const sigLine = !verdict.signaturePresent ? 'unsigned (integrity only)' |
| 167 | : verdict.signatureValid === undefined ? `signed (keyId ${log.keyId ?? '?'}) — no key supplied to verify` |
| 168 | : verdict.signatureValid ? `VALID — HMAC-SHA256, keyId ${log.keyId ?? '?'} (authentic)` : 'INVALID — wrong key or forged'; |
| 169 | const blocks: import('./pdf-lite.js').PdfBlock[] = [ |
| 170 | { text: 'Maintain Audit Log', size: 18, bold: true }, |
| 171 | { text: `Exported ${log.exportedAt} - ${log.count} run(s) - tamper-evident hash chain (each entry commits to the previous; altering, reordering, or dropping any entry breaks every downstream hash).`, size: 9, spaceBefore: 4 }, |
| 172 | { text: 'Verification', size: 13, bold: true, spaceBefore: 12 }, |
| 173 | kv('Chain integrity', verdict.chainValid ? 'INTACT — no entry altered, reordered, or dropped' : `BROKEN at #${verdict.brokenAt} — ${verdict.reason}`), |
| 174 | kv('Head', `${verdict.headMatches ? 'matches the chain' : 'MISMATCH'} (${log.head.slice(0, 24)}...)`), |
| 175 | kv('Signature', sigLine), |
| 176 | kv('Overall', verdict.ok ? 'PASS — this log is trustworthy' : 'FAIL — do not trust this log'), |
| 177 | { text: 'Run chain (oldest first)', size: 13, bold: true, spaceBefore: 12 }, |
| 178 | ]; |
| 179 | if (!log.entries.length) blocks.push({ text: 'no runs recorded', size: 10, indent: 10 }); |
| 180 | for (const e of log.entries) { |
| 181 | const outcome = e.status === 'opened' ? '[OK] opened' : e.status === 'blocked' ? '[BLOCKED]' : e.status; |
| 182 | const checks = e.verification.length ? ` - checks: ${e.verification.map(v => `${v.passed ? 'v' : 'x'} ${v.command}`).join(', ')}` : ''; |
| 183 | const files = e.filesChanged ? ` - ${e.filesChanged} file(s)` : ''; |
| 184 | blocks.push({ text: `#${e.seq} ${e.at.slice(0, 10)} ${e.scope} ${outcome}${files}${checks}`, size: 9, mono: true, indent: 10 }); |
| 185 | if (e.prUrl) blocks.push({ text: ` PR: ${e.prUrl}`, size: 8, mono: true, indent: 10 }); |
| 186 | } |
| 187 | blocks.push({ text: 'Verify this log offline anytime: qodex maintain-audit-verify <file> (exit 1 on tamper - CI-friendly).', size: 8, spaceBefore: 12 }); |
| 188 | return blocks; |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * Full audit verification of a parsed log: chain integrity, that the stored head matches the chain, |
no test coverage detected