Commit 849b372
feat(v1.100 PR-26-code-D): post-restore evidence record (§39.3 / §48.6 lock)
PR-26-code-D — restore verification / evidence hardening, slice D.
Adds the structured post-restore evidence-record writer per §39.3
+ §48.6 operator lock. Recording-only — does NOT re-run PR-24
decisions, rebuild TargetAuthority, or add validator/module-health
probes (operator design call).
Authority:
- PR #512 / contract.md Part IV §§37-50
- PR #513 / §51 lock record
- PR #514 / code-A merge 4e98ff5
- PR #515 / code-B merge 45fc63e
- PR #516 / code-C merge 6d8386d
- §39 Q1 BLOCKING evidence rows
- §39.3 evidence-record file requirement
- §46 CI gate requirements
- §48.6 (operator-locked at this commit's open):
- path: /var/lib/nftban/state/restore-evidence/
- filename: restore-evidence-<UTC-RFC3339-basic>-<short-random>.json
- schema: 1.0.0
- writer helper: writeRestoreEvidenceRecord(ctx, exec, record)
- path constant: restoreEvidenceDir
- §51.5-A2 (read-only typed introspection outside mutation cap)
Files added (2):
cmd/nftban-installer/restore_evidence.go
- Constants:
restoreEvidenceSchemaVersion = "1.0.0"
restoreEvidenceDir = "/var/lib/nftban/state/restore-evidence"
restoreEvidenceFilenamePrefix = "restore-evidence-"
restoreEvidenceMode = 0o640
restoreEvidenceDirMode = 0o750
- Schema types: RestoreEvidenceRecord (schema_version, timestamp_utc,
mode, phase, target, result, verification, history_gate, warnings) +
the 4 nested structs.
- Sentinels: ErrEvidenceWriteFailed, ErrEvidenceNilExecutor,
ErrEvidenceNilRecord.
- writeRestoreEvidenceRecord — the SINGLE helper. MkdirAll, marshal,
WriteFileAtomic. Filename: prefix + UTC RFC3339-basic stamp +
"-" + 8-hex random suffix + ".json".
- buildRestoreEvidenceRecord — recording-only assembler. Sources:
target.Kind/FirewallType/Panel, execRes.Terminal/Stage/VerifyResult,
exec.NftTableExists for emergency + nftban tables, detect.SSHPortWithSource.
No re-derivation; no Probe / Decide / DetectPanel calls.
- evidenceShortRandom — crypto/rand-backed 8-hex suffix to avoid
same-second filename collisions.
cmd/nftban-installer/restore_evidence_test.go
- 10 tests:
1. WriteRestoreEvidence_HappyPath — filename pattern + single write
2. WriteRestoreEvidence_RoundTripsJSON — schema_version + mode +
phase + history_gate flags
3. WriteRestoreEvidence_NilExecutor — defensive guard
4. WriteRestoreEvidence_NilRecord — defensive guard
5. WriteRestoreEvidence_OnlyHelperWritesUnderEvidenceDir_FileScan —
single-WriteFileAtomic invariant
6. WriteRestoreEvidence_NoForbiddenSurfaces_FileScan —
recording-only invariant pin
7. BuildRestoreEvidenceRecord_RecordedPriorHappy — full happy
path with ss-listener SSH port resolution
8. BuildRestoreEvidenceRecord_NftbanTablesPresent_Recorded —
post-mutation kernel observation
9. BuildRestoreEvidenceRecord_AuthorityClassDivergenceWarning —
ObservedAuthority diverging from AuthorityExternal surfaces
in warnings
10. RestoreEvidenceConstants_LockPin — §48.6 path/version/prefix
pinned exactly
Files modified (4):
internal/installer/detect/ssh.go
- Added detect.SSHPortWithSource (read-only). Same 4-source priority
chain as detect.SSHPort but also returns the source name (ss /
sshd_config / state / config) — required by the §48.6 schema's
ssh_port_source enum. Per §51.5-A2 outside the mutation cap.
cmd/nftban-installer/restore_decide.go
- runRestoreExecutionFromProceed gains a Step D (between Execute
and Transition):
1. buildRestoreEvidenceRecord(target, execRes)
2. writeRestoreEvidenceRecord(ctx, exec, rec, log)
- §48.6 downgrade rule: if evidence-write fails AFTER a successful
StateRestoreExecuted, downgrade to StateRestoreDegraded
(state.machine.go:152 already supports this terminal). The state
model supports the downgrade; no contract amendment needed.
- Operator-facing log line on Degraded now includes the evidence-
write failure reason.
- No state-machine / exit-code / history-gate change. main.go:132
mode-gate untouched.
cmd/nftban-installer/restore_decide_test.go
- TestRunRestoreExecutionFromProceed_FakeDeps_HappyPath_PersistsExecuted
+ 4 other dispatcher tests updated: pass executor.NewMockExecutor()
instead of nil so the new evidence-write step succeeds and the
terminal stays at StateRestoreExecuted (fake happy path). The 3
tests that pass nil exec via _ = runRestoreExecutionFromProceed
do not assert on sf.State so they still pass under the downgrade.
.github/workflows/ci-restore-canonization.yml
- New gate G4-RESTORE-EVIDENCE-RECORD (§46). Structural — pins the
named-constant + single-helper invariant:
* restore_evidence.go declares restoreEvidenceDir,
restoreEvidenceSchemaVersion, restoreEvidenceFilenamePrefix
verbatim + locked values
* restore_evidence.go declares writeRestoreEvidenceRecord +
buildRestoreEvidenceRecord + RestoreEvidenceRecord struct
* exactly ONE WriteFileAtomic call in restore_evidence.go
(the single-helper invariant — locked by §48.6)
* forbidden-symbol scan: restore.Decide /
restore.PlanFromDecision / uninstall.Probe / detect.DetectPanel
/ writeHistory / update-history.json / mutation primitives /
direct OS bypass (recording-only invariant)
* dispatcher (restore_decide.go) calls BOTH
writeRestoreEvidenceRecord AND buildRestoreEvidenceRecord
(proves evidence is consumed, not just imported)
- §46.1 line-skipping discipline applied (production-code-only,
comment-stripped).
Recording-only invariant (operator design call) honored:
- No restore.Decide / restore.PlanFromDecision calls
- No uninstall.Probe call
- No detect.DetectPanel call (only detect.SSHPortWithSource —
read-only typed introspection)
- No validator full-sweep / module-health probe
- No update-history.json write (§19.2 layer 4 / main.go:132 retained)
- No new mutation primitive
Constraints honored (per operator scope):
IN:
- evidence record type + schema ✓ (§48.6 lock)
- evidence writer helper ✓ (single helper writeRestoreEvidenceRecord)
- production write after restore execution path ✓ (dispatcher Step D)
- structural CI gate G4-RESTORE-EVIDENCE-RECORD ✓
- tests proving all writes stay under restoreEvidenceDir ✓
- tests proving update-history is untouched ✓ (HistoryGate flags +
no writeHistory references in evidence module)
OUT:
- destructive soak (PR-26-code-E)
- A.4 cron changes (already shipped in code-C)
- executor new mutation methods (Stat is read-only, shipped in code-C)
- iptables introspection (Option B lock)
- main.go history gate changes (untouched)
- state/exit-code changes — only the existing StateRestoreDegraded
is consumed, no new state added
- repo hygiene / UX / GOTH / metrics / module cleanup
Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./... PASS (full repo, 64 packages)
- go test -race -count=1 cmd + restore + state + switchop + detect PASS
- go vet ./... clean
- go mod tidy no-op
- 10 new TestWriteRestoreEvidence_* / TestBuildRestoreEvidenceRecord_* /
TestRestoreEvidenceConstants_LockPin tests all PASS
- existing 5 dispatcher fake-deps tests updated + still PASS
- All 3 G4 gates (NO-OUT-OF-TARGET / CRON-MANIFEST-INTEGRITY /
EVIDENCE-RECORD) local replay: FAIL=0
Awaiting auditor pass before push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 6d8386d commit 849b372
6 files changed
Lines changed: 894 additions & 11 deletions
File tree
- .github/workflows
- cmd/nftban-installer
- internal/installer/detect
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
549 | 549 | | |
550 | 550 | | |
551 | 551 | | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
552 | 676 | | |
553 | 677 | | |
554 | 678 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
303 | | - | |
304 | 303 | | |
305 | | - | |
306 | | - | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
307 | 332 | | |
308 | 333 | | |
309 | 334 | | |
310 | | - | |
| 335 | + | |
311 | 336 | | |
312 | | - | |
| 337 | + | |
313 | 338 | | |
314 | 339 | | |
315 | | - | |
| 340 | + | |
316 | 341 | | |
317 | 342 | | |
318 | 343 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
222 | 226 | | |
223 | 227 | | |
224 | 228 | | |
| |||
251 | 255 | | |
252 | 256 | | |
253 | 257 | | |
254 | | - | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
255 | 263 | | |
256 | 264 | | |
257 | 265 | | |
| |||
374 | 382 | | |
375 | 383 | | |
376 | 384 | | |
377 | | - | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
378 | 390 | | |
379 | 391 | | |
380 | 392 | | |
| |||
407 | 419 | | |
408 | 420 | | |
409 | 421 | | |
410 | | - | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
411 | 427 | | |
412 | 428 | | |
413 | 429 | | |
| |||
439 | 455 | | |
440 | 456 | | |
441 | 457 | | |
442 | | - | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
443 | 463 | | |
444 | 464 | | |
445 | 465 | | |
| |||
0 commit comments