From a03c25aa1f2a0b5e5fdadccd485dbdb3551b9715 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 19 Oct 2026 21:11:31 +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 | 4 ++++ .security-accepted | 7 +++++++ CHANGELOG.md | 9 +++++++++ controller/scripts/controller_gates.py | 9 +++++++++ controller/scripts/test_gate_decoys.py | 27 ++++++++++++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 .security-accepted diff --git a/.gitea/workflows/gates.yml b/.gitea/workflows/gates.yml index 29a81a7..49a01bf 100644 --- a/.gitea/workflows/gates.yml +++ b/.gitea/workflows/gates.yml @@ -52,6 +52,10 @@ jobs: - name: Run the gate entry point # The ONLY thing CI runs. No go build, no go test, no linting, no deploy. The # exit code IS the result: no `|| true`, no pipe that could swallow it. + env: + # 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-controller/controller && python3 scripts/controller_gates.py --fast - 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..9af0304 --- /dev/null +++ b/.security-accepted @@ -0,0 +1,7 @@ +# .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@93610586e88d controller/internal/stacks/samba_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@a882b8f2ea6b controller/internal/backup/offbox_offer_shapec_r241_test.go # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@b3c473a524c2 controller/scripts/test_gate_decoys.py # false: a test constant or a decoy value, no live secret +gitleaks generic-api-key@c9c6aa2fcae6 controller/scripts/test_gate_decoys.py # false: a test constant or a decoy value, no live secret diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e2288..e19b701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased (2026-10-19) — the security-scan gate (R-986); no code change + +- `controller_gates.py` registers `security-scan` (FAST set, so the pre-push hook and CI run it): the SHARED + `felhom.eu/scripts/security_scan_gate.py` — govulncheck (called vulnerabilities) on `controller/` and gitleaks on the + pushed commits, **warn-first** (it lists, it never refuses). `.security-accepted` at the repo root holds the first + scan's 4 accepted hits (test constants and decoy values), each with a reason. CI fetches the pinned gitleaks + (`FELHOM_SCAN_FETCH=1` in `gates.yml`). Decoys: the shared 8 run from `test_gate_decoys.py`, plus the gate on this + repo. First scan: 0 called vulnerabilities, 0 real secrets (felhom.eu `audits/night-2026-10-19/P7/triage.md`). + ## v0.335.0 — no app stop for an off-site tier the agent will refuse because its host is busy (R-921) (2026-10-19) **MinAgent: 0.158.1** — the floor's, carried. The new check needs the agent's `host_busy` answer (felhom-agent, diff --git a/controller/scripts/controller_gates.py b/controller/scripts/controller_gates.py index 4f741f8..aa8654c 100644 --- a/controller/scripts/controller_gates.py +++ b/controller/scripts/controller_gates.py @@ -18,6 +18,9 @@ Gates, in order (all must pass; **non-zero exit on any failure**): 8. debug-routes every debug-page control resolves to a handler, and back (R-400) 9. reuse-refs every path cited by this repo's REUSE.md still resolves … gofmt every Go file is gofmt-clean (R-454) + … 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). Fast + 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 above still convicts its decoys (R-1116). ~22 s and it edits real files in this tree for a moment, so the full run only, never --fast (never the hook, never CI) @@ -60,6 +63,8 @@ SHARED_REUSE = os.path.join(SHARED_DIR, "reuse_refs_check.py") SHARED_INSTRUCTIONS = os.path.join(SHARED_DIR, "instructions_gate.py") # R-389 — shared, like the two above: it lives in felhom.eu/scripts/ and is never copied. SHARED_OBSERVATIONS = os.path.join(SHARED_DIR, "observations_gate.py") +# R-986 — shared too: the dependency and secret scanner, warn-first. +SHARED_SECURITY = os.path.join(SHARED_DIR, "security_scan_gate.py") # R-404 — the golden NOTICE. Lives here, beside the runner, because it is about THIS repo's # releases; it imports the felhom.eu gate rather than copying its comparison. @@ -105,6 +110,10 @@ GATES = [ # R-454 — every Go file is gofmt-clean. `go vet` does not check formatting; nothing did. Fast: # the toolchain's own formatter, no network, no container runtime. ("gofmt", os.path.join(SCRIPTS, "gofmt_gate.py"), [], True, True), + # R-986 (2026-10-19) — govulncheck + gitleaks, WARN-FIRST (exit 0 whatever it finds; a hit is listed). Fast + # although it reads vuln.go.dev: it cannot refuse, so a network blink is a NOT CHECKED line. ~8 s here. CI fetches + # the pinned gitleaks (FELHOM_SCAN_FETCH=1) and has no Go, so govulncheck says NOT CHECKED there. + ("security-scan", SHARED_SECURITY, [REPO], True, True), # R-404 — ADVISORY. Reports the golden debt where it is created; never refuses. ("golden-notice", GOLDEN_NOTICE, [REPO], True, False), # R-1116 (2026-10-17) — the gates' OWN decoy suite: every gate above, red-proved against its decoys. It was diff --git a/controller/scripts/test_gate_decoys.py b/controller/scripts/test_gate_decoys.py index 62c5df9..f4646f7 100644 --- a/controller/scripts/test_gate_decoys.py +++ b/controller/scripts/test_gate_decoys.py @@ -50,6 +50,11 @@ COVERS = { "key left out of the map (v0.252.0, R-557); a call that lost an argument, and a key glued " "to more text with + (R-576)", "gofmt": "R-454: a planted unformatted .go file in internal/ is convicted; the clean tree passes", + "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: exit 0 and its summary line (warn-first)"), "offbox-rename": "R-425: NAS branding in a NEW backups*.html, and in a bundle value an offbox Go file " "names; control: the same token in the network-storage feature's copy is accepted", "docker-v": "R-426: an unallowlisted `-v /etc:/x` in a NEW .go file two directories down " @@ -506,6 +511,28 @@ def shared_cases(): shared_cases() + +def security_cases(): + """R-986: the shared scanner's decoys (they build their own scratch repos), then the gate on this repo.""" + global ran + suite, gate_ = os.path.join(SHARED, "test_security_scan_gate.py"), 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 + for name, argv, must in (("security-scan: the shared gate's 8 decoys", [sys.executable, suite], "all 8 cases passed"), + ("security-scan: GENUINE: this repo, warn-first", [sys.executable, gate_, REPO], + "security-scan:")): + ran += 1 + rc, out = _run(argv, REPO) + if rc != 0 or must not in out: + fails.append("%s: rc=%d, %r missing\n%s" % (name, rc, must, out[-700:])) + else: + print(" ok %s" % name) + + +security_cases() + print() if fails: for f in fails: