Skip to content

Commit 4e98ff5

Browse files
itcmsgrclaude
andauthored
feat(v1.100 PR-26-code-A): target-specific safety predicate + firewallType plumbing (§§51.3-51.4) (#514)
PR-26-code-A — restore verification / evidence hardening, slice A. Tightens the safety-net-safe predicate from PR-25's any-external-FW heuristic to the target-specific check operator-locked at §51.3 Option B. Plumbs the resolved firewallType into the inline-verify dep per §51.4. No new mutation primitives. No iptables introspection (Option A explicitly deferred to a future amendment). Authority: - PR #512 merge cd76842 (PR-26-doc / Part IV §§37-50) - PR #513 merge 52fadcc (PR-26 operator lock record §51) Behavior delta (target-specific predicate): - PR-25 / 4B-4: IsSafetyNetRemovalSafe accepted ANY service in {csf, ufw, firewalld, iptables, netfilter-persistent} being active. - PR-26-code-A: IsSafetyNetRemovalSafe requires the RESOLVED TARGET's specific service unit (csf.service for csf restore) to be active. A non-target external firewall being active no longer satisfies CSF restore safety. The §41 looseness is closed. Files changed (5): cmd/nftban-installer/restore_deps.go - productionInlineVerifyDep struct gains read-only firewallType field (constructor-injected; never re-derived per §17.3). - IsSafetyNetRemovalSafe rewritten: * SSH port via detect.SSHPort (kept). * Defensive guard: empty firewallType → ErrInlineVerifyTargetFirewallTypeMissing. * Defensive guard: firewallType not in §18.2 set → ErrInlineVerifyUnknownFirewall. * Amendment-1 §30.2 lock: firewallType != csf → ErrInlineVerifyOnlyCSFAuthorized. * Target-specific check: only the resolved unit's ServiceActive counts. Other external FW services are no longer probed. - IsTargetFirewallActive gains defensive cross-check: caller-passed firewallType must match v.firewallType when the constructor injected one. Mismatch → ErrInlineVerifyTargetMismatch. - New sentinels: ErrInlineVerifyTargetFirewallTypeMissing, ErrInlineVerifyTargetMismatch. - Removed: var inlineVerifyExternalFirewallServices (the any-FW list is dead under §51.3 Option B). - newProductionRestoreDeps + newProductionRestoreDepsWithEvidence signatures take firewallType. Old (4-arg) call sites in production routed through the new 5-arg signature with "" for the back-compat shim; tests pass "csf" or omit per fixture intent. - restoreDepsFactory function-pointer type extended to 5 args. - New helper resolveFirewallTypeForDeps maps TargetAuthority → firewallType (§18.3 invariants + §20 panel mapping). Lives in restore_deps.go (NOT restore_decide.go) to keep TargetAuthorityKind* constants out of dispatcher source — the no-Group-Kind-mapping invariant in TestDispatcher_NoLocalGroupKindMapping stays green. cmd/nftban-installer/restore_decide.go - runRestoreExecutionFromProceed gains a single line: resolves firewallType via resolveFirewallTypeForDeps(target) BEFORE calling the factory. On resolution failure: persists StateRestoreFailedExecution + the standard exit code. - newRestoreDeps call updated to the new 5-arg signature. - No Group→Kind mapping leaks into this file. - No state-machine / exit-code / history-gate change. cmd/nftban-installer/restore_decide_test.go - withFakeDeps + withFakeDepsRecordingEvidence updated to the new 5-arg factory signature. - recordingFactoryCall struct gains firewallType field. cmd/nftban-installer/restore_deps_csf_test.go - TestCSFMutate_4B3csf_PR25NonShipping_PredicateUnwiredByDefault call to newProductionRestoreDepsWithEvidence updated to the 5-arg form. - (Test still asserts the predicate is wired non-nil per 4B-4 lock; PR-26-code-A does not change that wiring — it changes what the predicate evaluates.) cmd/nftban-installer/restore_deps_inlineverify_test.go - All existing IsSafetyNetRemovalSafe tests migrated to use the new newInlineVerifyDepWithTarget(t, mock, firewallType) helper. The tests' semantic assertions are preserved (TrueOnlyWhenTargetFWActive, FalseWhenOnlyEmergencyProtects, FalseWhenSSHPortUnknown, NoMutation, FactoryWiresSafetyNetPredicate, A7_DeletesWhenPredicateTrue, A7_RefusesWhenPredicateFalse, FullRun_ChecksThreeAssertionsOnly). - Test #6 renamed from TrueOnlyWhenExternalFWActive to TrueOnlyWhenTargetFWActive to reflect the §51.3 lock. - 10 new PR-26-code-A tests (TestInlineVerify_PR26A_*): 1. NonTargetFWDoesNotSatisfy (4 sub-cases: ufw / firewalld / iptables / netfilter-persistent active does NOT satisfy CSF restore when csf.service is inactive) 2. EmptyFirewallType_DefensiveGuard 3. FactoryWiresFirewallTypeIntoInlineVerify 4. A7GateUsesTargetSpecificPredicate (mutation-side integration) 5. TargetCSFActive_SafeToRemove (happy path under tightened rule) 6. NonCSFTarget_TypedUnsupported (3 sub-cases: ufw / firewalld / iptables → ErrInlineVerifyOnlyCSFAuthorized) 7. UnknownTarget_TypedUnknown (4 sub-cases: shorewall / pf / CSF / "csf " → ErrInlineVerifyUnknownFirewall) 8. IsTargetFirewallActive_MismatchGuard (caller-passed firewallType must match v.firewallType when injected) 9. OldExternalFWListRemoved_FileScan (compile-time + grep pin that the old any-external-FW list is gone) 10. FactorySignatureCarriesFirewallType (compile-time pin that the factory signature requires firewallType) Constraints honored (per §51.6 entry criteria + operator scope): IN scope: - target-specific safety predicate ✓ - inline verification hardening ✓ - §51.3 Option B semantics (kernel SSH-rule evidence ADVISORY) ✓ - §51.4 firewallType plumbing ✓ - tests proving non-target external FWs do not satisfy CSF restore ✓ OUT of scope (and untouched): - cron backup / A.4 (PR-26-code-C) - typed executor.ServiceUnmask / Rename (PR-26-code-B) - destructive soak (PR-26-code-E) - IptablesRuleExists / iptables introspection (Option A — needs amendment) - iptables / ip6tables / iptables-save / nft list parsing (none added) - repo hygiene / UX / GOTH / metrics / module cleanup (untouched) - service lifecycle audit (untouched) - rebuild/idempotency audit (untouched) - PR-25 state terminals or exit codes (untouched) - main.go history gate (untouched) - restore_deps_csf.go production logic (untouched — only its companion test file received the 1-line factory-signature update) - contract.md (untouched) - workflows (untouched) Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./cmd/nftban-installer/... ./internal/installer/restore/... ./internal/installer/state/... PASS - go test ./... PASS (full suite) - go test -race -count=1 ./cmd/nftban-installer ./internal/installer/restore/... ./internal/installer/state/... PASS - go vet clean - go mod tidy no-op - 10 new PR-26-code-A tests + 18 sub-tests all PASS - TestDispatcher_NoLocalGroupKindMapping still green (helper lives in restore_deps.go, not restore_decide.go) Awaiting auditor pass before push. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 52fadcc commit 4e98ff5

5 files changed

Lines changed: 510 additions & 79 deletions

File tree

cmd/nftban-installer/restore_decide.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,22 @@ func runRestoreExecutionFromProceed(
274274
// values come from the PR-24 path the planner already used —
275275
// the dispatcher does NOT re-probe or re-detect them, per
276276
// INV-PR25-AUTHORITY-IMMUTABILITY (§17.3) + §33 E.7.
277-
deps := newRestoreDeps(exec, log, priorRec, panel)
277+
//
278+
// PR-26-code-A: also resolve firewallType from the target so the
279+
// inline-verify dep's safety predicate is target-specific (§51.3
280+
// Option B + §51.4 firewallType plumbing). For Kind=RecordedPrior
281+
// the value is on the TargetAuthority directly; for Kind=PanelNative
282+
// the value comes from the static §20 panel mapping
283+
// (restore.ResolvePanelFirewall). No precomputed targetUnit drift
284+
// — we pass the raw firewallType identity.
285+
resolvedFirewallType, ftErr := resolveFirewallTypeForDeps(target)
286+
if ftErr != nil {
287+
log.Error("restore execute: firewallType resolution failed: %v", ftErr)
288+
_ = sf.Transition(state.StateRestoreFailedExecution, state.PhaseDetect, ftErr.Error())
289+
log.Result("[NFTBan] restore execution: FAILED at firewallType resolution — %s", ftErr.Error())
290+
return sf.State.ExitCode()
291+
}
292+
deps := newRestoreDeps(exec, log, priorRec, panel, resolvedFirewallType)
278293

279294
// Step C — Execute the §23 six-step sequence.
280295
execRes := restore.Execute(ctx, target, deps)
@@ -302,3 +317,4 @@ func runRestoreExecutionFromProceed(
302317

303318
return sf.State.ExitCode()
304319
}
320+

cmd/nftban-installer/restore_decide_test.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ func withFakeDeps(t *testing.T, fake *fakeDispatcherDeps) {
349349
_ *logging.Logger,
350350
_ *uninstall.PriorRecord,
351351
_ detect.PanelType,
352+
_ string, // PR-26-code-A: firewallType plumbing — fakes ignore it.
352353
) restore.ExecuteDeps {
353354
return restore.ExecuteDeps{
354355
Preflight: fake,
@@ -575,10 +576,11 @@ func readSelfRestoreDecide() (string, error) {
575576
// recordingFactoryCall captures one call to the deps factory so tests
576577
// can assert exactly which evidence reached it.
577578
type recordingFactoryCall struct {
578-
exec executor.Executor
579-
log *logging.Logger
580-
priorRec *uninstall.PriorRecord
581-
panel detect.PanelType
579+
exec executor.Executor
580+
log *logging.Logger
581+
priorRec *uninstall.PriorRecord
582+
panel detect.PanelType
583+
firewallType string // PR-26-code-A: §51.4 plumbing
582584
}
583585

584586
// withFakeDepsRecordingEvidence swaps newRestoreDeps with a factory
@@ -594,12 +596,14 @@ func withFakeDepsRecordingEvidence(t *testing.T, fake *fakeDispatcherDeps) *[]re
594596
log *logging.Logger,
595597
priorRec *uninstall.PriorRecord,
596598
panel detect.PanelType,
599+
firewallType string,
597600
) restore.ExecuteDeps {
598601
calls = append(calls, recordingFactoryCall{
599-
exec: exec,
600-
log: log,
601-
priorRec: priorRec,
602-
panel: panel,
602+
exec: exec,
603+
log: log,
604+
priorRec: priorRec,
605+
panel: panel,
606+
firewallType: firewallType,
603607
})
604608
return restore.ExecuteDeps{
605609
Preflight: fake,

0 commit comments

Comments
 (0)