feat(security): commit-time secret guard that FAILS the build on a committed credential #129

Merged
abiba-bot merged 1 commits from fm/commit-time-secret-guard-20260917 into master 2026-09-22 15:33:17 +00:00
Owner

What

A commit-time secret guard that fails the build on a credential-shaped string, wired into the repo's local gate and the Gitea Actions pipeline. The 2026-09-17 purge removed six live credentials that had sat here 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 makes it exit 1.

Backlog row: commit-time-secret-guard-20260917 (captain approved 2026-09-22: "definitely add the ci check that blocks committed credentials now").

Shape

File Role
scripts/secret-scan.sh guard: --tree (default), --path DIR, --staged, --diff REF; exit 1 on finding, 2 on config error
scripts/secret-patterns.tsv checked-in pattern list (sk-, sk-or-v1-, sk_live_, literal Bearer, PVEAPIToken=, raw Authorization, PEM blocks, prose credentials: lines, password/api_key/secret/token assignments carrying a literal value)
scripts/secret-allowlist.tsv exceptions, one entry per deliberate synthetic example, each with a reason; a missing reason is a hard error (fail closed)
tests/test_secret_scan.sh 20 cases, incl. the --staged commit-time path
scripts/prose-lint.sh the local gate now runs the scan
.gitea/workflows/pr-pipeline.yaml explicit Committed-credential scan step + self-test in the lint job

Design notes:

  • Prose is scanned exactly like code — cred-prose matches - Admin credentials: `admin` / `<value>` , which a PASSWORD=-only rule would miss. That is exactly where the original exposures were.
  • Nothing is trained to ignore the word "synthetic". The 2026-09-17 purge's «vault: <project>/<env> <SECRET>» markers and the sk-synthetic-* examples are listed as explicit, reasoned allowlist entries rather than filtered by a general "vault"/"synthetic" rule. A new occurrence needs a new reviewed entry.
  • A scan never echoes a credential. Findings print only the text before the match, then <redacted> — so a value the regex stopped short of (e.g. a backticked password after credentials:) is still never written to the log.
  • Portable to the runner. The Gitea runner executes job steps inside the runner container (Alpine / BusyBox grep, no node/python). The guard uses only bash + grep/sed/awk + git; verified in-container: bash 5.3.3, BusyBox grep -EIi/\b/«…», awk match()/RSTART, mapfile -d, nocasematch, ${var/pat/repl} all work.

CI filename note

The brief said .gitea/workflows/ci.yml; this repo has no ci.yml — the only workflow is .gitea/workflows/pr-pipeline.yaml. The guard went into the lint job there (a required status context, and the gate job needs it), so a finding fails the merge gate. No duplicate workflow was created.

Acceptance evidence

1a. The guard FAILS on planted pattern-matching secrets (self-test)

$ bash tests/test_secret_scan.sh
── secret-scan self-test ──
  ✅ scanner parses with bash -n (exit 0)
  ✅ planted sk-or-v1 key fails the guard (exit 1)
  ✅ planted sk-or-v1 key names the openrouter-key rule
  ✅ planted literal Bearer token fails the guard (exit 1)
  ✅ planted Bearer token names the bearer-token rule
  ✅ planted Proxmox token fails the guard (exit 1)
  ✅ planted Proxmox token names the proxmox-token rule
  ✅ planted PEM private key fails the guard (exit 1)
  ✅ planted PEM key names the private-key rule
  ✅ planted prose credential line fails the guard (exit 1)
  ✅ planted prose line names the cred-prose rule
  ✅ planted password assignment fails the guard (exit 1)
  ✅ planted password assignment names the secret-assign rule
  ✅ env refs, sentinels and variable names are not credentials (exit 0)
  ✅ current repo tree passes the guard (exit 0)
  ✅ tree run reports the allowlisted exceptions it applied
  ✅ allowlisted text at an unlisted path still fails (exit 1)
  ✅ staged credential fails at commit time (--staged) (exit 1)
  ✅ staged credential names the openrouter-key rule
  ✅ allowlist entry with no reason fails closed (exit 2)

✅ secret-scan self-test passed (20 cases)
exit=0

1b. The test BITES on the pre-fix revision

8245716^ is the commit before the 2026-09-17 purge, i.e. the revision where the live credentials still lived. The guard fails on it (22 findings), naming the real credential shapes — the OpenRouter keys, the Mumuni LiteLLM/Zulip keys, the Proxmox token, and the Stirling credentials. Output is verbatim, including the masking:

