Skip to content

Commit 3e4bd4c

Browse files
feat(governance): add secret-scanner-reusable.yml — propagate shell-secrets to 281 repos (#190)
## Summary Extends the reusable-workflow pattern from #168 / #174 / #187 to **secret-scanner.yml**. Same shape as #187 (no per-call inputs except `runs-on`; caller uses `secrets: inherit`). ### Why secret-scanner is the next foundational reusable Estate drift survey (`gh api /search/code` paginated against `org:hyperpolymath`, blob-SHA grouped over **all 281 deployments**): | Metric | Value | |---|---| | Total deployments | **281** | | Unique blob SHAs | **54** | | Structural drift | **19%** (top 4 SHAs cover 69%, top 6 cover 79%) | | Feature variance | **near-zero** — all sampled variants carry the same 3 jobs (trufflehog + gitleaks + rust-secrets) at 75-81 lines | | True drift source | action-SHA pin churn + whitespace | The 100-sample drift estimate (55%) initially ranked secret-scanner third behind mirror; the full pagination reveals the actual figure is 19%. The variance was a sampling artefact. ### Security debt this PR force-fixes The `shell-secrets` job was added to the canonical 2026-05-21 (commit `080c394`) in direct response to the **live Cloudflare API token leak** via `avow-protocol/deploy-repos.sh` (commit `5f2f8b2`) — a leak that both `trufflehog --only-verified` and default `gitleaks` missed. Of 16 estate `secret-scanner.yml` blobs sampled across the top + long-tail SHAs, **0 carry the `shell-secrets` job**. The post-incident guardrail intended to catch the *next* such leak has propagated to nothing. Consolidating the workflow behind this reusable means the wrapper sweep that follows this PR force-promotes `shell-secrets` to all 281 repos in one batch. ### Design - **No per-call inputs other than `runs-on`** — each job self-conditions internally: - `rust-secrets` exits early on no `Cargo.toml` (safe on every repo) - `shell-secrets` no-ops without `.sh`/`.bash` files - `trufflehog` + `gitleaks` always-on (intended) - **`secrets: inherit` required at the call site** — so the inner `secrets.GITHUB_TOKEN` reference in the `gitleaks-action` step resolves. Without `inherit` it falls back to anonymous mode (rate-limited; misses some PRs). - **Caller keeps `on:` + `concurrency:`** — so the read-only cancel-superseded guardrail stays in the wrapper. - SPDX header, top-level `permissions: contents: read`, all actions SHA-pinned — passes the `workflow-lint` job in `governance-reusable.yml`. ### Caller wrapper shape (post-merge) ```yaml # SPDX-License-Identifier: PMPL-1.0-or-later name: Secret Scanner on: pull_request: push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: scan: uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@<sha> secrets: inherit ``` ~12 lines per repo, replacing ~75-116 lines. ### Rollout plan **NOT started in this PR — owner-gated, same as #187 / #174 sweeps.** | Wave | Repos | Action | |---|---|---| | 1: bulk-mechanical | ~275 | Canonical 3-job match. Fan-out single-commit wrapper PR per repo, pinned to this PR HEAD; rebase to merged-main SHA before batch firing. | | 2: slim variants | ~6 | Repos with 2-job (missing `rust-secrets`) or 1-job (`trufflehog` only) older copies. Standardize-up safely since the missing job self-skips on non-applicable repos. | Total expected sweep: ~281 PRs (well above the 82-PR rust-ci precedent — recommend batching by wave; user gates each wave start). ### Pattern hardening - Same `workflow_call` shape as #168 / #174 / #187 — no new infrastructure. - Independent of #174 (`rust-ci-reusable.yml`), #180 (`apply-baseline.sh` glob fix), and #187 (`mirror-reusable.yml`) — no file conflicts; lands in any order. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent e6b2884 commit 3e4bd4c

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# secret-scanner-reusable.yml — Reusable secret-scanner bundle.
3+
#
4+
# Consolidates the per-repo `secret-scanner.yml` workflow (estate-wide:
5+
# 281 deployments, 54 unique blob SHAs, 19% structural drift — top 4
6+
# SHAs cover 69% of repos, top 6 cover 79%). Sampled top + long-tail
7+
# variants all carry the same 3-job structure (trufflehog + gitleaks +
8+
# rust-secrets) at 75-81 lines; drift is action-SHA / whitespace churn.
9+
#
10+
# Critical security debt this PR addresses:
11+
# The `shell-secrets` job (added to the canonical 2026-05-21 in
12+
# response to the Cloudflare token leak via `avow-protocol/
13+
# deploy-repos.sh`, which both `trufflehog --only-verified` and
14+
# default gitleaks missed) is currently present in 0 of 16 sampled
15+
# estate repos. The post-incident guardrail intended to catch the
16+
# *next* leak has propagated to nothing. Consolidating behind this
17+
# reusable force-promotes shell-secrets to all 281 repos on the next
18+
# wrapper sweep.
19+
#
20+
# Design:
21+
# - No per-call inputs other than `runs-on`. Each job self-conditions
22+
# internally (rust-secrets exits early on no Cargo.toml; shell-secrets
23+
# no-ops on no .sh/.bash files), so there is nothing for the caller
24+
# to toggle.
25+
# - Caller MUST use `secrets: inherit` so the gitleaks-action's
26+
# `GITHUB_TOKEN` reference resolves. Without `secrets: inherit`,
27+
# the inner `secrets.GITHUB_TOKEN` is empty and gitleaks falls back
28+
# to anonymous mode (rate-limited; misses some PRs).
29+
# - Caller keeps its own `on:` triggers and `concurrency:` group, so
30+
# the read-only cancel-superseded behaviour stays in the wrapper.
31+
#
32+
# Caller example (wrapper):
33+
# # SPDX-License-Identifier: PMPL-1.0-or-later
34+
# name: Secret Scanner
35+
# on:
36+
# pull_request:
37+
# push:
38+
# branches: [main]
39+
# concurrency:
40+
# group: ${{ github.workflow }}-${{ github.ref }}
41+
# cancel-in-progress: true
42+
# permissions:
43+
# contents: read
44+
# jobs:
45+
# scan:
46+
# uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@<sha>
47+
# secrets: inherit
48+
49+
name: Secret Scanner (reusable)
50+
51+
on:
52+
workflow_call:
53+
inputs:
54+
runs-on:
55+
description: Runner label for all secret-scanner jobs
56+
type: string
57+
required: false
58+
default: ubuntu-latest
59+
60+
permissions:
61+
contents: read
62+
63+
jobs:
64+
trufflehog:
65+
runs-on: ${{ inputs.runs-on }}
66+
steps:
67+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
68+
with:
69+
fetch-depth: 0 # Full history for scanning
70+
71+
- name: TruffleHog Secret Scan
72+
uses: trufflesecurity/trufflehog@6c05c4a00b91aa542267d8e32a8254774799d68d # v3
73+
with:
74+
# The v3 action injects --fail automatically on pull_request events.
75+
# Passing --fail here triggers "flag 'fail' cannot be repeated".
76+
extra_args: --only-verified
77+
78+
gitleaks:
79+
runs-on: ${{ inputs.runs-on }}
80+
steps:
81+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
82+
with:
83+
fetch-depth: 0
84+
85+
- name: Gitleaks Secret Scan
86+
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
87+
env:
88+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
89+
90+
# Rust-specific: hardcoded crypto values. Self-skips when no Cargo.toml
91+
# is present, so this job is safe to run on non-Rust repos.
92+
rust-secrets:
93+
runs-on: ${{ inputs.runs-on }}
94+
steps:
95+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
96+
97+
- name: Check for hardcoded secrets in Rust
98+
run: |
99+
if ! find . -name Cargo.toml -not -path './target/*' -print -quit | grep -q .; then
100+
echo 'No Cargo.toml found — skipping Rust secrets check'
101+
exit 0
102+
fi
103+
PATTERNS=(
104+
'const.*SECRET.*=.*"'
105+
'const.*KEY.*=.*"[a-zA-Z0-9]{16,}"'
106+
'const.*TOKEN.*=.*"'
107+
'let.*api_key.*=.*"'
108+
'HMAC.*"[a-fA-F0-9]{32,}"'
109+
'password.*=.*"[^"]+"'
110+
)
111+
112+
found=0
113+
for pattern in "${PATTERNS[@]}"; do
114+
if grep -rn --include="*.rs" -E "$pattern" src/; then
115+
echo "WARNING: Potential hardcoded secret found matching: $pattern"
116+
found=1
117+
fi
118+
done
119+
120+
if [ $found -eq 1 ]; then
121+
echo "::error::Potential hardcoded secrets detected. Use environment variables instead."
122+
exit 1
123+
fi
124+
125+
# Shell-specific: catch hardcoded credentials in shell scripts.
126+
# Added to canonical 2026-05-21 after trufflehog --only-verified +
127+
# gitleaks defaults both missed a real Cloudflare API token leaked
128+
# via avow-protocol/deploy-repos.sh. As of this PR, 0 of 16 sampled
129+
# estate repos carry this guardrail — the reusable closes that gap.
130+
shell-secrets:
131+
runs-on: ${{ inputs.runs-on }}
132+
steps:
133+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
134+
135+
- name: Check for hardcoded secrets in shell scripts
136+
run: |
137+
# Patterns: an `export FOO=` or `FOO=` with a quoted literal of meaningful length.
138+
# Restricted to *_TOKEN / *_KEY / *_SECRET / PASSWORD to keep false-positives low.
139+
PATTERNS=(
140+
'(export[[:space:]]+)?[A-Z_]*TOKEN[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']'
141+
'(export[[:space:]]+)?[A-Z_]*API_KEY[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{20,}["'"'"']'
142+
'(export[[:space:]]+)?[A-Z_]*SECRET[A-Z_]*=["'"'"'][A-Za-z0-9_./+=-]{16,}["'"'"']'
143+
'(export[[:space:]]+)?PASSWORD=["'"'"'][^"'"'"']{6,}["'"'"']'
144+
)
145+
146+
found=0
147+
for pattern in "${PATTERNS[@]}"; do
148+
if grep -rnE --include='*.sh' --include='*.bash' \
149+
--exclude-dir='.git' --exclude-dir='node_modules' --exclude-dir='target' \
150+
"$pattern" . ; then
151+
echo "::warning::Potential hardcoded secret matching: $pattern"
152+
found=1
153+
fi
154+
done
155+
156+
if [ $found -eq 1 ]; then
157+
echo "::error::Hardcoded secret detected in a shell script. Source from env (see avow-protocol/deploy-repos.sh) instead."
158+
exit 1
159+
fi

0 commit comments

Comments
 (0)