feat(management): parquet audit export and audit archive list/verify (ScadaBridge#42)

GET /api/audit/export?format=parquet returned 501. It now streams the same 24
columns as csv/jsonl (AuditExportRow, CsvHeader order) as one Parquet file,
one 10,000-row row group at a time. Each row group is encoded into a small
non-seekable staging buffer and copied to the response asynchronously, because
Kestrel rejects the synchronous writes the Parquet writer issues.

New management commands, with CLI parity:
- ListAuditArchives -> `audit archive list` (AuditArchiveListResult: enabled
  flag + every manifest).
- VerifyAuditArchive(Name) -> `audit archive verify --name` (files, chain,
  purge anchor); exit 0 verified, 3 not verified, 1 error, 2 unauthorized.
Both carry verify-chain's gate: OperationalAudit roles (Administrator, Viewer),
site-scoped non-Administrator refused because an archive spans every site.
VerifyAuditArchive uses the long-running Ask timeout. Error codes
ARCHIVE_NOT_CONFIGURED, INVALID_ARGUMENT, NOT_FOUND.

RequiredRoleMatrixTests freezes both gates; ManagementSurfaceParityTests now
probes `audit archive list|verify` alongside `audit verify-chain`.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 17:00:58 -05:00
parent 7be4f19861
commit d1fc336e18
17 changed files with 982 additions and 42 deletions
@@ -31,3 +31,19 @@ public record AuditLogQueryResult(IReadOnlyList<AuditLogEntry> Entries, int Tota
/// </summary>
/// <param name="Month">The month to verify, <c>YYYY-MM</c>.</param>
public record VerifyAuditChainCommand(string Month);
/// <summary>
/// Lists the AuditLog Parquet cold archives under the configured archive directory (audit
/// roadmap T2, ScadaBridge#42). Read-only; OperationalAudit roles, refused for a site-scoped
/// caller because an archive holds every site's rows.
/// </summary>
public record ListAuditArchivesCommand;
/// <summary>
/// Verifies one AuditLog Parquet archive (ScadaBridge#42): file integrity against its
/// manifest, the hash chain recomputed over the archived rows, and the chain end against the
/// checkpoint anchor the partition purge left. Read-only; same gate as
/// <see cref="ListAuditArchivesCommand"/>.
/// </summary>
/// <param name="Name">The archive name, as <c>audit archive list</c> reports it.</param>
public record VerifyAuditArchiveCommand(string Name);