Skip to content

feat(v1.100 PR-26): code-C cron backup manifest writer + A.4 manifest restore#516

Merged
itcmsgr merged 5 commits intomainfrom
feat/v1.100-pr26-code-c-cron-manifest
Apr 28, 2026
Merged

feat(v1.100 PR-26): code-C cron backup manifest writer + A.4 manifest restore#516
itcmsgr merged 5 commits intomainfrom
feat/v1.100-pr26-code-c-cron-manifest

Conversation

@itcmsgr
Copy link
Copy Markdown
Owner

@itcmsgr itcmsgr commented Apr 28, 2026

PR-26-code-C: cron backup manifest + A.4 manifest restore

This PR is split internally into 4 reviewable commits on the same branch:

Commit SHA Scope
C1 96162f69 Install-time CSF/LFD cron-backup writer in switchop.disarmCSFArtifacts + new shared cron_manifest.go (sha256-pinned manifest types, ComputeCronBackupSHA256, WriteCronBackupManifest, ReadCronBackupManifest, VerifyCronBackupEntry) + read-only typed executor.Stat (per §51.5-A2 outside the mutation cap).
C2 c5767f45 A.4 manifest restore in restore_deps_csf.go::mutateToCSFTarget step 3. Two new typed sentinels: ErrCSFRestoreCronManifestCorrupt, ErrCSFRestoreCronTargetExists.
CI 93e86e25 New structural gate G4-RESTORE-CRON-MANIFEST-INTEGRITY — pins writer + reader symbols, allow-list cron-target literals, §46.1 line-skipping discipline.
Fix f7be0c49 Auditor verdict fix: corrupt manifest is HARD refusal before A.5 (was: soft-skip with A.5 still running). Aligns with the evidence model — absence is migration tolerance; corruption is untrusted evidence.

Authority

Evidence model

Branch Outcome A.5 runs? Sentinel
Manifest absent migration soft-skip + warning YES
Parse failure HARD refuse NO ErrCSFRestoreCronManifestCorrupt
Schema mismatch HARD refuse NO ErrCSFRestoreCronManifestCorrupt
Unknown-entry path HARD refuse NO ErrCSFRestoreCronManifestCorrupt
sha256 mismatch HARD refuse NO ErrCSFRestoreCronManifestCorrupt
WriteFileAtomic failure HARD refuse NO ErrCSFRestoreCronManifestCorrupt
Target already exists HARD refuse NO ErrCSFRestoreCronTargetExists
Clean manifest restore both files YES

Rationale: absence = migration tolerance for pre-PR-26 hosts (graceful soft-skip with operator warning, A.5 continues). Corruption = untrusted evidence (HARD refuse before A.5 — the existing §32 step-3 failure path retains the safety net; operator must inspect). This alignment was applied per the focused-audit verdict on f7be0c49.

Manifest schema (locked at §42.2)

  • Dir: /var/lib/nftban/state/csf-cron-backup/
  • Files: csf-cron, lfd-cron (the sha256-pinned content copies)
  • Manifest: manifest.jsonschema_version + captured_at + per-entry {path, backup_name, sha256, mode, uid, gid, size}
  • Schema version: 1.0.0 (any drift is rejected with ErrCronManifestSchemaMismatch)
  • Source paths locked: /etc/cron.d/csf-cron AND /etc/cron.d/lfd-cron ONLY. Unknown-entry paths refused.

Pre-PR-26 hosts without manifest

By design: graceful soft-skip with operator warning. A.5 continues so csf can still start. The migration is purely additive — pre-PR-26 hosts do not require manifest creation.

Out of scope (not touched)

  • Destructive real-host CSF soak (PR-26-code-E)
  • IptablesRuleExists / iptables introspection (Option B lock)
  • main.go / state machine / exit codes / history gate (INV-PR25-HISTORY-GATE retained)
  • Restore planner / TargetAuthority / PR-24 lattice
  • contract.md (no amendment needed; §42 lock satisfied as-is)
  • Repo hygiene / UX / GOTH / metrics / module cleanup

Files changed (8)

