security_scan_gate.py: govulncheck (called vulns) + gitleaks on the pushed commits in every repo's fast set and CI, warn-first, with an accepted-hits file and 8 decoys (R-986); first scan triaged: 0 real secrets, 0 called vulnerabilities; evidence for R-953 and the 0.335.0 release
gates / gates (push) Failing after 12m59s

This commit is contained in:
2026-10-19 21:03:23 +02:00
parent d92a712ee4
commit 15f7428871
15 changed files with 811 additions and 2 deletions
+4
View File
@@ -184,6 +184,10 @@ jobs:
# which prints at the moment a release is committed, where someone can actually act on it.
# Those are the compensating controls that make this green honest. Every other gate still
# fails this job on any push, and golden-currency still fails it on a push touching code.
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: python3 scripts/repo_gates.py --fast --scope="${PUSH_SCOPE:-code}"
- name: Manual proof - alarm key present, optional forced failure (dispatch only, R-1129)
+37
View File
@@ -0,0 +1,37 @@
# .security-accepted — hits security_scan_gate.py (felhom.eu/scripts, R-986) lists as ACCEPTED, not WARN.
# One per line: <tool> <id> <where> # reason. govulncheck: OSV id + module dir; gitleaks: rule@<sha256[:12] of the line> + file.
# The first scan's verdicts (2026-10-19): felhom.eu documentation/audits/night-2026-10-19/P7/triage.md
gitleaks generic-api-key@0275c9c51957 documentation/audits/usenet-2026-10-12/sabnzbd-bench/13-compose-resolved.txt # accepted: a bench app's generated admin password and API key (LXC 9401, stopped; app removed from 9202)
gitleaks generic-api-key@0a189ae60ea4 documentation/tests/campaign2-evidence-2026-07-07/state/runner.log # accepted: the same dry-run deploy values in the campaign runner's log
gitleaks generic-api-key@2a27df76f661 documentation/audits/usenet-2026-10-12/sabnzbd-bench/02-telemetry-and-net.txt # accepted: a bench app's generated API key (LXC 9401, stopped; app removed from 9202 at teardown)
gitleaks generic-api-key@3a4c7b17e349 documentation/audits/usenet-2026-10-12/nzbget-bench/03-process-and-entrypoint.txt # accepted: a bench app's generated control password (LXC 9401, stopped; app removed from 9202 at teardown)
gitleaks generic-api-key@3c180a9f957c documentation/tests/campaign2-evidence-2026-07-07/state/runner.log # accepted: the same dry-run deploy values in the campaign runner's log
gitleaks generic-api-key@486819f82f58 documentation/audits/update-night-2026-09-21/apps/actualbudget/log.txt # accepted: a demo app's session token, demo-hp 2026-09-21 (Tier 0); no running demo app has it (2026-10-12 review)
gitleaks generic-api-key@486819f82f58 documentation/audits/update-night-2026-09-21/batchA.log # accepted: the same demo app session token as actualbudget/log.txt
gitleaks generic-api-key@5a64c6ffd6df documentation/audits/signup-lock-2026-09-29/B/wd.sh # accepted: a stranger-probe test password in an audit script (signup lock, Tier 0)
gitleaks generic-api-key@6e2e899b7d6e documentation/audits/offsite-lock-build-2026-10-03/partD/demo-hp-migration.txt # false: an SSH key FINGERPRINT in a log line, not a key
gitleaks generic-api-key@70380348263d hub/internal/web/bind_language_test.go # false: a test constant or a decoy value, no live secret
gitleaks generic-api-key@7134caf03160 documentation/audits/evidence-rehearsal-2026-08-09/GATE0-demo-hp-before.txt # false: a WireGuard PUBLIC key
gitleaks generic-api-key@7134caf03160 documentation/audits/evidence-rehearsal-2026-08-09/P1-demo-hp-after.txt # false: a WireGuard PUBLIC key
gitleaks generic-api-key@71ba98258279 hub/internal/api/r1140_accesslog_test.go # false: a test constant or a decoy value, no live secret
gitleaks generic-api-key@7aa746ddb210 documentation/audits/offsite-lock-build-2026-10-03/partD/demo-hp-migration.txt # false: an SSH key FINGERPRINT in a log line, not a key
gitleaks generic-api-key@7ce547845b1e documentation/audits/night-rulings-2026-09-30/D/D3-wanderer-routes-probe.txt # accepted: a throwaway app's session cookie (wanderer probe, 2026-09-30, Tier 0)
gitleaks generic-api-key@8044eef94360 documentation/tests/campaign2-evidence-2026-07-07/state/runner.log # accepted: the same dry-run deploy values in the campaign runner's log
gitleaks generic-api-key@85b662612937 hub/internal/store/r879_box_seal_test.go # false: a test constant or a decoy value, no live secret
gitleaks generic-api-key@9530a022fde8 documentation/audits/usenet-2026-10-12/sabnzbd-bench/13-compose-resolved.txt # accepted: a bench app's generated admin password and API key (LXC 9401, stopped; app removed from 9202)
gitleaks generic-api-key@9b3a9c2be48a documentation/tests/campaign2-evidence-2026-07-07/evidence/T-DEPLOY-SET/values_bookstack.json # accepted: dry-run deploy values of a 2026-07-07 campaign (generated, demo boxes since rebuilt)
gitleaks generic-api-key@a9538cfd7063 documentation/tests/campaign2-evidence-2026-07-07/evidence/T-DEPLOY-SET/values_paperless-ngx.json # accepted: dry-run deploy values of a 2026-07-07 campaign (generated, demo boxes since rebuilt)
gitleaks generic-api-key@b0414c6f59b9 documentation/audits/offsite-lock-build-2026-10-03/partD/demo-hp-migration.txt # false: an SSH key FINGERPRINT in a log line, not a key
gitleaks generic-api-key@b1db45e463b6 documentation/audits/family-gate-2026-10-02/tools/fg_items.py # false: test strings in an audit tool (websocket probe labels)
gitleaks generic-api-key@b1db45e463b6 documentation/audits/permanent-gate-2026-10-01/tools/items12.py # false: test strings in an audit tool (websocket probe labels)
gitleaks generic-api-key@bd228c851720 documentation/tests/walk5-r201-2026-08-07/journal.md # false: sha256 digests compared in a walk journal, not secrets
gitleaks generic-api-key@c1be98b7b703 documentation/tests/walk5-r201-2026-08-07/journal.md # false: sha256 digests compared in a walk journal, not secrets
gitleaks generic-api-key@c3b2b5665c9c documentation/audits/evidence-fixes-first-tester-2026-09-30/part0/cloudflare-dns.txt # false: a tunnel ID (a UUID), not the tunnel token
gitleaks generic-api-key@cf453385b707 documentation/tests/campaign2-evidence-2026-07-07/state/runner.log # accepted: the same dry-run deploy values in the campaign runner's log
gitleaks generic-api-key@e59eaeb4aee7 documentation/audits/new-apps-2026-10-01/tools/newapp_box.py # accepted: a throwaway test user's password in an audit tool for a fresh-app walk on a Tier-0 box
gitleaks generic-api-key@eada8358f62a hub/internal/web/render_test.go # false: a test constant or a decoy value, no live secret
gitleaks generic-api-key@efd4cc86d8d9 documentation/audits/power-cut-fixes-2026-10-14/E/demo-hp-journal-boot.txt # false: the id of HP's public UEFI certificate in a boot journal
gitleaks generic-api-key@f28c207293c1 documentation/tests/campaign2-evidence-2026-07-07/evidence/T-DEPLOY-SET/values_vaultwarden.json # accepted: dry-run deploy values of a 2026-07-07 campaign (generated, demo boxes since rebuilt)
gitleaks generic-api-key@f399eb9e3059 documentation/audits/night-2026-09-23/chaos/round-01-run.txt # accepted: the same demo app session token as round-01.log
gitleaks generic-api-key@f399eb9e3059 documentation/audits/night-2026-09-23/chaos/round-01.log # accepted: a demo app's session token, demo-hp 2026-09-23 (Tier 0); the 2026-10-12 review found no running demo app with it
gitleaks generic-api-key@fdabf6a76b48 documentation/audits/r890-instructions-2026-10-06/tools/wgerwalk.py # accepted: a throwaway wger walk's API token in an audit tool (Tier 0)
@@ -0,0 +1,46 @@
# R-953 read-back, 2026-10-19 18:21:10 UTC (read only; no sync, no kubectl change)
## ArgoCD app felhom
sync: Synced rev: a28f162c health: Healthy
tracked resources (25):
ConfigMap/filebrowser-config Synced
ConfigMap/git-sync-sparse-checkout Synced
ConfigMap/hub-config Synced
ConfigMap/nginx-config Synced
PersistentVolumeClaim/filebrowser-db Synced
PersistentVolumeClaim/filebrowser-files Synced
PersistentVolumeClaim/hub-data Synced
PersistentVolumeClaim/umami-db-data Synced
Service/contact-mailer Synced
Service/felhom-webpage Synced
Service/filebrowser Synced
Service/hub Synced
Service/umami Synced
Service/umami-db Synced
Deployment/contact-mailer Synced
Deployment/felhom-webpage Synced
Deployment/filebrowser Synced
Deployment/hub Synced
Deployment/umami Synced
Deployment/umami-db Synced
Ingress/contact-mailer Synced
Ingress/felhom-webpage Synced
Ingress/filebrowser Synced
Ingress/hub Synced
Ingress/umami Synced
## the three Secrets (describe: names, labels, annotations, key sizes — no values)
Name: gitea-creds
Labels: <none>
Annotations: <none>
Name: umami-config
Labels: <none>
Annotations: <none>
Name: healthchecks-config
Labels: <none>
Annotations: <none>
NAME TYPE DATA AGE
gitea-creds Opaque 2 241d
umami-config Opaque 2 255d
healthchecks-config Opaque 10 257d
@@ -0,0 +1,70 @@
# R-986 — first scan of today's main, every hit with a verdict (2026-10-19)
Tools: gitleaks 8.24.3 (default rules pinned, no repo ignore file), govulncheck v1.8.0 built with go1.27.2
(v1.1.4, the 2026-10-12 pin, panics on Go 1.27 source — `unexpected expr: *ast.KeyValueExpr` — so the
newest release older than two weeks was taken). Values were never printed: each line was read with every
value-like token replaced by its length and a hash prefix.
## govulncheck (called vulnerabilities only)
| module | called | imported/required only |
|---|---|---|
| felhom-controller `controller` | 0 | 1 |
| felhom.eu `hub` | 0 | 1 |
| felhom-agent `.` | 0 | 1 |
| app-catalog | no Go module | — |
## gitleaks — full history (`--all`)
| repo | commits | hits | 2026-10-12 review | new since |
|---|---|---|---|---|
| felhom-controller | 1370 | 12 | 12 | 0 |
| felhom.eu | 2102 | 50 | 43 | 7 (below) |
| felhom-agent | 539 | 15 | 15 | 0 |
| app-catalog-felhom.eu | 519 | 1 | 1 | 0 |
The 7 new history hits are files still in the tree (the usenet bench 5, the boot journal 1, r1140_accesslog_test.go 1) — their verdicts are in the tree table.
## gitleaks — today's tree (`git archive HEAD`)
Verdicts: **real** = a live secret (none); **accepted** = a value that opens nothing that matters now, listed in the repo's `.security-accepted`; **false** = not a secret.
| repo | n | rule | file | verdict | why |
|---|---|---|---|---|---|
| felhom-controller | 1 | generic-api-key | `controller/internal/backup/offbox_offer_shapec_r241_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-controller | 1 | generic-api-key | `controller/internal/stacks/samba_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-controller | 2 | generic-api-key | `controller/scripts/test_gate_decoys.py` | false | a test constant or a decoy value, no live secret |
| felhom.eu | 1 | generic-api-key | `documentation/audits/evidence-fixes-first-tester-2026-09-30/part0/cloudflare-dns.txt` | false | a tunnel ID (a UUID), not the tunnel token |
| felhom.eu | 1 | generic-api-key | `documentation/audits/evidence-rehearsal-2026-08-09/GATE0-demo-hp-before.txt` | false | a WireGuard PUBLIC key |
| felhom.eu | 1 | generic-api-key | `documentation/audits/evidence-rehearsal-2026-08-09/P1-demo-hp-after.txt` | false | a WireGuard PUBLIC key |
| felhom.eu | 1 | generic-api-key | `documentation/audits/family-gate-2026-10-02/tools/fg_items.py` | false | test strings in an audit tool (websocket probe labels) |
| felhom.eu | 1 | generic-api-key | `documentation/audits/new-apps-2026-10-01/tools/newapp_box.py` | accepted | a throwaway test user's password in an audit tool for a fresh-app walk on a Tier-0 box |
| felhom.eu | 1 | generic-api-key | `documentation/audits/night-2026-09-23/chaos/round-01-run.txt` | accepted | the same demo app session token as round-01.log |
| felhom.eu | 1 | generic-api-key | `documentation/audits/night-2026-09-23/chaos/round-01.log` | accepted | a demo app's session token, demo-hp 2026-09-23 (Tier 0); the 2026-10-12 review found no running demo app with it |
| felhom.eu | 1 | generic-api-key | `documentation/audits/night-rulings-2026-09-30/D/D3-wanderer-routes-probe.txt` | accepted | a throwaway app's session cookie (wanderer probe, 2026-09-30, Tier 0) |
| felhom.eu | 3 | generic-api-key | `documentation/audits/offsite-lock-build-2026-10-03/partD/demo-hp-migration.txt` | false | an SSH key FINGERPRINT in a log line, not a key |
| felhom.eu | 1 | generic-api-key | `documentation/audits/permanent-gate-2026-10-01/tools/items12.py` | false | test strings in an audit tool (websocket probe labels) |
| felhom.eu | 1 | generic-api-key | `documentation/audits/power-cut-fixes-2026-10-14/E/demo-hp-journal-boot.txt` | false | the id of HP's public UEFI certificate in a boot journal |
| felhom.eu | 1 | generic-api-key | `documentation/audits/r890-instructions-2026-10-06/tools/wgerwalk.py` | accepted | a throwaway wger walk's API token in an audit tool (Tier 0) |
| felhom.eu | 2 | generic-api-key | `documentation/audits/signup-lock-2026-09-29/B/wd.sh` | accepted | a stranger-probe test password in an audit script (signup lock, Tier 0) |
| felhom.eu | 1 | generic-api-key | `documentation/audits/update-night-2026-09-21/apps/actualbudget/log.txt` | accepted | a demo app's session token, demo-hp 2026-09-21 (Tier 0); no running demo app has it (2026-10-12 review) |
| felhom.eu | 1 | generic-api-key | `documentation/audits/update-night-2026-09-21/batchA.log` | accepted | the same demo app session token as actualbudget/log.txt |
| felhom.eu | 1 | generic-api-key | `documentation/audits/usenet-2026-10-12/nzbget-bench/03-process-and-entrypoint.txt` | accepted | a bench app's generated control password (LXC 9401, stopped; app removed from 9202 at teardown) |
| felhom.eu | 2 | generic-api-key | `documentation/audits/usenet-2026-10-12/sabnzbd-bench/02-telemetry-and-net.txt` | accepted | a bench app's generated API key (LXC 9401, stopped; app removed from 9202 at teardown) |
| felhom.eu | 2 | generic-api-key | `documentation/audits/usenet-2026-10-12/sabnzbd-bench/13-compose-resolved.txt` | accepted | a bench app's generated admin password and API key (LXC 9401, stopped; app removed from 9202) |
| felhom.eu | 2 | generic-api-key | `documentation/tests/campaign2-evidence-2026-07-07/evidence/T-DEPLOY-SET/values_bookstack.json` | accepted | dry-run deploy values of a 2026-07-07 campaign (generated, demo boxes since rebuilt) |
| felhom.eu | 3 | generic-api-key | `documentation/tests/campaign2-evidence-2026-07-07/evidence/T-DEPLOY-SET/values_paperless-ngx.json` | accepted | dry-run deploy values of a 2026-07-07 campaign (generated, demo boxes since rebuilt) |
| felhom.eu | 1 | generic-api-key | `documentation/tests/campaign2-evidence-2026-07-07/evidence/T-DEPLOY-SET/values_vaultwarden.json` | accepted | dry-run deploy values of a 2026-07-07 campaign (generated, demo boxes since rebuilt) |
| felhom.eu | 7 | generic-api-key | `documentation/tests/campaign2-evidence-2026-07-07/state/runner.log` | accepted | the same dry-run deploy values in the campaign runner's log |
| felhom.eu | 2 | generic-api-key | `documentation/tests/walk5-r201-2026-08-07/journal.md` | false | sha256 digests compared in a walk journal, not secrets |
| felhom.eu | 1 | generic-api-key | `hub/internal/api/r1140_accesslog_test.go` | false | a test constant or a decoy value, no live secret |
| felhom.eu | 1 | generic-api-key | `hub/internal/store/r879_box_seal_test.go` | false | a test constant or a decoy value, no live secret |
| felhom.eu | 1 | generic-api-key | `hub/internal/web/bind_language_test.go` | false | a test constant or a decoy value, no live secret |
| felhom.eu | 1 | generic-api-key | `hub/internal/web/render_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-agent | 1 | generic-api-key | `internal/escrow/identity_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-agent | 11 | generic-api-key | `internal/escrow/recover_retained_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-agent | 1 | generic-api-key | `internal/escrow/recover_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-agent | 1 | generic-api-key | `internal/localapi/escrow_stage_test.go` | false | a test constant or a decoy value, no live secret |
| felhom-agent | 1 | generic-api-key | `internal/pbsdr/wrapper_read_test.go` | false | a test constant or a decoy value, no live secret |
**Real secrets: 0.** app-catalog-felhom.eu: 0 tree hits (its one history hit is a health-probe URL, the 2026-10-12 verdict).
@@ -0,0 +1,2 @@
## per-customer floors 0.335.0 / MinAgent 0.158.1 — 18:22:21 UTC: tester-1, demo-hp, demo-felhom each HTTP 303 Location /customers/<id>?flash=floor_set
## boxes moved (hub reports, 18:38:57 UTC read): demo-felhom 18:37:34 0.335.0; tester-1 18:37:34 0.335.0; demo-hp 18:37:36 0.335.0 (from 0.335.0-rc.1)
@@ -0,0 +1,27 @@
=== demo-hp (enkisfelhom.hu) 18:38:57
containers: 24 running; not healthy/running: traefik Up 3 days;
unhealthy: 0
2026/10/19 18:22:34 updater.go:855: [INFO] [selfupdate] Post-update startup: update successful (0.335.0-rc.1 → 0.335.0)
allow-list refusals since start: 0
2026/10/19 18:22:34 sync.go:142: [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
installed apps (stacks with app.yaml): 12
public https://felhom.enkisfelhom.hu/: 302
public https://files.enkisfelhom.hu/: 200
=== demo-felhom (demo-felhom.eu) 18:38:59
containers: 9 running; not healthy/running: traefik Up 3 days;
unhealthy: 0
2026/10/19 18:22:23 [INFO] [selfupdate] Post-update startup: update successful (0.334.0 → 0.335.0)
allow-list refusals since start: 0
2026/10/19 18:22:24 [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
installed apps (stacks with app.yaml): 5
public https://felhom.demo-felhom.eu/: 302
public https://files.demo-felhom.eu/: 200
=== tester-1 (enkicsifelhom.hu) 18:39:01
containers: 10 running; not healthy/running: traefik Up 3 days;
unhealthy: 0
2026/10/19 18:22:33 [INFO] [selfupdate] Post-update startup: update successful (0.334.0 → 0.335.0)
allow-list refusals since start: 0
2026/10/19 18:22:33 [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
installed apps (stacks with app.yaml): 3
public https://felhom.enkicsifelhom.hu/: 302
public https://files.enkicsifelhom.hu/: 200
@@ -0,0 +1,33 @@
control NEGATIVE (a stub answering 200 over plain http): convicted, as it must (200 -)
== tester-1 (enkicsifelhom.hu): 6 public name(s)
OK control POSITIVE http://felhom.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkicsifelhom.hu/r1033-check?q=1
OK http://files.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://files.enkicsifelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1
OK http://paperless.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paperless.enkicsifelhom.hu/r1033-check?q=1
OK http://paste.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paste.enkicsifelhom.hu/r1033-check?q=1
OK http://wiki.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkicsifelhom.hu/r1033-check?q=1
== demo-hp (enkisfelhom.hu): 14 public name(s)
OK http://arcade.enkisfelhom.hu/r1033-check?q=1 -> 301 https://arcade.enkisfelhom.hu/r1033-check?q=1
OK http://books.enkisfelhom.hu/r1033-check?q=1 -> 301 https://books.enkisfelhom.hu/r1033-check?q=1
OK http://docs.enkisfelhom.hu/r1033-check?q=1 -> 301 https://docs.enkisfelhom.hu/r1033-check?q=1
OK http://felhom.enkisfelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkisfelhom.hu/r1033-check?q=1
OK http://files.enkisfelhom.hu/r1033-check?q=1 -> 301 https://files.enkisfelhom.hu/r1033-check?q=1
OK http://gist.enkisfelhom.hu/r1033-check?q=1 -> 301 https://gist.enkisfelhom.hu/r1033-check?q=1
OK http://ha.enkisfelhom.hu/r1033-check?q=1 -> 301 https://ha.enkisfelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1
OK http://papir.enkisfelhom.hu/r1033-check?q=1 -> 301 https://papir.enkisfelhom.hu/r1033-check?q=1
OK http://paste.enkisfelhom.hu/r1033-check?q=1 -> 301 https://paste.enkisfelhom.hu/r1033-check?q=1
OK http://pdf.enkisfelhom.hu/r1033-check?q=1 -> 301 https://pdf.enkisfelhom.hu/r1033-check?q=1
OK http://plex.enkisfelhom.hu/r1033-check?q=1 -> 301 https://plex.enkisfelhom.hu/r1033-check?q=1
OK http://travel.enkisfelhom.hu/r1033-check?q=1 -> 301 https://travel.enkisfelhom.hu/r1033-check?q=1
OK http://wiki.enkisfelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkisfelhom.hu/r1033-check?q=1
== demo-felhom (demo-felhom.eu): 8 public name(s)
OK http://felhom.demo-felhom.eu/r1033-check?q=1 -> 301 https://felhom.demo-felhom.eu/r1033-check?q=1
OK http://files.demo-felhom.eu/r1033-check?q=1 -> 301 https://files.demo-felhom.eu/r1033-check?q=1
OK http://gist.demo-felhom.eu/r1033-check?q=1 -> 301 https://gist.demo-felhom.eu/r1033-check?q=1
OK http://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1 -> 301 https://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1
OK http://prowlarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://prowlarr.demo-felhom.eu/r1033-check?q=1
OK http://radarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://radarr.demo-felhom.eu/r1033-check?q=1
OK http://sonarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://sonarr.demo-felhom.eu/r1033-check?q=1
OK http://torrent.demo-felhom.eu/r1033-check?q=1 -> 301 https://torrent.demo-felhom.eu/r1033-check?q=1
== every public name redirects http -> https
@@ -0,0 +1,16 @@
== demo-hp
dashboard sign-in: 302
the Fajlok button goes to: https://files.enkisfelhom.hu/_felhom/sso
the pass answer goes to: https://files.enkisfelhom.hu/files/; files cookie set: 1
FileBrowser with the files cookie: felhom-household admin: False modify: True
FileBrowser without it, a fresh browser: HTTP 401
dashboard sign-out: 302
after the sign-out, the files cookie sends the browser to: https://felhom.enkisfelhom.hu/__files/open?to=%2Ffiles%2F
== tester-1
dashboard sign-in: 302
the Fajlok button goes to: https://files.enkicsifelhom.hu/_felhom/sso
the pass answer goes to: https://files.enkicsifelhom.hu/files/; files cookie set: 1
FileBrowser with the files cookie: felhom-household admin: False modify: True
FileBrowser without it, a fresh browser: HTTP 401
dashboard sign-out: 302
after the sign-out, the files cookie sends the browser to: https://felhom.enkicsifelhom.hu/__files/open?to=%2Ffiles%2F
@@ -0,0 +1,13 @@
## global floor 0.335.0 / MinAgent 0.158.1 — 18:39:26 UTC
HTTP/1.1 303 See Other
Location: /configuration?flash=floor_set
## per-box floors cleared — 18:39:26 UTC
tester-1: HTTP/1.1 303 See Other Location: /customers/tester-1?flash=floor_set
demo-hp: HTTP/1.1 303 See Other Location: /customers/demo-hp?flash=floor_set
demo-felhom: HTTP/1.1 303 See Other Location: /customers/demo-felhom?flash=floor_set
## read back (hub.db + WAL, one tar)
min_controller_version|0.335.0
min_controller_version_declared_min_agent|0.335.0=0.158.1
demo-felhom|
demo-hp|
tester-1|
@@ -76,6 +76,23 @@ verify off) — each sits beside a certificate pin, noise. G407/G404 — the exp
backoff jitter uses math/rand, noise. G101 — comments and constant names, noise. G304/G301/G306/G104 — bulk noise,
not filed.
## 5. Proposed push gate
## 5. The push gate (R-986, wired 2026-10-19)
Not wired in this session. Shape: R-986.
`scripts/security_scan_gate.py` (shared by the four repos, in each runner's FAST set and in CI), **warn-first**: it lists
hits and exits 0. govulncheck (called vulnerabilities only) on each Go module; gitleaks on the commits being pushed;
accepted hits per repo in `.security-accepted` (gitleaks by rule + a hash of the hit's line + file, so one accepted
test constant does not accept the next secret in the same file). First scan of every repo's `main` with verdicts:
`documentation/audits/night-2026-10-19/P7/triage.md` (0 real secrets, 0 called vulnerabilities).
| tool | version | how installed | sha256 |
|---|---|---|---|
| govulncheck | **v1.8.0** (2026-09-08), built with go1.27.2 | `GOTOOLCHAIN=go1.27.2 go install golang.org/x/vuln/cmd/govulncheck@v1.8.0` → `tools/security/bin/govulncheck-v1.8.0` (`govulncheck` links to it) | `aec435d4c0b29cb8987d466ae61ed4c334eab7d4b58073aaa873702c50a0da02` |
| gitleaks | 8.24.3 (unchanged) | the same binary; CI fetches the release tarball | tarball `9991e0b2903da4c8f6122b5c3186448b927a5da4deef1fe45271c3793f4ee29c` (the release's own checksums file), binary `e18325d5…69ad` |
**Why the govulncheck pin moved:** v1.1.4 (the 2026-10-12 pin) cannot read the repos since they moved to Go 1.27.2:
built with go1.26 it fails to load the standard library (`unknown field rfd in struct literal of type splicePipe`), and
rebuilt with go1.27.2 it panics in its SSA builder (`unexpected expr: *ast.KeyValueExpr`). v1.8.0 is the newest release
older than two weeks (v1.9.0 is from 2026-10-11). The old binary is kept as `govulncheck-v1.1.4-go1.26.0`.
**CI:** the runner has python3 and git and no Go, so govulncheck says NOT CHECKED there, out loud; the pre-push hook
(with Go) runs it. gitleaks is fetched pinned (`FELHOM_SCAN_FETCH=1` in each `gates.yml`), both checksums checked.
+16
View File
@@ -1,3 +1,19 @@
## 2026-10-19 — a dependency and secret scanner on every push, warn-first (`security_scan_gate.py`, R-986)
New shared gate `scripts/security_scan_gate.py`, registered in all four runners (felhom.eu `repo_gates.py` gate 18,
`controller_gates.py`, `agent_gates.py`, `catalog_gates.py` through `check-security-scan.py`), FAST set, and so in the
pre-push hook and in CI. **Warn-first:** a hit is listed, the exit stays 0; a missing tool, no Go (CI) or no network is
NOT CHECKED out loud. govulncheck v1.8.0 on each Go module, called vulnerabilities only; gitleaks 8.24.3 on the commits
being pushed (`@{upstream}..HEAD`; CI: the pushed commit), default rules pinned (a `.gitleaks.toml` or
`.gitleaksignore` in the tree cannot switch rules off), values redacted. Accepted hits: `.security-accepted` per repo,
with a reason per line — gitleaks by rule + a hash of the hit's line + file. CI (`FELHOM_SCAN_FETCH=1`) fetches the
pinned gitleaks release and checks the tarball's and the binary's sha256. The catalog's CI now clones the felhom.eu
sibling (the gate is shared, never copied). Decoys: `scripts/test_security_scan_gate.py` (8 cases in scratch git repos)
run from felhom.eu's decoy suite and from each other repo's; red-proved (acceptance by rule alone → the second-secret
case FAILs; an imported-only finding counted as called → the govulncheck cases FAIL). The govulncheck pin moved from
v1.1.4 to v1.8.0 (v1.1.4 cannot read Go 1.27 code; `SCANNERS.md` §5). First scan of today's `main` in all four repos:
`documentation/audits/night-2026-10-19/P7/triage.md` — 0 called vulnerabilities, 0 real secrets.
## 2026-10-19 — felhom-host-install.sh 1.35.0 (NOT published): the hub's „live box" 409 is worded (R-1167)
Written for the night brief of 2026-10-19, Part 6; **not published** (publishing is a tag `installer-v1.35.0` plus the
+8
View File
@@ -33,6 +33,9 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
16. decoy-coverage every registered gate in all four repos has a decoy, or a named exemption (R-421)
17. poster-facts WARNS (never fails) when felhom-system-poster.facts.md is newer than the poster
17b. hub-gofmt every hub Go file is gofmt-clean (R-1160; the controller's R-454 shape)
18. security-scan govulncheck (called vulnerabilities) + gitleaks on the pushed commits — WARN-FIRST: it
lists, it never refuses; it touches the network (vuln.go.dev) and says NOT CHECKED
offline, which is why it may sit in the fast set (R-986; shared, felhom.eu/scripts)
**THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already —
it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so
@@ -202,6 +205,11 @@ GATES = [
# R-1160 (2026-10-19) — every hub Go file is gofmt-clean; the controller's gofmt_gate.py shape. Fast: one gofmt -l.
# The CI runner has no Go: there it says NOT CHECKED out loud and passes; the pre-push hook (with Go) enforces it.
("hub-gofmt", os.path.join(SCRIPTS, "hub_gofmt_gate.py"), [], True, False),
# R-986 (2026-10-19) — the dependency and secret scanner, WARN-FIRST (exit 0 whatever it finds; a hit is listed).
# In the fast set although it reads vuln.go.dev: it cannot refuse, so a network blink costs a NOT CHECKED line,
# never a push. ~10 s here (govulncheck on hub/ + gitleaks on the unpushed commits). CI fetches the pinned
# gitleaks (FELHOM_SCAN_FETCH=1 in gates.yml) and has no Go, so govulncheck says NOT CHECKED there, out loud.
("security-scan", os.path.join(SCRIPTS, "security_scan_gate.py"), [ROOT], True, False),
]
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""security_scan_gate.py — a dependency and secret scanner on every push, WARN-FIRST (R-986).
Usage: python3 scripts/security_scan_gate.py <repo-root>
Shared by the four felhom repos (it lives HERE, in felhom.eu/scripts/, and is never copied — the
reuse_refs_check.py rule). Each repo's gate runner calls it with its own root, in the FAST set, so the
pre-push hook runs it too.
WHAT IT RUNS (pinned versions; `documentation/audits/security-review-2026-10-12/SCANNERS.md`):
govulncheck v1.8.0 on every Go module of the repo (each directory with a tracked go.mod), source mode.
Only CALLED vulnerabilities are listed (a trace that reaches a function); an imported-
or required-only one is counted, not listed — the brief's „called vulnerabilities only".
gitleaks 8.24.3 on the commits being pushed: `@{upstream}..HEAD` in a clone with an upstream, the newest
commit when that range is empty, and the pushed commit in CI (a depth-1 clone). Values are
redacted (`--redact=100`): this gate never prints a secret.
WARN-FIRST. A hit is printed and listed; the exit code stays 0. A tool that is missing, a toolchain that is
absent (CI's runner has python3 and git, no Go), or a network that cannot reach the vulnerability database
is printed as NOT CHECKED — never a pass in silence, never a refusal. Turning a hit into a refusal is a
later step with its own row. EXIT: 0 always, except a usage error (2).
TOUCHES THE NETWORK, and is still in the fast set — deliberately, and only because it cannot refuse:
govulncheck reads vuln.go.dev. Offline, it says NOT CHECKED and the push goes on (the fast set's promise is
„a push never fails because the network blinked", and that promise holds). Each tool has a time limit.
ACCEPTED HITS live in `<repo-root>/.security-accepted`, one per line, with a reason:
govulncheck GO-2026-1234 hub # why it is accepted
gitleaks generic-api-key@3f2a9c01d4e5 hub/internal/x_test.go # why
A govulncheck line names the OSV id and the module directory („." for the root). A gitleaks line names the
rule id, `@`, and the first 12 hex of the sha256 of the hit's whole LINE (read from git, never printed), then
the file — so one accepted test constant does not accept the next secret written into the same file. The
WARN line prints that `rule@hash` to copy. A line with no `# reason` is itself a WARN. An accepted hit is
listed as ACCEPTED.
CI (`FELHOM_SCAN_FETCH=1`, set in each repo's gates.yml): when gitleaks is not on this machine, the pinned
release tarball is fetched from GitHub and both the tarball and the binary are checked against the sha256
values below before it runs; a mismatch is NOT CHECKED, never a run.
SEAMS (for the decoy suite only): `FELHOM_SCAN_GOVULNCHECK_JSON=<file>` reads govulncheck's JSON from a file
instead of running it (each module gets that file); `FELHOM_SCAN_RANGE=<git log range>` pins the commits.
"""
import hashlib
import io
import json
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError, OSError): # pragma: no cover
pass
GOVULNCHECK_VERSION = "v1.8.0"
GITLEAKS_VERSION = "8.24.3"
GITLEAKS_URL = ("https://github.com/gitleaks/gitleaks/releases/download/v%s/gitleaks_%s_linux_x64.tar.gz"
% (GITLEAKS_VERSION, GITLEAKS_VERSION))
# The release's own checksums file (gitleaks_8.24.3_checksums.txt) and the binary inside, read 2026-10-19;
# the binary's sha is the one SCANNERS.md recorded for the 2026-10-12 review.
GITLEAKS_TGZ_SHA256 = "9991e0b2903da4c8f6122b5c3186448b927a5da4deef1fe45271c3793f4ee29c"
GITLEAKS_BIN_SHA256 = "e18325d568268b6efb6cd0cc721f8bea5667f4cd1c8d03618ab61987f41269ad"
# FELHOM_SECURITY_TOOLS_DIR moves the default (the decoy suite points it at an empty dir to make a tool „missing").
TOOLS_DIR = os.environ.get("FELHOM_SECURITY_TOOLS_DIR", "/mnt/5_hdd/felhom.eu/tools/security/bin")
TIMEOUT = 180
ACCEPTED = ".security-accepted"
def find_tool(name):
for d in (os.environ.get("FELHOM_SECURITY_BIN", ""), TOOLS_DIR):
p = os.path.join(d, name) if d else ""
if p and os.path.isfile(p) and os.access(p, os.X_OK):
return p
return shutil.which(name)
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def fetch_gitleaks():
"""CI only: the pinned release, both checksums verified. Returns (path, note)."""
cache = os.path.join(os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"),
"felhom-scan", GITLEAKS_VERSION)
binp = os.path.join(cache, "gitleaks")
if os.path.isfile(binp) and sha256(binp) == GITLEAKS_BIN_SHA256:
return binp, "cached pinned release"
try:
os.makedirs(cache, exist_ok=True)
req = urllib.request.Request(GITLEAKS_URL, headers={"User-Agent": "felhom-security-scan/1.0"})
data = urllib.request.urlopen(req, timeout=60).read()
except Exception as e: # network, DNS, TLS
return None, "could not fetch the pinned release (%s)" % str(e)[:120]
if hashlib.sha256(data).hexdigest() != GITLEAKS_TGZ_SHA256:
return None, "the fetched tarball's sha256 is not the pinned one — not run"
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as t:
m = t.getmember("gitleaks")
with open(binp + ".new", "wb") as out:
out.write(t.extractfile(m).read())
if sha256(binp + ".new") != GITLEAKS_BIN_SHA256:
os.remove(binp + ".new")
return None, "the binary inside the tarball is not the pinned one — not run"
os.chmod(binp + ".new", 0o755)
os.replace(binp + ".new", binp)
return binp, "fetched pinned release, sha256 checked"
def read_accepted(root, warns):
out = {"govulncheck": {}, "gitleaks": {}}
p = os.path.join(root, ACCEPTED)
if not os.path.isfile(p):
return out
for n, line in enumerate(io.open(p, encoding="utf-8"), 1):
body, _, reason = line.partition("#")
f = body.split()
if not f:
continue
if len(f) != 3 or f[0] not in out:
warns.append("%s:%d: not „<tool> <id> <where> # reason\" — ignored" % (ACCEPTED, n))
continue
if not reason.strip():
warns.append("%s:%d: %s %s %s has no reason — accepted anyway, but say why" % (ACCEPTED, n, f[0], f[1], f[2]))
out[f[0]][(f[1], f[2])] = reason.strip() or "(no reason given)"
return out
def git(root, *args):
r = subprocess.run(["git", "-C", root] + list(args), capture_output=True, text=True, timeout=60)
return r.returncode, r.stdout.strip()
def go_modules(root):
rc, out = git(root, "ls-files", "*go.mod", "go.mod")
if rc != 0:
return []
mods = []
for f in out.splitlines():
parts = f.split("/")
# product modules only: not a test fixture, a vendored copy, or an audit's throwaway program
if f.endswith("go.mod") and not {"testdata", "vendor", "documentation", "audits"} & set(parts[:-1]):
mods.append(os.path.dirname(f) or ".")
return sorted(set(mods))
def parse_govulncheck(text):
"""govulncheck -format json is a stream of objects. Returns (called {id: (module, summary)}, other_ids, error)."""
dec = json.JSONDecoder()
i, osv, called, other = 0, {}, {}, set()
while i < len(text):
while i < len(text) and text[i].isspace():
i += 1
if i >= len(text):
break
try:
o, i = dec.raw_decode(text, i)
except ValueError:
return called, other, text[i:i + 200].strip().splitlines()[0] if text[i:].strip() else "unreadable output"
if "osv" in o:
osv[o["osv"].get("id", "?")] = o["osv"].get("summary", "")
if "finding" in o:
f = o["finding"]
tr = f.get("trace") or []
if tr and tr[0].get("function"):
called[f.get("osv", "?")] = tr[0].get("module", "")
else:
other.add(f.get("osv", "?"))
return {k: (m, osv.get(k, "")) for k, m in called.items()}, other - set(called), None
def run_govulncheck(root, accepted, lines):
new = acc = 0
mods = go_modules(root)
if not mods:
lines.append(" govulncheck: no Go module in this repo — nothing to scan")
return 0, 0
seam = os.environ.get("FELHOM_SCAN_GOVULNCHECK_JSON")
tool = None if seam else find_tool("govulncheck")
if not seam and not tool:
lines.append(" govulncheck: NOT CHECKED — govulncheck %s is not on this machine (tried $FELHOM_SECURITY_BIN, %s, PATH)"
% (GOVULNCHECK_VERSION, TOOLS_DIR))
return 0, 0
if not seam and not shutil.which("go"):
lines.append(" govulncheck: NOT CHECKED — no Go toolchain here (CI's runner has python3 and git only); the "
"pre-push hook runs it")
return 0, 0
for m in mods:
if seam:
text, note = io.open(seam, encoding="utf-8").read(), "(seam: %s)" % os.path.basename(seam)
else:
try:
r = subprocess.run([tool, "-format", "json", "./..."], cwd=os.path.join(root, m),
capture_output=True, text=True, timeout=TIMEOUT)
except subprocess.TimeoutExpired:
lines.append(" govulncheck %s: NOT CHECKED — over %ds" % (m, TIMEOUT))
continue
text, note = r.stdout + r.stderr, ""
called, other, err = parse_govulncheck(text)
if err and not called:
lines.append(" govulncheck %s: NOT CHECKED — %s %s" % (m, err[:160], note))
continue
lines.append(" govulncheck %s: %d called vulnerabilit%s (%d imported or required only, not listed) %s"
% (m, len(called), "y" if len(called) == 1 else "ies", len(other), note))
for vid in sorted(called):
mod, summary = called[vid]
why = accepted.get((vid, m))
if why:
acc += 1
lines.append(" ACCEPTED %s (%s) — %s" % (vid, mod, why))
else:
new += 1
lines.append(" WARN %s called, in %s — %s" % (vid, mod, summary[:100]))
return new, acc
def scan_range(root):
r = os.environ.get("FELHOM_SCAN_RANGE")
if r:
return r, "pinned by FELHOM_SCAN_RANGE"
if os.environ.get("GITHUB_ACTIONS") or os.environ.get("GITEA_ACTIONS"):
return "-n 1 HEAD", "the pushed commit; CI clones at depth 1"
rc, up = git(root, "rev-parse", "--verify", "-q", "@{upstream}")
if rc == 0 and up:
rc2, n = git(root, "rev-list", "--count", "@{upstream}..HEAD")
if rc2 == 0 and n.isdigit() and int(n) > 0:
return "@{upstream}..HEAD", "the %s commit(s) not yet on the upstream" % n
return "-n 1 HEAD", "the newest commit; nothing unpushed"
def line_hash(root, commit, path, lineno):
"""sha256[:12] of the hit's stripped line, read from the commit (or the working tree) — in memory only."""
text = None
if commit:
r = subprocess.run(["git", "-C", root, "show", "%s:%s" % (commit, path)], capture_output=True, timeout=60)
if r.returncode == 0:
text = r.stdout.decode("utf-8", "replace")
if text is None:
try:
text = io.open(os.path.join(root, path), encoding="utf-8", errors="replace").read()
except OSError:
return "unread"
ls = text.splitlines()
if not 0 < lineno <= len(ls):
return "unread"
return hashlib.sha256(ls[lineno - 1].strip().encode("utf-8")).hexdigest()[:12]
def run_gitleaks(root, accepted, lines):
tool = find_tool("gitleaks")
note = ""
if not tool and os.environ.get("FELHOM_SCAN_FETCH") == "1":
tool, note = fetch_gitleaks()
if not tool:
lines.append(" gitleaks: NOT CHECKED — gitleaks %s is not on this machine%s" % (GITLEAKS_VERSION, "; " + note if note else ""))
return 0, 0
for f in (".gitleaks.toml", ".gitleaksignore"):
if os.path.exists(os.path.join(root, f)):
lines.append(" gitleaks: WARN this repo carries %s — the scan below ignores it (its config and ignore list are "
"pinned by this gate, so a file in the repo cannot switch rules off)" % f)
rng, why = scan_range(root)
tmp = tempfile.mkdtemp(prefix="felhom-scan-")
try:
rep = os.path.join(tmp, "report.json")
empty_ignore = os.path.join(tmp, "ignore")
os.makedirs(empty_ignore)
cfg_env = dict(os.environ)
for k in ("GITLEAKS_CONFIG", "GITLEAKS_CONFIG_TOML"):
cfg_env.pop(k, None)
argv = [tool, "git", root, "--log-opts=" + rng, "--redact=100", "--no-banner", "--report-format", "json",
"--report-path", rep, "--exit-code", "0", "--gitleaks-ignore-path", empty_ignore]
# The default rules, never a .gitleaks.toml from the scanned tree: hand gitleaks its own default config.
dflt = os.path.join(tmp, "default.toml")
io.open(dflt, "w").write('[extend]\nuseDefault = true\n')
argv += ["--config", dflt]
try:
r = subprocess.run(argv, capture_output=True, text=True, timeout=TIMEOUT, env=cfg_env)
except subprocess.TimeoutExpired:
lines.append(" gitleaks: NOT CHECKED — over %ds" % TIMEOUT)
return 0, 0
if r.returncode != 0 or not os.path.isfile(rep):
lines.append(" gitleaks: NOT CHECKED — exit %d: %s" % (r.returncode, (r.stderr or r.stdout).strip()[-160:]))
return 0, 0
hits = json.load(io.open(rep, encoding="utf-8")) or []
finally:
shutil.rmtree(tmp, ignore_errors=True)
lines.append(" gitleaks %s on %s (%s)%s: %d hit(s)" % (GITLEAKS_VERSION, rng, why, " [" + note + "]" if note else "", len(hits)))
new = acc = 0
for h in hits:
rule, path = h.get("RuleID", "?"), h.get("File", "?")
key = "%s@%s" % (rule, line_hash(root, h.get("Commit") or "", path, h.get("StartLine") or 0))
why = accepted.get((key, path))
where = "%s:%s, commit %s" % (path, h.get("StartLine", "?"), (h.get("Commit") or "")[:10])
if why:
acc += 1
lines.append(" ACCEPTED %s %s — %s" % (key, where, why))
else:
new += 1
lines.append(" WARN %s %s (value redacted)" % (key, where))
return new, acc
def main(argv):
if len(argv) != 2 or not os.path.isdir(argv[1]):
print(__doc__.split("\n\n")[1])
return 2
root = os.path.abspath(argv[1])
warns, lines = [], []
accepted = read_accepted(root, warns)
print("security-scan (R-986, WARN-FIRST — it lists, it never refuses): %s" % root)
n1, a1 = run_govulncheck(root, accepted["govulncheck"], lines)
n2, a2 = run_gitleaks(root, accepted["gitleaks"], lines)
for l in lines:
print(l)
for w in warns:
print(" WARN %s" % w)
print("security-scan: %d new hit(s), %d accepted — warn-first, the push goes on" % (n1 + n2, a1 + a2))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+19
View File
@@ -110,6 +110,12 @@ COVERS = {
"one-register": ("R-426: an open roadmap row whose state NAMES another row's CLOSED (a live hole until "
"2026-10-06), its id only in register prose, a suffix id; genuine: a row with a counterpart, "
"and an `idea` row (R-424's accepted hole, asserted BY DESIGN)"),
"security-scan": ("R-986: EIGHT cases in scripts/test_security_scan_gate.py, run from here, each in a SCRATCH "
"git repo: a planted token is listed by rule and file and its value never printed; a second "
"secret on another line of an ACCEPTED file is still listed (acceptance is per line); an "
"accepted line with no reason warns; a CALLED govulncheck finding is listed, an imported-only "
"one is not; no tool anywhere is NOT CHECKED out loud, never a silent pass; the clean commit "
"and the accepted line pass. Warn-first: every case exits 0, the LISTING is the fact"),
"hub-gofmt": ("R-1160: an unformatted .go file in a SCRATCH module dir is convicted by name; its "
"formatted twin passes; with no gofmt reachable, CI passes OUT LOUD and a dev machine is "
"INCONCLUSIVE, never a silent pass"),
@@ -786,6 +792,19 @@ decoy("due-checks/due-tomorrow", "due_checks_gate.py",
decoy("due-checks/marker-named-in-prose", "due_checks_gate.py",
_dc_plant(u"", prose=u"\nThe dated checks sit between the DUE-CHECKS-BEGIN and DUE-CHECKS-END markers.\n"))
# ── security-scan (R-986) ────────────────────────────────────────────────────────────────────────
#
# Its decoys are scratch git repos, so they live in their own file and are RUN from here (script-tests' shape).
ran += 1
_ss = subprocess.run([sys.executable, os.path.join("scripts", "test_security_scan_gate.py")],
cwd=ROOT, capture_output=True, text=True)
if _ss.returncode != 0:
fails.append("security-scan: its decoy suite FAILED — a decoy did not convict\n%s"
% (_ss.stdout + _ss.stderr)[-800:])
else:
_n = _ss.stdout.strip().splitlines()[-1] if _ss.stdout.strip() else "?"
print(" ok %-20s %s" % ("security-scan", _n))
# ── script-tests (R-885) ─────────────────────────────────────────────────────────────────────────
#
# Its decoys are whole fake repos, so they live in their own file and are RUN from here (guide-quote's shape).
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""test_security_scan_gate.py — the decoys of security_scan_gate.py (R-986). Run from felhom.eu's decoy suite and by
the script-tests gate; each other repo's decoy suite runs the shared gate on its own scratch clone.
Every case builds a SCRATCH git repo in a temp dir (never this tree) and judges the gate's OUTPUT — warn-first, so the
exit code is 0 in every case and the listing is the fact:
1. FACT: a commit that adds a GitHub-token-shaped string → WARN with the rule and the file; the value is NOT printed.
2. GENUINE: a clean commit → 0 hits, no WARN.
3. ACCEPTED: the same hit listed in .security-accepted (rule@line-hash + file + reason) → ACCEPTED, 0 new.
4. FACT: a SECOND secret on another line of an accepted file → still WARN (acceptance is per line, not per file).
5. FACT: an accepted line with no reason → WARN naming the line.
6. govulncheck (its JSON from a file, the seam): one CALLED finding is listed by id, an imported-only one is not; the
same id accepted → ACCEPTED.
7. FACT: no gitleaks anywhere (an empty tools dir, an empty PATH) → NOT CHECKED, out loud, exit 0 — never a silent
pass.
The fake token is built at run time, so no secret-shaped literal sits in this file. Without gitleaks on this machine
cases 1-5 say NOT CHECKED HERE and pass out loud (the workspace and CI with FELHOM_SCAN_FETCH=1 run them).
"""
import json
import os
import random
import shutil
import string
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
GATE = os.path.join(HERE, "security_scan_gate.py")
fails, ran = [], 0
def fake_token(seed=1986):
rnd = random.Random(seed)
return "ghp" + "_" + "".join(rnd.choice(string.ascii_letters + string.digits) for _ in range(36))
def run(cwd, argv, env=None):
p = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, env=env)
return p.returncode, p.stdout + p.stderr
def scratch_repo(base):
r = os.path.join(base, "repo")
os.makedirs(r)
for a in (["git", "init", "-q"], ["git", "config", "user.email", "decoy@felhom.invalid"],
["git", "config", "user.name", "decoy"], ["git", "config", "commit.gpgsign", "false"]):
run(r, a)
open(os.path.join(r, "README.md"), "w").write("scratch\n")
run(r, ["git", "add", "README.md"])
run(r, ["git", "commit", "-q", "-m", "base"])
return r
def commit(repo, path, text, msg):
full = os.path.join(repo, path)
os.makedirs(os.path.dirname(full), exist_ok=True)
open(full, "w").write(text)
run(repo, ["git", "add", path])
run(repo, ["git", "commit", "-q", "-m", msg])
def gate(repo, env_extra=None):
env = dict(os.environ)
env["FELHOM_SCAN_RANGE"] = "-n 1 HEAD"
env.pop("GITHUB_ACTIONS", None)
env.pop("GITEA_ACTIONS", None)
env.update(env_extra or {})
return run(repo, [sys.executable, GATE, repo], env)
def check(name, cond, out):
global ran
ran += 1
if cond:
print(" ok %s" % name)
else:
fails.append("%s\n%s" % (name, out[-900:]))
def main():
base = tempfile.mkdtemp(prefix="secscan-decoy-")
try:
tok = fake_token()
repo = scratch_repo(base)
rc, out = gate(repo)
gitleaks_here = "gitleaks: NOT CHECKED" not in out
check("2 GENUINE: a clean commit — 0 hits, no WARN, exit 0",
rc == 0 and ("0 hit(s)" in out or not gitleaks_here)
and not [l for l in out.splitlines() if l.strip().startswith("WARN")], out)
if gitleaks_here:
commit(repo, "pkg/config.go", 'package pkg\n\nconst Token = "%s"\n' % tok, "plant")
rc, out = gate(repo)
warn = [l for l in out.splitlines() if l.strip().startswith("WARN github-pat@")]
check("1 FACT: a planted token is listed (rule + file), the value is never printed, exit 0",
rc == 0 and warn and "pkg/config.go:3" in warn[0] and tok not in out and "1 new hit(s)" in out, out)
key = warn[0].split()[1] if warn else "github-pat@none"
open(os.path.join(repo, ".security-accepted"), "w").write(
"gitleaks %s pkg/config.go # a decoy constant\n" % key)
rc, out = gate(repo)
check("3 ACCEPTED: the same line accepted by rule@hash + file", rc == 0 and "ACCEPTED " + key in out
and "0 new hit(s), 1 accepted" in out, out)
commit(repo, "pkg/config.go", 'package pkg\n\nconst Token = "%s"\nconst Other = "%s"\n'
% (tok, fake_token(1167)), "second")
rc, out = gate(repo)
check("4 FACT: a second secret on another line of an accepted file is still WARN",
rc == 0 and "pkg/config.go:4" in out and "WARN github-pat@" in out and tok not in out, out)
open(os.path.join(repo, ".security-accepted"), "w").write("gitleaks %s pkg/config.go\n" % key)
rc, out = gate(repo)
check("5 FACT: an accepted line with no reason is a WARN", rc == 0 and "has no reason" in out, out)
else:
print(" NOT CHECKED HERE cases 1, 3, 4, 5 — no gitleaks on this machine (the workspace and CI run them)")
# 6 — govulncheck through the JSON seam (no network, no Go needed).
mod = os.path.join(repo, "svc")
os.makedirs(mod, exist_ok=True)
open(os.path.join(mod, "go.mod"), "w").write("module example.invalid/svc\n\ngo 1.22\n")
run(repo, ["git", "add", "svc/go.mod"])
run(repo, ["git", "commit", "-q", "-m", "module"])
stream = [
{"config": {"scanner_name": "govulncheck", "scanner_version": "v1.8.0"}},
{"osv": {"id": "GO-2099-0001", "summary": "a decoy called vulnerability"}},
{"osv": {"id": "GO-2099-0002", "summary": "a decoy imported-only vulnerability"}},
{"finding": {"osv": "GO-2099-0001", "trace": [{"module": "example.invalid/dep", "function": "Parse"}]}},
{"finding": {"osv": "GO-2099-0002", "trace": [{"module": "example.invalid/dep2"}]}},
]
jf = os.path.join(base, "gv.json")
open(jf, "w").write("\n".join(json.dumps(o, indent=2) for o in stream))
open(os.path.join(repo, ".security-accepted"), "w").write("")
rc, out = gate(repo, {"FELHOM_SCAN_GOVULNCHECK_JSON": jf})
check("6a FACT: a CALLED vulnerability is listed by id; an imported-only one is counted, not listed",
rc == 0 and "WARN GO-2099-0001 called" in out and "GO-2099-0002" not in out
and "1 called vulnerability (1 imported" in out, out)
open(os.path.join(repo, ".security-accepted"), "w").write("govulncheck GO-2099-0001 svc # a decoy, accepted\n")
rc, out = gate(repo, {"FELHOM_SCAN_GOVULNCHECK_JSON": jf})
check("6b ACCEPTED: the same id accepted for that module", rc == 0 and "ACCEPTED GO-2099-0001" in out
and "WARN GO-2099" not in out, out)
# 7 — no gitleaks anywhere: NOT CHECKED out loud, exit 0.
empty = os.path.join(base, "empty")
os.makedirs(empty)
git_dir = os.path.dirname(shutil.which("git") or "/usr/bin/git")
only_git = os.path.join(base, "onlygit")
os.makedirs(only_git)
os.symlink(os.path.join(git_dir, "git"), os.path.join(only_git, "git"))
rc, out = gate(repo, {"FELHOM_SECURITY_TOOLS_DIR": empty, "FELHOM_SECURITY_BIN": "", "PATH": only_git,
"FELHOM_SCAN_FETCH": "0"})
check("7 FACT: no gitleaks and no govulncheck anywhere — NOT CHECKED, out loud, exit 0",
rc == 0 and "gitleaks: NOT CHECKED" in out and "govulncheck: NOT CHECKED" in out, out)
finally:
shutil.rmtree(base, ignore_errors=True)
if fails:
for f in fails:
print("FAIL: " + f)
return 1
print("test_security_scan_gate: all %d cases passed" % ran)
return 0
if __name__ == "__main__":
sys.exit(main())