feat(audit): off-box chain-head export, verify-heads, maintenance login (ScadaBridge#107)
Item 1 (owner decision: no signing). AuditLog:HashChain:HeadExport (default
off): both centrals periodically read every month's chain head (LastSeq,
LastHash, purge anchor, chained rows, tombstones; one snapshot under the
chain lock SHARED) and append the changed heads to a sink the DB login cannot
write - File (append-only JSONL per node per day, local or UNC) or Syslog
(RFC 5424 over UDP / TCP octet counting). `audit verify-heads --path` reads
those records back (file or syslog-collector lines) and the new
VerifyAuditChainHeads command checks each against the database: Matches /
Extended / Purged, or Mismatch / Regressed / MissingHead / MissingLink /
CountMismatch (exit 3). AuditChainHeadStoreTests forges a chain with the
production hasher: verify-chain says Intact, verify-heads says Mismatch.
Item 3. AuditLog:MaintenanceConnectionString (${secret:}-capable, unset =
today's single login) runs the chain job, chain verify / head export, the
partition switch-out + archive and the per-channel purge on
AuditMaintenanceContext (keyed audit-maintenance repository/archive, purge
actor prefers them). AuditMaintenanceLoginTests runs the hardened setup on
SQL Server: writer (datareader/datawriter/audit_writer) ingests and is
refused chaining and deleting; the chainer+purger login does the full cycle.
Two-login grants documented in Component-AuditLog.md.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -2071,6 +2071,41 @@ end, `DuplicateSequence`, `IncompleteLink`). `--format table` prints a summary;
|
||||
The chain only exists once `AuditLog:HashChain:Enabled` is on for the central nodes
|
||||
(default off); see `Component-AuditLog.md`, Security & Tamper-Evidence.
|
||||
|
||||
#### `audit verify-heads`
|
||||
|
||||
Check AuditLog chain heads that were exported off-box (ScadaBridge#107) against the
|
||||
database chain.
|
||||
|
||||
```sh
|
||||
scadabridge --url <url> audit verify-heads --path <file-or-directory>
|
||||
```
|
||||
|
||||
| Option | Required | Default | Description |
|
||||
|--------|----------|---------|-------------|
|
||||
| `--path` | yes | — | An export file, or a directory read recursively for `*.jsonl` / `*.log` / `*.txt` |
|
||||
|
||||
The central nodes write one JSON record per month head (`month`, `lastSeq`, `lastHash`,
|
||||
purge anchor, chained-row and tombstone counts, `exportedAtUtc`, `node`) when
|
||||
`AuditLog:HashChain:HeadExport:Enabled` is on — to append-only files
|
||||
(`{Directory}/{yyyy}/audit-chain-heads-{yyyy-MM-dd}-{node}.jsonl`) or to syslog (RFC 5424, the
|
||||
JSON record as the message). The CLI reads both forms (the JSON object is taken from each
|
||||
line's first `{` to its last `}`, so a syslog collector's prefixes do not matter), keeps one
|
||||
record per distinct (month, sequence, hash), and sends `VerifyAuditChainHeads`. Per record the
|
||||
server reports `Matches`, `Extended`, `Purged` (the link was removed by a partition purge —
|
||||
check the month's archive), or a failure: `Mismatch` (the chain was rewritten and re-hashed),
|
||||
`Regressed` (head rolled back), `MissingHead`, `MissingLink` (a chained row deleted outside the
|
||||
purge paths), `CountMismatch`.
|
||||
|
||||
| Exit code | Meaning |
|
||||
|-----------|---------|
|
||||
| `0` | Every exported head is consistent with the database |
|
||||
| `3` | At least one failure — tamper evidence |
|
||||
| `1` | No records found, bad input, `CHAIN_BUSY`, server error |
|
||||
| `2` | Authorization failure (needs `Administrator` or `Viewer`, not site-scoped) |
|
||||
|
||||
Run it with `audit verify-chain`: `verify-chain` recomputes each month's links;
|
||||
`verify-heads` proves the database still holds the heads that left the box.
|
||||
|
||||
#### `audit archive list` / `audit archive verify`
|
||||
|
||||
List and verify the AuditLog Parquet cold archives (ScadaBridge#42) that the retention
|
||||
|
||||
Reference in New Issue
Block a user