File Δ Role
internal/installer/executor/executor.go +24 / −2 New FileMeta + Stat (read-only)
internal/installer/executor/real.go +21 / −0 RealExecutor.Stat via os.Stat + syscall.Stat_t
internal/installer/executor/mock.go +24 / −0 MockExecutor.Stat + FileStats map
internal/installer/switchop/cron_manifest.go NEW (305 lines) Manifest types + sha256 helper + writer + reader + verifier
internal/installer/switchop/cron_manifest_test.go NEW (351 lines) 15 writer/reader/round-trip tests
internal/installer/switchop/takeover.go +20 / −1 disarmCSFArtifacts writes manifest before rm
cmd/nftban-installer/restore_deps_csf.go +132 / −31 A.4 manifest-restore + 2 new sentinels + hard-refusal semantics
cmd/nftban-installer/restore_deps_csf_test.go +323 / −51 11 PR26C2 tests + seedCronManifest helper
.github/workflows/ci-restore-canonization.yml +140 / −0 New G4-RESTORE-CRON-MANIFEST-INTEGRITY structural gate

Lab2 verification (head f7be0c49)

  • go build ./... clean
  • go test ./... PASS (full repo, 64 packages)
  • go test -race -count=1 cmd + restore + state + switchop PASS
  • go vet ./... clean
  • go mod tidy no-op
  • 15 cron-manifest writer/reader/round-trip tests PASS
  • 11 PR26C2 A.4 tests PASS (4 hard-refusal retargets + 3 new behavior pins + 4 unchanged)
  • G4-RESTORE-CRON-MANIFEST-INTEGRITY local gate replay: FAIL=0
  • All existing PR-25 / PR-26-code-A / PR-26-code-B tests still PASS

Test plan

  • Restore Canonization Gate matrix (ubuntu-24.04 + almalinux-9 + summary) green
  • G4-RESTORE-EXEC-NO-OUT-OF-TARGET green (no new mutation symbols outside the bounded-3 cap)
  • G4-RESTORE-CRON-MANIFEST-INTEGRITY (NEW) green
  • G4-RESTORE-NO-IMPLICIT-EXEC green
  • Architecture Policy / Policy Gates green
  • Go Build & Test + race + full DEB+RPM matrix + CodeQL / Semgrep / Secure Go / OSV / Gitleaks / GitGuardian green
  • ShellCheck / Bash Validation / Docs Quality green

After CI completes, audit the gate results before merge.

🤖 Generated with Claude Code

itcmsgr and others added 4 commits April 28, 2026 13:43
…+ executor.Stat

PR-26-code-C is split into two reviewable sub-slices on the same
branch. C1 (this commit) lands the WRITER side; C2 (next commit)
lands the READER side. §50 ordering lock: writer commit BEFORE reader.

Authority:
- PR #512 / contract.md Part IV §§37-50
- PR #513 / §51 lock record (§51.5-A2: read-only typed introspection
  is OUTSIDE the bounded-3 mutation cap)
- PR #514 / code-A merge 4e98ff5
- PR #515 / code-B merge 45fc63e
- §42 cron backup / A.4 contract
- §51.6 entry criteria (code-B merged)

C1 scope (this commit):

1. Add typed executor.Stat read-only introspection method.
   - executor/executor.go: new FileMeta struct + Stat method on
     Executor interface.
   - executor/real.go: RealExecutor.Stat via os.Stat + syscall.Stat_t.
     UID/GID extracted from the platform-specific Sys() interface
     (Linux-only target).
   - executor/mock.go: MockExecutor.Stat reads from new FileStats map
     (path → FileMeta); falls back to (0644, 0:0, len(content)) if
     the path is in Files but not FileStats. Returns os.ErrNotExist
     if neither holds.
   - Per §51.5-A2 invariant: read-only introspection is OUTSIDE the
     bounded-3 mutation surface cap of
     INV-PR26-NEW-MUTATION-SURFACES-BOUNDED. Stat does NOT count
     against §44 row 2's mutation budget.

