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
gates / gates (push) Failing after 12m59s
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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))
|
||||
@@ -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).
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user