Skip to content

fix(validator): B80-1 shim — validate_structure() → Go validator authority#369

Merged
itcmsgr merged 1 commit intomainfrom
fix/b80-1-validator-shim
Apr 12, 2026
Merged

fix(validator): B80-1 shim — validate_structure() → Go validator authority#369
itcmsgr merged 1 commit intomainfrom
fix/b80-1-validator-shim

Conversation

@itcmsgr
Copy link
Copy Markdown
Owner

@itcmsgr itcmsgr commented Apr 12, 2026

Summary

B80-1 is closed behaviorally, not structurally. Full shell-file deletion and responsibility split (check_ip_or_port / get_firewall_stats / load_spec / get_live_ruleset) are deferred to v1.81.

This patch does not change rebuild ordering, CLI output format, or caller interfaces. It replaces the body of validate_structure() in cli/lib/nftban/core/nftban_validator.sh with a thin compatibility shim over the Go validator binary (nftban-validate), making validation authority Go-only at runtime while preserving backward compatibility for all existing callers.

What this patch does

  • validate_structure() now calls ${NFTBAN_LIB_DIR}/bin/nftban-validate --json and translates the Go schema into the legacy shell schema {status:OK|WARNING|ERROR, errors[], warnings[], info[]}
  • Legacy text output format preserved byte-for-byte (header, status line, error/warning lists) — cmd_validate.sh:103 prints this directly to the user
  • $1 preserved as output_json boolean flag (true/false) controlling text vs JSON mode
  • Exit codes: 0 (OK / warnings-only), 1 (validation errors / parse failure), 2 (Go binary missing — Amend-1, distinguishes "tool absent" from "tool ran but validation failed")
  • jq-missing fallback (Amend-2): when jq is not installed, shim falls back to grep-based protected-detection with minimal text output, preserving the exit-code contract
  • Fail-closed paths for missing-binary and empty-output use printf (not jq -n) so they work without jq
  • No independent validation logic — no load_spec, no get_live_ruleset, no nft list ruleset, no spec-field references

JSON field mismatch — intentional, documented