2. New shared cron-manifest module: internal/installer/switchop/cron_manifest.go.
   - Constants:
       CronManifestSchemaVersion  = "1.0.0"
       CronManifestDir            = "/var/lib/nftban/state/csf-cron-backup"
       CronManifestFile           = "/var/lib/nftban/state/csf-cron-backup/manifest.json"
       CronCSFSrcPath             = "/etc/cron.d/csf-cron"
       CronLFDSrcPath             = "/etc/cron.d/lfd-cron"
   - Types: CronManifestEntry (path / backup_name / sha256 / mode /
     uid / gid / size) + CronManifest (schema_version / captured_at /
     files).
   - Helpers:
       ComputeCronBackupSHA256(content) — single source of truth
         shared by writer + reader; identical bytes-to-hex semantics
         in both directions.
       WriteCronBackupManifest(exec, log) — install-time writer.
         For each of {csf-cron, lfd-cron} that exists: read content,
         Stat for mode/uid/gid/size, compute sha256, copy under
         CronManifestDir, append manifest entry. Then write
         manifest.json. Files absent at capture time are skipped (no
         entry recorded; no fabrication).
       ReadCronBackupManifest(exec, log) — used by the C2 reader.
         Three return shapes: absent (zero, false, nil), present-but-
         corrupt (zero, true, ErrCronManifestParseFailed/
         ErrCronManifestSchemaMismatch/ErrCronManifestUnknownEntry),
         present-and-valid (manifest, true, nil).
       VerifyCronBackupEntry(exec, entry) — sha256 integrity check
         against the on-disk backup.
   - Sentinels: ErrCronManifestSchemaMismatch,
     ErrCronManifestSHA256Mismatch, ErrCronManifestUnknownEntry,
     ErrCronManifestParseFailed.

3. Modified disarmCSFArtifacts in switchop/takeover.go to call
   WriteCronBackupManifest BEFORE the existing rm -f of the cron
   files. Writer failure is logged but non-fatal: the rm path MUST
   still execute (nftban-takeover correctness invariant). Hosts
   installed before PR-26-code-C ship without a manifest; A.4 stays
   soft-skip on those hosts (§42.2 graceful migration).

4. Tests in internal/installer/switchop/cron_manifest_test.go:
   - WriteCronBackupManifest_BothPresent_RecordsBoth
   - WriteCronBackupManifest_OnlyOnePresent_OnlyOneRecorded
   - WriteCronBackupManifest_NeitherPresent_EmptyManifest
   - WriteCronBackupManifest_WritesOnlyManifestDir (no writes
     outside CronManifestDir)
   - WriteCronBackupManifest_ManifestPathPinnedExact
   - WriteCronBackupManifest_OnlyAuthorizedSrcPaths (writer ignores
     non-{csf-cron, lfd-cron} cron files; never invents content)
   - WriteCronBackupManifest_SHA256ComputedCorrectly
   - ReadCronBackupManifest_AbsentReturnsFalse (graceful skip path)
   - ReadCronBackupManifest_ParseFailure (corrupt JSON refused)
   - ReadCronBackupManifest_SchemaMismatch
   - ReadCronBackupManifest_UnknownEntryPath (defense-in-depth)
   - ReadCronBackupManifest_HappyPath
   - CronManifest_WriteThenRead_Roundtrip
   - VerifyCronBackupEntry_HappyPath
   - VerifyCronBackupEntry_SHA256Mismatch

Constraints honored (per §51.6 + operator C scope):

IN scope (C1):
- install-time cron-backup manifest writer ✓
- only the two §42.2-locked cron files (csf-cron, lfd-cron) ✓
- only writes under CronManifestDir ✓
- manifest records: path, sha256, mode, uid, gid, size, schema_version ✓
- no template regeneration ✓
- no DirectAdmin custombuild ✓
- no unrelated cron files ✓
- absent files cleanly skipped (no fabrication) ✓

OUT of scope (and untouched):
- A.4 reader / restore path (PR-26-code-C2 in next commit on same branch)
- Destructive real-host CSF soak (PR-26-code-E)
- IptablesRuleExists / iptables introspection (Option B lock)
- main.go / state-machine / exit codes / history gate (untouched)
- Restore planner / TargetAuthority / PR-24 lattice (untouched)
- contract.md (untouched)
- Repo hygiene / UX / GOTH / metrics / module cleanup (untouched)

Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./internal/installer/switchop/... PASS

C2 lands the reader side in the next commit on this branch. Both
ship in PR-26-code-C; auditor checkpoint after C1+C2 compile +
tests pass before push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… step 3

