PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 18s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 1s
The 2026-09-17 purge removed six live credentials that had sat in this repo for weeks, several in .md prose. Nothing blocked that class of commit, so a warning in a stream nobody reads was the only signal. This adds a guard that fails the build instead of warning. Guard - scripts/secret-scan.sh: bash + coreutils + grep/sed/awk + git only (the Gitea Actions runner executes job steps inside the runner container — BusyBox grep, no node/python). Modes: --tree (git-tracked, default), --path DIR (no git), --staged (pre-commit), --diff REF. Exit 1 on a finding, 2 on config error. - scripts/secret-patterns.tsv: checked-in pattern list — sk-, sk-or-v1-, sk_live_, literal Bearer tokens, PVEAPIToken=, raw Authorization values, PEM private-key blocks, prose credential lines, and password/api_key/secret/token assignments carrying a literal value. Prose is scanned exactly like code. - scripts/secret-allowlist.tsv: one entry per deliberate synthetic example, each with a reason. A missing reason is a hard error (fail closed). The 2026-09-17 purge's `«vault: ...»` placeholders are listed explicitly rather than filtered by a general "vault"/"synthetic" rule, so a new occurrence still needs a reviewed, reasoned entry. - A small inert-value classifier drops env refs, paths, dotted code access, variable names and right-truncated redactions; it does not know the words "synthetic"/"example", so a fabrication is always an explicit exception. - Findings are printed with the credential masked; a scan never echoes a full secret into the log. Wiring - .gitea/workflows/pr-pipeline.yaml lint job: explicit "Committed-credential scan" step plus the self-test. A finding fails the required `pr-pipeline / lint` context, which the merge gate depends on. - scripts/prose-lint.sh (the local gate): a "Secret scan" section, so `bash scripts/prose-lint.sh` before pushing is equivalent to CI. Tests - tests/test_secret_scan.sh: 20 cases. Plants pattern-matching fixtures in temp trees (outside every allowlisted path) and asserts the guard FAILS, including the --staged commit-time path; asserts the tree is quiet; asserts allowlisted text at an unlisted path still fails (path-explicit, not word-based); asserts a reasonless allowlist entry exits 2. Verified: guard run against 8245716^ (the pre-fix revision, before the purge) fails on the real OpenRouter/LiteLLM/Zulip/Proxmox/Stirling credentials; guard run over the current tree is clean.
6.1 KiB
6.1 KiB
| 1 | # secret-allowlist.tsv — exceptions for scripts/secret-scan.sh, every entry with a reason. | |||
|---|---|---|---|---|
| 2 | # | |||
| 3 | # Format: <rule-id|*><TAB><path-glob><TAB><literal-substring><TAB><reason> | |||
| 4 | # Blank lines and lines whose first field starts with '#' are ignored. | |||
| 5 | # A finding is suppressed only when ALL THREE of rule, path and literal match: | |||
| 6 | # * the rule id equals the finding's rule id, or is '*' | |||
| 7 | # * the finding's repo-relative path matches <path-glob> (bash glob) | |||
| 8 | # * the finding's line contains <literal-substring> verbatim | |||
| 9 | # An entry whose reason is empty is a hard error (exit 2) — no silent exceptions. | |||
| 10 | # | |||
| 11 | # RULE: never allowlist a live credential, and never broaden an entry (rule '*', | |||
| 12 | # a wide path glob, or a short generic literal) just to silence a finding. | |||
| 13 | # If the finding is real, remove the credential from the file. | |||
| 14 | # | |||
| 15 | # Entries are one per deliberate synthetic example, so the file reads as an | |||
| 16 | # audit trail of reviewed exceptions rather than a list of things to ignore. | |||
| 17 | # Rule '*' is used only where the same literal is matched by more than one rule. | |||
| 18 | # | |||
| 19 | # ── The 2026-09-17 purge placeholders ───────────────────────────────────── | |||
| 20 | # PR #112 replaced six live credentials with `«vault: <project>/<env> <SECRET>»` | |||
| 21 | # references. Those references are safe by construction (they name where the | |||
| 22 | # secret is read from), but they are listed here explicitly rather than being | |||
| 23 | # deliberate, reasoned entry. | |||
| 24 | secret-assign | litellm-api-keys.prose.md | MUMUNI_LITELLM_API_KEY=«vault: agents/production LITELLM_API_KEY» | 2026-09-17 purge: replaced the live Mumuni LiteLLM key with its vault reference; no literal credential. |
| 25 | secret-assign | litellm-api-keys.prose.md | MUMUNI_ZULIP_API_KEY=«vault: agents/production ZULIP_API_KEY» | 2026-09-17 purge: replaced the live Mumuni Zulip key with its vault reference; no literal credential. |
| 26 | * | infrastructure-control.prose.md | PVEAPIToken=«vault: infrastructure/production PVE_API_TOKEN» | 2026-09-17 purge: Proxmox API token is read from the vault; the line only names the vault path. |
| 27 | cred-prose | infrastructure-control.prose.md | Admin credentials: | 2026-09-17 purge: the Stirling admin user/password are two `«vault: ...»` references; no literal credential. |
| 28 | * | scripts/daily-infra-report.py | PVEAPIToken=«vault: infrastructure/production PVE_API_TOKEN» | 2026-09-17 purge: Proxmox API token is read from the vault; the line only names the vault path. |
| 29 | secret-assign | stirling-pdf-agent-access.prose.md | «vault: infrastructure/production STIRLING_API_KEY» | 2026-09-17 purge: Stirling PDF API key is read from the vault; the curl example only names the vault path. |
| 30 | bearer-token | agent-zero-fix-summary.md | «vault: agents/production OPENROUTER_API_KEY» | 2026-09-17 purge: OpenRouter key is read from the vault; the example curl only names the vault path. |
| 31 | # ── Deliberate synthetic examples in contracts (not from the purge) ─────── | |||
| 32 | # These exist to teach the rule they illustrate. They are listed here so the | |||
| 33 | # example is always an explicit exception, never a pattern-level exemption. | |||
| 34 | * | hermes-key-enforcement.prose.md | sk-synthetic-external-example | Rule 15 illustration of a hardcoded external key that is tolerated; fabricated, never a live key. |
| 35 | * | hermes-key-enforcement.prose.md | sk-synthetic-example-12345 | Rule 15 illustration of a forbidden hardcoded key; fabricated, never a live key. |
| 36 | openai-key | hermes-key-enforcement.prose.md | sk-synthetic-litellm- | Fabricated key name inside a `grep 'LITELLM_API_KEY=...'` example; not a live key. |
| 37 | secret-assign | hermes-key-enforcement.prose.md | sk-NEW_KEY | Placeholder standing for the rotated key in an `infisical secrets set` command; not a literal key. |
| 38 | openrouter-key | agent-zero-openrouter-key.prose.md | sk-or-v1-synthetic | Synthetic key prefix in the contract's example response; the real key is read from the vault. |
| 39 | openai-key | litellm-api-keys.prose.md | sk-synthetic-tanko-example | Fabricated key name in migration history prose; not a live key. |
| 40 | openai-key | litellm-self-heal.prose.md | sk-syslog-local-master-key | Deprecated local LiteLLM master key name documented as no-live-usage; kept for history, not a usable credential. |
| 41 | # ── Redacted evidence, not a credential ────────────────────────────────── | |||
| 42 | secret-assign | docs/probe-drift-round2-evidence.md | =sk-... | Probe evidence records redacted key trailers (`sk-...x6uw`); the usable part of the key is not present. |
| 43 | # ── tests/test_secret_scan.sh fixtures ─────────────────────────────────── | |||
| 44 | # The self-test plants these fabricated values into a TEMP tree, whose path no | |||
| 45 | # entry here covers, so each still fails the guard when planted (see the test's | |||
| 46 | # the test file itself stays quiet. | |||
| 47 | * | tests/test_secret_scan.sh | sk-or-v1-00000000000000000000000000000000000000000000000000000000deadbeef | Self-test fixture: fabricated OpenRouter-shaped key written to a temp tree; the guard must fail on it there. |
| 48 | bearer-token | tests/test_secret_scan.sh | Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb | Self-test fixture: fabricated Bearer token written to a temp tree; the guard must fail on it there. |
| 49 | proxmox-token | tests/test_secret_scan.sh | PVEAPIToken=root@pam!monitor=11111111-2222-3333-4444-555555555555 | Self-test fixture: fabricated Proxmox token written to a temp tree; the guard must fail on it there. |
| 50 | private-key | tests/test_secret_scan.sh | -----BEGIN OPENSSH PRIVATE KEY----- | Self-test fixture: fabricated PEM banner written to a temp tree; the guard must fail on it there. |
| 51 | cred-prose | tests/test_secret_scan.sh | Admin credentials: | Self-test fixture: fabricated prose credential line written to a temp tree; the guard must fail on it there. |
| 52 | secret-assign | tests/test_secret_scan.sh | DB_PASSWORD=correct-horse-battery-staple | Self-test fixture: fabricated password assignment written to a temp tree; the guard must fail on it there. |