nftban status --json currently emits .state (see cmd_status.sh:1562). The new INV-CONS-001 smoke assertion prefers canonical keys first (.status // .protection_state // .health) and falls through to .state for current-main compatibility. If the shell key is later renamed to a canonical form, the test picks it up automatically without code change.

Scope fence

TOUCHED:
  validate_structure() body in nftban_validator.sh   (ONLY this function)
  smoke_test.sh — ONE new assertion block            (INV-CONS-001)
  NEW test file test_validate_structure_is_shim.sh   (static regression guard)

NOT TOUCHED:
  check_ip_or_port() / get_firewall_stats()          (v1.81 targets)
  load_spec() / get_live_ruleset()                   (left defined, now unused)
  cmd_check.sh / cmd_firewall.sh / cmd_status.sh     (zero caller changes)
  cmd_validate.sh                                    (zero changes — it calls validate_structure as before)
  internal/nftbanconf/commands.go ValidatorScript()   (unchanged)
  no file rename, no file deletion

Patch-size framing

The raw diff reads 477 / -167, but the runtime behavioral change is narrow:

  • ~60 lines actual functional shim body
  • ~50 lines Amend-2 jq-missing fallback path (compatibility scaffolding)
  • ~30 lines NOTE comment block inside the shim
  • ~40 lines new INV-CONS-001 smoke assertion in smoke_test.sh
  • ~260 lines new test_validate_structure_is_shim.sh (17-assertion regression guardrail)
  • ~130 lines removed from the old batch-jq validate_structure() body

Risk surface = the 60-line shim body. Everything else is guardrail, comment, or test.

Tests (three layers of proof)

1. Static regression guard — tests/test_validate_structure_is_shim.sh (17 assertions)

Positive: body calls Go binary, builds path via $NFTBAN_LIB_DIR, contains B80-1 NOTE, preserves legacy JSON schema, maps severity levels

Negative: body does NOT call load_spec/get_live_ruleset, does NOT use --slurpfile/nft list ruleset/spec field names, body ≤260 lines, jq ≤12 invocations, has jq-missing fallback (Amend-2 contract), uses return 2 on missing binary (Amend-1 contract)

Scope fence: file still exists, check_ip_or_port() and get_firewall_stats() preserved

2. Runtime INV-CONS-001 smoke — added to smoke_test.sh run_runtime_tests

Compares nftban status --json state (extraction: .status // .protection_state // .health // .state // empty) against nftban-validate --json .status, uppercase-normalized. First positive enforcement of INV-CONS-001 at the top-level CLI truth surface.

3. Behavioural equivalence — verified locally via fake Go binary

Go input Shell output Exit
protected, findings=[] {status:OK, errors:[], warnings:[]} 0
degraded, findings=[critical, error] {status:ERROR, errors:[CRITICAL:…]} 1
protected, findings=[warn] {status:WARNING, warnings:[WARNING:…]} 0
(binary missing) {status:ERROR, errors:[CRITICAL: Go validator not found…]} 2

Text rendering preserved (header + ❌ ERRORS (N): + message lines).

Test plan

  • Static regression guard: 17/17 PASS locally
  • Behavioural equivalence: 15/15 PASS locally (hermetic, offline)
  • BotGuard rebuild sequencing (cross-regression check): 10/10 PASS
  • CI pipeline
  • lab2 Day-6 rebuild (GATE 8 — separate workstream, not a merge gate for this PR)
  • srv1 72h anchor observation (ongoing soak, not gated by this PR)

Refs

  • V1.80_ROADMAP/MASTER_TODO.md B80-1 (truth consolidation)
  • V1.80_ROADMAP/10_EXECUTION_PLAN.md Day 2 (legacy validator removal)
  • B80-2 is separately a no-op: cmd_status.sh already calls Go binary only (line 131). Marked CLOSED as already-satisfied.

🤖 Generated with Claude Code

…ority

Option B (shim) per discussion 2026-04-11, with Amend-1/2/3 from
2026-04-13 review.

The function validate_structure() in cli/lib/nftban/core/nftban_validator.sh
is now a thin shim over ${NFTBAN_LIB_DIR}/bin/nftban-validate (the Go
validator). The body:

  1. calls the Go binary via NFTBAN_LIB_DIR/bin/nftban-validate --json
  2. translates the Go schema {status:protected|degraded|down, findings[]}
     into the legacy shell schema {status:OK|WARNING|ERROR, errors[],
     warnings[], info[]}
  3. preserves the legacy text output format byte-for-byte so existing
     callers in cmd_validate.sh and cmd_firewall.sh are unaffected
     (cmd_validate.sh:103 prints validate_structure stdout directly to
     the user — losing the output format would regress `nftban validate`)
  4. fails CLOSED with distinct exit codes:
       return 2 — Go binary not found/not executable  (Amend-1)
       return 1 — Go binary empty/unparseable output OR validation errors
       return 0 — OK or warnings-only
  5. has a jq-missing graceful fallback (Amend-2): when jq is not
     installed the shim falls back to grep-based protected-detection,
     emits a minimal text banner, and preserves the exit-code contract
  6. has NO independent fallback validation logic — that would re-create
     the exact drift risk B80-1 is closing

Amend-1: missing Go binary → return 2 (distinct from 1=validation-failed)
  — matches Unix convention for "prerequisite missing" vs "check failed"
  — Go binary missing is a broken-installation case; exit 2 lets callers
    distinguish it from a real validation failure

Amend-2: jq-missing fallback
  — if command -v jq fails, shim bypasses the jq schema-translation path
    and uses grep for `"status":"protected"` on the Go output
  — preserves the exit-code contract even on jq-broken environments
  — fail-closed paths for missing-binary and empty-output use hand-built
    JSON via printf (not jq -n) so they work without jq

Amend-3: INV-CONS-001 smoke now reads nftban status --json
  — extraction hierarchy: .status // .protection_state // .health // .state
  — the first three are the "canonical" keys from the truth hierarchy
  — .state is the ACTUAL key currently emitted by cmd_status.sh:1562
    output_json() (verified 2026-04-13 via source read)
  — the fall-through order means if the shell key is later renamed to
    the canonical .status, this test automatically picks up the rename
  — comparison is uppercase-normalized

Scope fence (NOT touched):
  - check_ip_or_port()          — v1.81 migration target
  - get_firewall_stats()        — v1.81 migration target
  - load_spec(), get_live_ruleset() — still defined but now unused
                                      (removal = v1.81 cleanup)
  - cmd_check.sh, cmd_firewall.sh — no caller changes
  - internal/nftbanconf/commands.go ValidatorScript() — unchanged
  - no file rename, no file deletion

Tests (three layers of proof):

1. Static regression guard (hermetic) — NEW
   cli/lib/nftban/tests/test_validate_structure_is_shim.sh
   17 assertions across:
     positive invariants (shim MUST call Go binary, preserve legacy
       schema, contain B80-1 NOTE comment)
     negative invariants (body MUST NOT call load_spec/get_live_ruleset,
       MUST NOT use --slurpfile/nft list ruleset/spec field names, body
       length ≤260 lines, jq invocations ≤12, has jq-missing fallback,
       uses return 2 on missing binary)
     scope-fence guardrails (check_ip_or_port and get_firewall_stats
       preserved; file still exists — deletion is v1.81 scope)

2. Runtime INV-CONS-001 smoke check — added to smoke_test.sh
   run_runtime_tests. Compares `nftban status --json` state against
   `nftban-validate --json` status with uppercase normalization.
   Extraction order .status // .protection_state // .health // .state
   preserves the intended truth hierarchy while working on current main.
   This is the first positive enforcement of INV-CONS-001 at the
   top-level CLI truth surface.

3. Behavioural equivalence (offline, verified locally)
   5 scenarios (protected / degraded_critical / warn_only / text /
   missing-binary) verified via fake Go binary against the shim:
     protected          → OK  + exit 0
     degraded_critical  → ERROR with CRITICAL: prefix + exit 1
     warn_only          → WARNING with WARNING: prefix + exit 0
     text rendering     → legacy header + ❌ ERRORS (N): + message lines
     missing binary     → ERROR fail-closed + exit 2 (Amend-1)
   jq-missing fallback path verified at the static-guard layer (N8);
   behavioural test would require brittle environment manipulation.

B80-2 is separately a no-op: cmd_status.sh on main already calls the
Go binary directly via _NFTBAN_VALIDATOR_BIN at line 131 and has no
shell validator fallback references. B80-2 marked CLOSED as
already-satisfied (roadmap doc update in a follow-up commit).

Refs: V1.80_ROADMAP/MASTER_TODO.md B80-1 (truth consolidation)
Refs: V1.80_ROADMAP/10_EXECUTION_PLAN.md Day 2 (legacy validator removal)

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

Dependency Review

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

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 982387e.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@itcmsgr itcmsgr merged commit fc7004c into main Apr 12, 2026
4 checks passed
@itcmsgr itcmsgr deleted the fix/b80-1-validator-shim branch April 12, 2026 21:28
itcmsgr added a commit that referenced this pull request Apr 13, 2026
v1.80 closes the structural truth-surface hardening line. Protection
state now fails correctly for broken kernel structure, empty required
chains, dead required runtime, schema drift, and duplicate schema
authority.

v1.80.0 does not change the effective detection/scoring model;
effectiveness tuning remains future work (BUG-1 / v1.81+).

Blockers closed: B80-1 through B80-8 + BUG-6 (NOT-A-BUG).
PRs: #368, #369, #370, #371, #372, #373.

Bumps VERSION 1.79.3 -> 1.80.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
itcmsgr added a commit that referenced this pull request Apr 13, 2026
v1.80 closes the structural truth-surface hardening line. Protection
state now fails correctly for broken kernel structure, empty required
chains, dead required runtime, schema drift, and duplicate schema
authority.

v1.80.0 does not change the effective detection/scoring model;
effectiveness tuning remains future work (BUG-1 / v1.81+).

Blockers closed: B80-1 through B80-8 + BUG-6 (NOT-A-BUG).
PRs: #368, #369, #370, #371, #372, #373.

Bumps VERSION 1.79.3 -> 1.80.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
itcmsgr added a commit that referenced this pull request Apr 13, 2026
v1.80 closes the structural truth-surface hardening line. Protection
state now fails correctly for broken kernel structure, empty required
chains, dead required runtime, schema drift, and duplicate schema
authority.

v1.80.0 does not change the effective detection/scoring model;
effectiveness tuning remains future work (BUG-1 / v1.81+).

Blockers closed: B80-1 through B80-8 + BUG-6 (NOT-A-BUG).
PRs: #368, #369, #370, #371, #372, #373.

Bumps VERSION 1.79.3 -> 1.80.0.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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