PR-26-code-C2 — companion to C1. C1 lands the install-time manifest
writer; C2 (this commit) flips A.4 from soft-skip to manifest-restore
when the §42.2 cron-backup manifest is present + integrity-clean.

Authority:
- C1 commit on this branch (cron_manifest.go writer + executor.Stat)
- §42.2 cron-backup contract (manifest-only restore; no template
  regeneration; no cron files NFTBan did not back up itself)
- §51.6 entry criteria

Behavior delta:

- Before (C1): A.4 always soft-skipped with a generic warning.
- After (C2): A.4 reads switchop.ReadCronBackupManifest. Three paths:

  - Manifest absent (pre-PR-26 host) → graceful soft-skip, no
    /etc/cron.d/* writes, A.5 runs.
  - Manifest present but corrupt / schema-mismatch / unknown-entry /
    sha256-mismatch → soft-skip with a specific operator warning,
    no /etc/cron.d/* writes, A.5 still runs (per §42.2-D: csf can
    function without cron; LFD just won't auto-restart).
  - Manifest present + integrity-clean → for each entry whose target
    is currently absent, restore via WriteFileAtomic (preserves
    mode) + Chown (preserves uid/gid). Targets that already exist
    are skipped (operator may have re-created a different version
    post-takeover; A.4 must not overwrite operator content).

Files changed (2):

cmd/nftban-installer/restore_deps_csf.go
- New typed sentinel: ErrCSFRestoreCronManifestCorrupt (exported for
  observability + test assertion via errors.Is). Per §42.2-D, A.4
  emits this informationally and continues to A.5; the overall
  mutation does NOT abort on cron failure.
- A.4 step rewritten: calls switchop.ReadCronBackupManifest,
  switches on (absent / corrupt / present), per-entry sha256
  verification via switchop.VerifyCronBackupEntry, restoration via
  exec.WriteFileAtomic + exec.Chown.
- New imports: "os" (for os.FileMode), "switchop" (for the shared
  manifest module).
- New local helper fileModeFromUint32 — single-purpose conversion
  for the manifest's uint32 mode bitfield to os.FileMode. Keeps os
  import scoped narrowly.

cmd/nftban-installer/restore_deps_csf_test.go
- New seedCronManifest helper writes a sha256-valid manifest +
  matching backup files into the mock for end-to-end A.4 tests.
- 8 new TestCSFMutate_PR26C2_* tests:
    1. A4_ManifestAbsent_SoftSkip — pre-PR-26 host case
    2. A4_HappyPath_RestoresBothFiles — manifest present + integrity
       clean + targets absent
    3. A4_TargetExists_SkipsRestore — operator content not overwritten
    4. A4_SHA256Mismatch_SoftSkip_A5StillRuns — §42.2-D non-abort
    5. A4_SchemaMismatch_SoftSkip_A5StillRuns — §42.2-D non-abort
    6. A4_OnlyAuthorizedTargetPaths — no broad /etc/cron.d/* writes
    7. TypedSentinelExported — ErrCSFRestoreCronManifestCorrupt visible
    8. A4_UnknownEntryPath_Rejected — defense-in-depth refusal

Constraints honored (per §51.6 + operator C scope):

IN scope (C2):
- A.4 reader / restore path enabled when manifest is present ✓
- soft-skip with warning for pre-PR-26 hosts ✓
- typed refusal (sentinel surfaced) for corrupt / hash-mismatch /
  ambiguous cases ✓
- restore only the two §42.2-locked cron files ✓
- preserve mode/uid/gid via WriteFileAtomic + Chown ✓
- no write outside the two backup-target paths ✓
- no cron restore unless evidence says NFTBan backed up the file ✓

OUT of scope (and untouched):
- Destructive real-host CSF soak (PR-26-code-E)
- IptablesRuleExists / iptables introspection (Option B lock)
- main.go / state-machine / exit codes / history gate
- Restore planner / TargetAuthority / PR-24 lattice
- contract.md
- Repo hygiene / UX / GOTH / metrics / module cleanup

§42.2-D semantics preserved: A.4 corrupt-manifest does NOT abort A.5.
csf can function without cron; LFD just won't auto-restart. The
operator-warning log line is more specific than 4B-3-csf's generic
warning (states which precondition failed). The typed sentinel is
exposed for higher-layer observability.

Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./cmd/nftban-installer/... ./internal/installer/restore/...
  ./internal/installer/state/... ./internal/installer/executor/...
  ./internal/installer/switchop/... PASS
- 8 new TestCSFMutate_PR26C2_* tests all PASS
- existing TestCSFMutate_4B3csf_A4_SoftSkip_ZeroFileWrites still
  passes (manifest-absent fixture takes the new soft-skip path)

Awaiting C1+C2 auditor checkpoint before push. CI gate update
(G4-RESTORE-CRON-MANIFEST-INTEGRITY) lands as a third commit on
the same branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tural gate

Strengthens the Restore Canonization workflow with the §46 cron-
manifest integrity gate locked at §51.6 entry criteria for code-C.

Authority:
- §42 cron backup / A.4 contract (manifest-only restore)
- §46 CI gate requirements (structural, not loose grep)
- §46.1 line-skipping discipline (production-code-only,
  comment-stripped)

Gate scope (writer + reader cross-pin):

WRITER required symbols (internal/installer/switchop/cron_manifest.go):
- CronManifestSchemaVersion = "1.0.0" const
- CronManifestDir / CronManifestFile constants pinned to the exact
  /var/lib/nftban/state/csf-cron-backup/{,manifest.json} paths
- CronCSFSrcPath / CronLFDSrcPath constants pinned to the exact
  /etc/cron.d/{csf-cron,lfd-cron} source paths
- func ComputeCronBackupSHA256(content []byte) string — single
  source of truth for the sha256 helper
- func WriteCronBackupManifest(...), ReadCronBackupManifest(...),
  VerifyCronBackupEntry(...) — the three exported API points
- sha256.Sum256 — proves the writer actually computes sha256 (not a
  no-op stub)

Pattern shape: whitespace-flexible ([[:space:]]+) so the patterns
don't break when gofmt re-aligns the const block.

READER required symbols (cmd/nftban-installer/restore_deps_csf.go):
- switchop.ReadCronBackupManifest( — A.4 reads the manifest
- switchop.VerifyCronBackupEntry( — A.4 verifies sha256 BEFORE
  restoring (this is the integrity guarantee §42.2-D requires)
- ErrCSFRestoreCronManifestCorrupt — the typed sentinel surfaced
  on integrity failure

If any required symbol is absent, the gate fails — proves the
integrity check is consumed, not just imported.

WRITER + READER forbidden patterns:
- \bcustombuild\b — defense-in-depth (§34: no DirectAdmin custombuild)
- iptables-restore — defense-in-depth (§34: csf manages its own)
- "/etc/cron.d/*" glob literal — no broad cron sweep
- WriteFile to /etc/cron.d/* with non-csf-prefixed leaf (rough check)

READER allow-list pin:
- Every WriteFileAtomic call in restore_deps_csf.go that targets a
  /etc/cron.d/* literal MUST equal one of the two §42.2-locked
  literals: "/etc/cron.d/csf-cron" OR "/etc/cron.d/lfd-cron".
- The reader uses the named constants csfCronPath / lfdCronPath, so
  in practice this grep returns zero matches (named-constant
  reference, not string-literal in WriteFileAtomic args). Defense-
  in-depth structural pin against accidental future literal-arg
  drift.

§46.1 discipline applied: production-code-only files, comment-
stripped before pattern matching. Avoids the false-positive class
that hit Policy Gates on PR #511 (//-comment text matching forbidden
substrings).

Local replay against the PR-26-code-C1 + C2 source:
  WRITER_MISS / READER_MISS / FORBIDDEN_HIT / BAD_LITERAL: all 0
  FAIL=0

Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./... PASS (64 packages)
- go test -race -count=1 ./cmd/nftban-installer
  ./internal/installer/restore/... ./internal/installer/state/...
  ./internal/installer/switchop/... PASS
- go vet ./... clean
- go mod tidy no-op

Auditor checkpoint: C1 + C2 + CI gate are now all locally compiled,
tested, and gate-replayed clean. Awaiting focused auditor pass before
push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…soft-skip (auditor verdict)

Auditor focused-audit on PR-26-code-C flagged a semantic risk in
the A.4 corrupt-manifest branch: previously a corrupt /
hash-mismatch / unknown-entry / parse-failure manifest was a
soft-skip with an informational sentinel, and A.5 still ran. The
auditor argued — correctly — that proceeding to start csf.service
when restore evidence is on disk but cannot be trusted weakens the
evidence chain.

Locked rule (per auditor verdict):

  manifest absent      → soft-skip warning, continue to A.5  [migration gap, kept]
  manifest incomplete  → ErrCSFRestoreCronManifestCorrupt, stop before A.5
  hash mismatch        → ErrCSFRestoreCronManifestCorrupt, stop before A.5
  target exists dirty  → ErrCSFRestoreCronTargetExists, stop before A.5
  manifest clean       → restore exact files, then continue to A.5

Behavior delta (this commit only — C1 + C2 + CI gate semantics
remain otherwise unchanged):

- Manifest parse failure / schema mismatch / unknown-entry path
  → A.4 returns wrapped ErrCSFRestoreCronManifestCorrupt; A.5 does
  NOT run; the existing §32 step-3 failure path retains the safety
  net.
- Per-entry sha256 mismatch → same hard refusal.
- Operator-content collision (target /etc/cron.d/<name> already
  exists) → A.4 returns wrapped ErrCSFRestoreCronTargetExists; A.5
  does NOT run.
- Manifest absent (pre-PR-26 host) → unchanged: graceful soft-skip
  with operator warning, control falls through to A.5.
- Manifest clean → unchanged: restore both files, fall through to
  A.5.

Files changed:

cmd/nftban-installer/restore_deps_csf.go
- ErrCSFRestoreCronManifestCorrupt docstring rewritten: now
  documents hard-refusal semantics (was: informational soft-skip).
  Wording updated: "refusing before A.5 (operator must inspect)".
- New typed sentinel ErrCSFRestoreCronTargetExists for the
  operator-content-collision case. Distinct from
  ErrCSFRestoreCronManifestCorrupt for cleaner classification: a
  collision is an evidence conflict, not a manifest-trust failure.
- A.4 step rewritten:
    * manifestErr branch now returns the wrapped sentinel instead
      of falling through.
    * Per-entry sha256 verify failure now returns instead of skip.
    * Per-entry unauthorized-Path now returns instead of skip.
    * Per-entry target-exists collision now returns
      ErrCSFRestoreCronTargetExists instead of skip.
    * Per-entry WriteFileAtomic failure now returns instead of skip.
    * Chown failure remains soft (logged warning, content already
      restored — partial-restore is recoverable; the integrity
      chain is unaffected).

cmd/nftban-installer/restore_deps_csf_test.go
- Renamed + retargeted three tests to assert hard-refusal:
    PR26C2_A4_TargetExists_SkipsRestore
      → PR26C2_A4_TargetExists_HardRefuses_StopsBeforeA5
        + asserts errors.Is(err, ErrCSFRestoreCronTargetExists)
        + asserts NOT mock.CommandCalled("systemctl","start",csf.service)
    PR26C2_A4_SHA256Mismatch_SoftSkip_A5StillRuns
      → PR26C2_A4_SHA256Mismatch_HardRefuses_StopsBeforeA5
        + asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt)
        + asserts A.5 NOT called
    PR26C2_A4_SchemaMismatch_SoftSkip_A5StillRuns
      → PR26C2_A4_SchemaMismatch_HardRefuses_StopsBeforeA5
        + asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt)
        + asserts A.5 NOT called
    PR26C2_A4_UnknownEntryPath_Rejected
      → PR26C2_A4_UnknownEntryPath_HardRefuses_StopsBeforeA5
        + asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt)
        + asserts A.5 NOT called
- 3 new tests pinning the kept-behavior branches:
    PR26C2_A4_HappyPath_ContinuesToA5 — clean restore continues
    PR26C2_A4_ManifestAbsent_ContinuesToA5 — migration soft-skip continues
    PR26C2_A4_ParseFailure_HardRefuses_StopsBeforeA5 — parse failure stops

Push criteria (all met as of this commit):

- manifest absent = migration soft-skip ✓ (test #10 above)
- manifest corrupt/hash mismatch = typed refusal before A.5 ✓ (tests #4, #5, #8, #11)
- target cron path broad writes = impossible ✓ (allow-list + writer scope)
- writer-before-reader invariant = tested ✓ (C1's roundtrip + C2's HappyPath_RestoresBothFiles)
- G4-RESTORE-CRON-MANIFEST-INTEGRITY = PASS (local replay clean)
- go test ./... + race + vet = PASS on lab2

Verified on lab2 (Ubuntu 24.04, go1.22.2):
- go build ./... clean
- go test ./... (full repo) PASS
- go test -race -count=1 cmd + restore + state + switchop PASS
- go vet ./... clean
- 11 TestCSFMutate_PR26C2_* tests all PASS (3 hard-refusal tests
  retargeted; 1 unchanged; 7 unchanged or new)
- existing PR-25 / PR-26-code-A / PR-26-code-B tests all still pass
- G4-RESTORE-CRON-MANIFEST-INTEGRITY local replay: FAIL=0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

…STORE-EXEC-NO-OUT-OF-TARGET

Classification: CI gate stale after authorized A.4 write became real,
not a production-code defect.

The G4-RESTORE-EXEC-NO-OUT-OF-TARGET gate was authored before A.4
became real (PR-25 commit 5 + tightened in PR-26-code-B). At that
time, A.4 was a soft-skip with no legitimate file-write path, so a
broad \bexec\.WriteFileAtomic\( forbid was correct.

PR-26-code-C2 changed that: A.4 now legitimately writes to
/etc/cron.d/csf-cron and /etc/cron.d/lfd-cron (and ONLY those two
paths) when the §42.2 manifest is present and integrity-clean. The
broad forbid is now stale and trips on legitimate code.

Resolution per auditor verdict + operator decision: drop the
\bexec\.WriteFileAtomic\( line from G4-RESTORE-EXEC-NO-OUT-OF-TARGET
forbidden_patterns and rely on the dedicated
G4-RESTORE-CRON-MANIFEST-INTEGRITY gate (added in commit 93e86e2)
to authorize and constrain A.4 writes structurally:

  G4-RESTORE-EXEC-NO-OUT-OF-TARGET   = forbid broad / unrelated
                                       mutation surfaces
  G4-RESTORE-CRON-MANIFEST-INTEGRITY = authorize and constrain the
                                       exact A.4 cron-restore writes
                                       (writer + reader symbol pin,
                                       cron-target literal allow-list,
                                       sha256-helper presence)

Carving line-exceptions into the EXEC gate was rejected — that
recreates the regex-brittleness class flagged at PR #515. Two
gates with separate scopes is cleaner than one gate with carve-outs.

Files changed: only .github/workflows/ci-restore-canonization.yml.
- Removed pattern: '\bexec\.WriteFileAtomic\('
- Added explanatory comment block above the forbidden_patterns
  pointing at G4-RESTORE-CRON-MANIFEST-INTEGRITY for cron-write
  authorization.

Kept (unchanged):
- os.WriteFile / os.Create / os.Remove / os.Rename / exec.Command
  forbids
- ServiceMask / ServiceDisable / DaemonReload forbids
- raw mutating Run("systemctl", verb, …) forbids (9 verbs)
- raw Run("mv", …) forbid
- NftDeleteTable allow-list pin (ip:nftban / ip6:nftban only)
- §46.1 line-skipping discipline
- G4-RESTORE-CRON-MANIFEST-INTEGRITY gate (entirely)

Local replay (exact CI workflow bash, against PR-26-code-C head):

  G4-RESTORE-EXEC-NO-OUT-OF-TARGET   fail=0
  G4-RESTORE-CRON-MANIFEST-INTEGRITY fail=0

No production code touched. Production semantics from C1 + C2 + the
hard-refusal fix (f7be0c4) all unchanged. Pre-PR-26 hosts continue
to soft-skip; A.4 hard-refuses on corrupt evidence; A.5 only runs
when restore evidence is trusted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@itcmsgr itcmsgr merged commit 6d8386d into main Apr 28, 2026
63 checks passed
@itcmsgr itcmsgr deleted the feat/v1.100-pr26-code-c-cron-manifest branch April 28, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant