From 84ef5818d5092ef091543f8c20aa26a18a5321c4 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 19 Oct 2026 21:11:46 +0200 Subject: [PATCH] security-scan gate (shared felhom.eu/scripts/security_scan_gate.py) in the fast set and CI, warn-first, with the accepted-hits file and decoys (R-986) --- .gitea/workflows/gates.yml | 3 +++ .security-accepted | 18 ++++++++++++++++++ CHANGELOG.md | 6 ++++++ scripts/agent_gates.py | 10 ++++++++++ scripts/test_gate_decoys.py | 20 ++++++++++++++++++++ 5 files changed, 57 insertions(+) create mode 100644 .security-accepted diff --git a/.gitea/workflows/gates.yml b/.gitea/workflows/gates.yml index 72b17f9..d193b95 100644 --- a/.gitea/workflows/gates.yml +++ b/.gitea/workflows/gates.yml @@ -64,6 +64,9 @@ jobs: env: # In-cluster, so the check does not depend on public DNS or the ingress TLS chain. GITEA_BASE: http://gitea.gitea-system.svc.cluster.local:3000 + # R-986: the security-scan gate fetches the PINNED gitleaks here (sha256-checked); no Go, so govulncheck says + # NOT CHECKED in CI, out loud. Warn-first either way: the scan lists, it never fails this job. + FELHOM_SCAN_FETCH: "1" run: cd ws/felhom-agent && python3 scripts/agent_gates.py - name: Manual proof - alarm key present, optional forced failure (dispatch only, R-1129) diff --git a/.security-accepted b/.security-accepted new file mode 100644 index 0000000..9f80b3e --- /dev/null +++ b/.security-accepted @@ -0,0 +1,18 @@ +# .security-accepted — hits security_scan_gate.py (felhom.eu/scripts, R-986) lists as ACCEPTED, not WARN. +# One per line: # reason. govulncheck: OSV id + module dir; gitleaks: rule@ + file. +# The first scan's verdicts (2026-10-19): felhom.eu documentation/audits/night-2026-10-19/P7/triage.md +gitleaks generic-api-key@1ad7cc8eb57e internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@46ce5a3eccc4 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@46fe126846b0 internal/escrow/identity_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@56474f1754f5 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@631c53997f7e internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@6e790ef41ef3 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@78334b854242 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@7b88d323a44f internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@b0eeee93cf24 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@b1cb2659967a internal/localapi/escrow_stage_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@b66bec43cc8a internal/escrow/recover_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@e0525160fe03 internal/pbsdr/wrapper_read_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@e442e455c360 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@f23f93f461b2 internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@f471b78e0f6e internal/escrow/recover_retained_test.go # false: a test constant or a decoy value, no live secret diff --git a/CHANGELOG.md b/CHANGELOG.md index e036d45..c71c8b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ sudoers, wrapper or config-bundle change. `TestStaleLock_R1010_ForcesOnceOnlyWhenTheVolumeIsGone` (6 rows) and `TestDeleteSnapshotForce_R1010`, red-proved (single plain delete → `calls [plain], want [plain force]`). +- **The security-scan gate (R-986), tooling only:** `agent_gates.py` registers the SHARED + `felhom.eu/scripts/security_scan_gate.py` (FAST set: the hook and CI) — govulncheck (called vulnerabilities) and gitleaks + on the pushed commits, **warn-first**. `.security-accepted` holds the first scan's 15 accepted hits (escrow and + local-API test constants), each with a reason. CI fetches the pinned gitleaks (`FELHOM_SCAN_FETCH=1`). Decoys: the + shared 8 plus the gate on this repo, in `test_gate_decoys.py`. + ### Earlier on 2026-10-19 (texts only) - The ceremony-complete log line now says the package uploads at the claim **or at the typed-back confirm** (R-1151); diff --git a/scripts/agent_gates.py b/scripts/agent_gates.py index 39070c7..c5c0798 100644 --- a/scripts/agent_gates.py +++ b/scripts/agent_gates.py @@ -15,6 +15,9 @@ Gates (all must pass; **non-zero exit on any failure**): or because someone is offline on a train. CI runs the FULL set for exactly this reason: it is the machine that can afford a network check, and it is the half that emails when something is wrong. + … security-scan govulncheck (called vulnerabilities) + gitleaks on the pushed commits, WARN-FIRST: it lists, + it never refuses (R-986). Shared: felhom.eu/scripts/security_scan_gate.py. In the fast set + although it reads vuln.go.dev — offline it says NOT CHECKED, so it cannot stop a push. … gate-decoys the decoy suite (test_gate_decoys.py): every gate here still convicts its decoys (R-1116). ~20 s in the workspace, so the full run only: not the hook, but CI runs the full set on every push. Scratch clones and a local fake Gitea only. @@ -49,6 +52,9 @@ SHARED_INSTRUCTIONS = os.path.join( # R-389 — shared, like the two above: it lives in felhom.eu/scripts/ and is never copied. SHARED_OBSERVATIONS = os.path.join( os.path.dirname(ROOT), "felhom.eu", "scripts", "observations_gate.py") +# R-986 — shared too: the dependency and secret scanner, warn-first. +SHARED_SECURITY = os.path.join( + os.path.dirname(ROOT), "felhom.eu", "scripts", "security_scan_gate.py") # (label, absolute script path, args, fast) GATES = [ @@ -70,6 +76,10 @@ GATES = [ # R-1160 (2026-10-19) — every Go file is gofmt-clean; the controller's gofmt_gate.py shape. Fast: one gofmt -l. # Where the runner has no Go it says NOT CHECKED out loud and passes; the pre-push hook (with Go) enforces it. ("gofmt", os.path.join(ROOT, "scripts", "gofmt_gate.py"), [], True), + # R-986 (2026-10-19) — govulncheck + gitleaks, WARN-FIRST (exit 0 whatever it finds). Fast although it reads + # vuln.go.dev: it cannot refuse, so a network blink is a NOT CHECKED line. ~4 s here. CI: gitleaks fetched pinned + # (FELHOM_SCAN_FETCH=1), no Go, so govulncheck says NOT CHECKED there. + ("security-scan", SHARED_SECURITY, [ROOT], True), ] VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"} diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index 32f538c..df65df7 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -69,6 +69,11 @@ COVERS = { "gofmt": "R-1160: an unformatted .go file planted under internal/ of a SCRATCH copy is convicted by name - vs the " "same tree formatted (passes); with no gofmt reachable, CI passes OUT LOUD and a dev machine is " "INCONCLUSIVE, never a silent pass", + "security-scan": "R-986: the shared gate's own decoys (felhom.eu scripts/test_security_scan_gate.py, 8 cases in " + "SCRATCH git repos: a planted token listed by rule and file, its value never printed; a second " + "secret on another line of an accepted file still listed; a called govulncheck finding listed, " + "an imported-only one not; no tool is NOT CHECKED out loud), run from here; plus the gate on THIS " + "repo's tree: exit 0 and its summary line (warn-first - the listing is the fact)", "gate-decoys": "this suite, run as a gate of agent_gates.py's full run, which CI runs on every push (R-1116): " "every case above is a decoy of its own gate, and ONE case that stops convicting turns the run " "red - red-proved 2026-10-17 by flipping a reuse-refs FACT case to expect rc 0", @@ -372,6 +377,20 @@ def shared_cases(ws): u"the same minute.**\n", 0) +def security_cases(): + """R-986: the shared scanner's decoys (they build their own scratch repos), then the gate on this repo.""" + suite = os.path.join(SHARED, "test_security_scan_gate.py") + gate = os.path.join(SHARED, "security_scan_gate.py") + for f in (suite, gate): + if not os.path.isfile(f): + fails.append("shared %s is MISSING beside this clone - a failure, never a skip" % f) + return + rc, out = run([sys.executable, suite], ROOT) + report("security-scan: the shared gate's 8 decoys", rc, out, 0, ("all 8 cases passed",)) + rc, out = run([sys.executable, gate, ROOT], ROOT) + report("security-scan: GENUINE: this repo, warn-first", rc, out, 0, ("security-scan:", "govulncheck")) + + def main(): srv = Server(("127.0.0.1", 0), Handler) threading.Thread(target=srv.serve_forever, daemon=True).start() @@ -383,6 +402,7 @@ def main(): release_cases(base, ws) shared_cases(ws) gofmt_cases(ws) + security_cases() finally: srv.shutdown() srv.server_close()