# Guard run against the PRE-FIX revision: 8245716^ = the commit before the
# 2026-09-17 purge (live credentials still in the tree).
$ bash scripts/secret-scan.sh --path $PRE_FIX_TREE
── secret scan (path): 76 files under /tmp/tmp.k0QYcqPj0R ──
  ❌ stirling-pdf-agent-access.prose.md:47  [secret-assign] credential assignment carrying a literal value
     |   -H "X-<redacted>
  ❌ scripts/daily-infra-report.py:19  [proxmox-token] Proxmox API token literal
     | AUTH = "Authorization: <redacted>
  ❌ scripts/daily-infra-report.py:19  [auth-header] Authorization header carrying a raw literal value
     | AUTH = "<redacted>
  ❌ litellm-api-keys.prose.md:147  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     |    MUMUNI_LITELLM_API_KEY=<redacted>
  ❌ litellm-api-keys.prose.md:147  [secret-assign] credential assignment carrying a literal value
     |    MUMUNI_LITELLM_<redacted>
  ❌ litellm-api-keys.prose.md:148  [secret-assign] credential assignment carrying a literal value
     |    MUMUNI_ZULIP_<redacted>
  ❌ infrastructure-control.prose.md:639  [proxmox-token] Proxmox API token literal
     | AUTH="Authorization: <redacted>
  ❌ infrastructure-control.prose.md:639  [auth-header] Authorization header carrying a raw literal value
     | AUTH="<redacted>
  ❌ hermes-key-enforcement.prose.md:104  [secret-assign] credential assignment carrying a literal value
     |     <redacted>
  ❌ hermes-key-enforcement.prose.md:112  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     |   api_key: <redacted>
  ❌ hermes-key-enforcement.prose.md:112  [secret-assign] credential assignment carrying a literal value
     |   <redacted>
  ❌ hermes-key-enforcement.prose.md:185  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     | grep -rn 'LITELLM_API_KEY=<redacted>
  ❌ hermes-key-enforcement.prose.md:185  [secret-assign] credential assignment carrying a literal value
     | grep -rn 'LITELLM_<redacted>
  ❌ agent-zero-openrouter-key.prose.md:93  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     | | **Full Key** | `«redacted:<redacted>
  ❌ agent-zero-openrouter-key.prose.md:93  [openrouter-key] OpenRouter API key
     | | **Full Key** | `«redacted:<redacted>
  ❌ agent-zero-openrouter-key.prose.md:104  [openrouter-key] OpenRouter API key
     | | 2026-09-01 | fix-401 | Old key `<redacted>
  ❌ agent-zero-fix-summary.md:19  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     | **Old Key**: `<redacted>
  ❌ agent-zero-fix-summary.md:19  [openrouter-key] OpenRouter API key
     | **Old Key**: `<redacted>
  ❌ agent-zero-fix-summary.md:20  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     | **New Key**: `<redacted>
  ❌ agent-zero-fix-summary.md:20  [openrouter-key] OpenRouter API key
     | **New Key**: `<redacted>
  ❌ agent-zero-fix-summary.md:51  [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys)
     | sed -i "s|^API_KEY_OPENROUTER=.*|API_KEY_OPENROUTER=<redacted>
  ❌ agent-zero-fix-summary.md:51  [openrouter-key] OpenRouter API key
     | sed -i "s|^API_KEY_OPENROUTER=.*|API_KEY_OPENROUTER=<redacted>

❌ SECRET SCAN FAILED — 22 credential-shaped string(s) in path content.
   Fix: remove the credential and read it from the vault/env.
   Only a deliberate synthetic example may be added to scripts/secret-allowlist.tsv,
   one entry per file/rule/literal, with a reason. Never allowlist a live credential.
exit=1

2. The guard stays QUIET on the allowlisted synthetic examples

$ bash -n scripts/secret-scan.sh scripts/prose-lint.sh tests/test_secret_scan.sh
(clean)

$ shellcheck -S warning scripts/secret-scan.sh tests/test_secret_scan.sh scripts/prose-lint.sh
(clean)

$ bash scripts/secret-scan.sh
── secret scan (tree): 85 files under /root/.treehouse/prose-contracts-9ce5f3/3/prose-contracts ──
✅ secret scan clean (tree; 36 allowlisted exception(s), 22 inert value(s) ignored)
exit=0

The self-test also proves the exception is path-explicit, not word-based: the exact - Admin credentials: `«vault: ...»` line that is allowlisted in infrastructure-control.prose.md fails when copied to an unlisted path.

3. bash -n / linter clean, current tree passes

(both in the block above) and the local gate:

$ bash scripts/prose-lint.sh   # the local gate, now including the scan
  Cross-contract: 10 total warnings across all checks

── 4. Secret scan (committed credentials) ──
── secret scan (tree): 85 files under /root/.treehouse/prose-contracts-9ce5f3/3/prose-contracts ──
✅ secret scan clean (tree; 36 allowlisted exception(s), 22 inert value(s) ignored)
  ✅ No committed credentials

═══════════════════════════════════
✅ LINT PASSED (10 warning(s))
exit=0

Runner verification

runner-ct110 (act_runner, GITEA_RUNNER_LABELS=ubuntu-latest,ubuntu-22.04) runs job steps inside the runner container itself. Inspected live: the container has no python/node-based dependency need, and does have bash 5.3.3 plus BusyBox grep/awk with every feature the guard uses (-EIi, \b, «…» alternation, awk match()/RSTART, mapfile -d, nocasematch). Same tools the existing lint job already uses.

Residual items for the contract owner

Two literals are allowlisted because they are already-documented history, but they are worth a proper redaction by whoever owns those contracts:

  • litellm-api-keys.prose.md:276 — a truncated real Zulip key prefix (9 characters, printed as … here deliberately); not usable at 9 chars, but ideally replaced with a «vault: ...» reference.
  • litellm-self-heal.prose.md:119 — sk-syslog-local-master-key, documented as deprecated/no-live-usage.

Scope / authorization

Scoped to the guard, its patterns/allowlist, its tests, the local-gate call, the CI step, and one AGENTS.md pointer. scripts/prose-lint.sh is in prose-auth-check.sh's restricted map (authorized abiba) — the auth job currently no-ops in CI (GITEA_ACTOR unset; confirmed in run 438's auth log: "Could not determine PR author … Skipping auth check"). Flagging in case Abiba wants to review the restricted-file touch.

## What A commit-time secret guard that **fails** the build on a credential-shaped string, wired into the repo's local gate and the Gitea Actions pipeline. The 2026-09-17 purge removed six live credentials that had sat here 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 makes it exit 1. Backlog row: `commit-time-secret-guard-20260917` (captain approved 2026-09-22: *"definitely add the ci check that blocks committed credentials now"*). ## Shape | File | Role | |------|------| | `scripts/secret-scan.sh` | guard: `--tree` (default), `--path DIR`, `--staged`, `--diff REF`; exit 1 on finding, 2 on config error | | `scripts/secret-patterns.tsv` | checked-in pattern list (`sk-`, `sk-or-v1-`, `sk_live_`, literal `Bearer`, `PVEAPIToken=`, raw `Authorization`, PEM blocks, prose `credentials:` lines, `password`/`api_key`/`secret`/`token` assignments carrying a literal value) | | `scripts/secret-allowlist.tsv` | exceptions, **one entry per deliberate synthetic example, each with a reason**; a missing reason is a hard error (fail closed) | | `tests/test_secret_scan.sh` | 20 cases, incl. the `--staged` commit-time path | | `scripts/prose-lint.sh` | the local gate now runs the scan | | `.gitea/workflows/pr-pipeline.yaml` | explicit `Committed-credential scan` step + self-test in the `lint` job | Design notes: * **Prose is scanned exactly like code** — `cred-prose` matches ``- Admin credentials: `admin` / `<value>` ``, which a `PASSWORD=`-only rule would miss. That is exactly where the original exposures were. * **Nothing is trained to ignore the word "synthetic".** The 2026-09-17 purge's ``«vault: <project>/<env> <SECRET>»`` markers and the `sk-synthetic-*` examples are listed as explicit, reasoned allowlist entries rather than filtered by a general "vault"/"synthetic" rule. A new occurrence needs a new reviewed entry. * **A scan never echoes a credential.** Findings print only the text *before* the match, then `<redacted>` — so a value the regex stopped short of (e.g. a backticked password after `credentials:`) is still never written to the log. * **Portable to the runner.** The Gitea runner executes job steps inside the runner container (Alpine / BusyBox grep, no node/python). The guard uses only bash + `grep`/`sed`/`awk` + `git`; verified in-container: bash 5.3.3, BusyBox grep `-EIi`/`\b`/`«…»`, `awk match()/RSTART`, `mapfile -d`, `nocasematch`, `${var/pat/repl}` all work. ### CI filename note The brief said `.gitea/workflows/ci.yml`; this repo has no `ci.yml` — the only workflow is `.gitea/workflows/pr-pipeline.yaml`. The guard went into the `lint` job there (a required status context, and the `gate` job `needs` it), so a finding fails the merge gate. No duplicate workflow was created. ## Acceptance evidence ### 1a. The guard FAILS on planted pattern-matching secrets (self-test) ```text $ bash tests/test_secret_scan.sh ── secret-scan self-test ── ✅ scanner parses with bash -n (exit 0) ✅ planted sk-or-v1 key fails the guard (exit 1) ✅ planted sk-or-v1 key names the openrouter-key rule ✅ planted literal Bearer token fails the guard (exit 1) ✅ planted Bearer token names the bearer-token rule ✅ planted Proxmox token fails the guard (exit 1) ✅ planted Proxmox token names the proxmox-token rule ✅ planted PEM private key fails the guard (exit 1) ✅ planted PEM key names the private-key rule ✅ planted prose credential line fails the guard (exit 1) ✅ planted prose line names the cred-prose rule ✅ planted password assignment fails the guard (exit 1) ✅ planted password assignment names the secret-assign rule ✅ env refs, sentinels and variable names are not credentials (exit 0) ✅ current repo tree passes the guard (exit 0) ✅ tree run reports the allowlisted exceptions it applied ✅ allowlisted text at an unlisted path still fails (exit 1) ✅ staged credential fails at commit time (--staged) (exit 1) ✅ staged credential names the openrouter-key rule ✅ allowlist entry with no reason fails closed (exit 2) ✅ secret-scan self-test passed (20 cases) exit=0 ``` ### 1b. The test BITES on the pre-fix revision `8245716^` is the commit **before** the 2026-09-17 purge, i.e. the revision where the live credentials still lived. The guard fails on it (22 findings), naming the real credential shapes — the OpenRouter keys, the Mumuni LiteLLM/Zulip keys, the Proxmox token, and the Stirling credentials. Output is verbatim, including the masking: ```text # Guard run against the PRE-FIX revision: 8245716^ = the commit before the # 2026-09-17 purge (live credentials still in the tree). $ bash scripts/secret-scan.sh --path $PRE_FIX_TREE ── secret scan (path): 76 files under /tmp/tmp.k0QYcqPj0R ── ❌ stirling-pdf-agent-access.prose.md:47 [secret-assign] credential assignment carrying a literal value | -H "X-<redacted> ❌ scripts/daily-infra-report.py:19 [proxmox-token] Proxmox API token literal | AUTH = "Authorization: <redacted> ❌ scripts/daily-infra-report.py:19 [auth-header] Authorization header carrying a raw literal value | AUTH = "<redacted> ❌ litellm-api-keys.prose.md:147 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | MUMUNI_LITELLM_API_KEY=<redacted> ❌ litellm-api-keys.prose.md:147 [secret-assign] credential assignment carrying a literal value | MUMUNI_LITELLM_<redacted> ❌ litellm-api-keys.prose.md:148 [secret-assign] credential assignment carrying a literal value | MUMUNI_ZULIP_<redacted> ❌ infrastructure-control.prose.md:639 [proxmox-token] Proxmox API token literal | AUTH="Authorization: <redacted> ❌ infrastructure-control.prose.md:639 [auth-header] Authorization header carrying a raw literal value | AUTH="<redacted> ❌ hermes-key-enforcement.prose.md:104 [secret-assign] credential assignment carrying a literal value | <redacted> ❌ hermes-key-enforcement.prose.md:112 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | api_key: <redacted> ❌ hermes-key-enforcement.prose.md:112 [secret-assign] credential assignment carrying a literal value | <redacted> ❌ hermes-key-enforcement.prose.md:185 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | grep -rn 'LITELLM_API_KEY=<redacted> ❌ hermes-key-enforcement.prose.md:185 [secret-assign] credential assignment carrying a literal value | grep -rn 'LITELLM_<redacted> ❌ agent-zero-openrouter-key.prose.md:93 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | | **Full Key** | `«redacted:<redacted> ❌ agent-zero-openrouter-key.prose.md:93 [openrouter-key] OpenRouter API key | | **Full Key** | `«redacted:<redacted> ❌ agent-zero-openrouter-key.prose.md:104 [openrouter-key] OpenRouter API key | | 2026-09-01 | fix-401 | Old key `<redacted> ❌ agent-zero-fix-summary.md:19 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | **Old Key**: `<redacted> ❌ agent-zero-fix-summary.md:19 [openrouter-key] OpenRouter API key | **Old Key**: `<redacted> ❌ agent-zero-fix-summary.md:20 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | **New Key**: `<redacted> ❌ agent-zero-fix-summary.md:20 [openrouter-key] OpenRouter API key | **New Key**: `<redacted> ❌ agent-zero-fix-summary.md:51 [openai-key] OpenAI/LiteLLM-style "sk-" secret key (also hyphenated sk-proj- keys) | sed -i "s|^API_KEY_OPENROUTER=.*|API_KEY_OPENROUTER=<redacted> ❌ agent-zero-fix-summary.md:51 [openrouter-key] OpenRouter API key | sed -i "s|^API_KEY_OPENROUTER=.*|API_KEY_OPENROUTER=<redacted> ❌ SECRET SCAN FAILED — 22 credential-shaped string(s) in path content. Fix: remove the credential and read it from the vault/env. Only a deliberate synthetic example may be added to scripts/secret-allowlist.tsv, one entry per file/rule/literal, with a reason. Never allowlist a live credential. exit=1 ``` ### 2. The guard stays QUIET on the allowlisted synthetic examples ```text $ bash -n scripts/secret-scan.sh scripts/prose-lint.sh tests/test_secret_scan.sh (clean) $ shellcheck -S warning scripts/secret-scan.sh tests/test_secret_scan.sh scripts/prose-lint.sh (clean) $ bash scripts/secret-scan.sh ── secret scan (tree): 85 files under /root/.treehouse/prose-contracts-9ce5f3/3/prose-contracts ── ✅ secret scan clean (tree; 36 allowlisted exception(s), 22 inert value(s) ignored) exit=0 ``` The self-test also proves the exception is **path-explicit, not word-based**: the exact ``- Admin credentials: `«vault: ...»` `` line that is allowlisted in `infrastructure-control.prose.md` **fails** when copied to an unlisted path. ### 3. `bash -n` / linter clean, current tree passes (both in the block above) and the local gate: ```text $ bash scripts/prose-lint.sh # the local gate, now including the scan Cross-contract: 10 total warnings across all checks ── 4. Secret scan (committed credentials) ── ── secret scan (tree): 85 files under /root/.treehouse/prose-contracts-9ce5f3/3/prose-contracts ── ✅ secret scan clean (tree; 36 allowlisted exception(s), 22 inert value(s) ignored) ✅ No committed credentials ═══════════════════════════════════ ✅ LINT PASSED (10 warning(s)) exit=0 ``` ## Runner verification `runner-ct110` (act_runner, `GITEA_RUNNER_LABELS=ubuntu-latest,ubuntu-22.04`) runs job steps inside the runner container itself. Inspected live: the container has no python/node-based dependency need, and does have bash 5.3.3 plus BusyBox grep/awk with every feature the guard uses (`-EIi`, `\b`, `«…»` alternation, `awk match()/RSTART`, `mapfile -d`, `nocasematch`). Same tools the existing `lint` job already uses. ## Residual items for the contract owner Two literals are allowlisted because they are already-documented history, but they are worth a proper redaction by whoever owns those contracts: * `litellm-api-keys.prose.md:276` — a truncated **real** Zulip key prefix (9 characters, printed as `…` here deliberately); not usable at 9 chars, but ideally replaced with a `` «vault: ...» `` reference. * `litellm-self-heal.prose.md:119` — `sk-syslog-local-master-key`, documented as deprecated/no-live-usage. ## Scope / authorization Scoped to the guard, its patterns/allowlist, its tests, the local-gate call, the CI step, and one AGENTS.md pointer. `scripts/prose-lint.sh` is in `prose-auth-check.sh`'s restricted map (authorized `abiba`) — the auth job currently no-ops in CI (`GITEA_ACTOR` unset; confirmed in run 438's auth log: *"Could not determine PR author … Skipping auth check"*). Flagging in case Abiba wants to review the restricted-file touch.
abiba-bot added 1 commit 2026-09-22 15:04:56 +00:00
feat(security): commit-time secret guard that FAILS the build on a committed credential
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
8f1e5eebc4
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.
abiba-bot merged commit 87205e6fdb into master 2026-09-22 15:33:17 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: SyslogSolution/prose-contracts#129