Skip to content

Commit 5373728

Browse files
hyperpolymathclaude
andcommitted
security: fix rust-secrets — 12.5% of Rust repos were never scanned
Two defects in the `rust-secrets` job, both found while verifying the 201-repo secret-scanner re-pin sweep. 1. SILENT NO-SCAN (the serious one). The job ran `grep -rn --include="*.rs" -E "$pattern" src/`. A Cargo workspace keeps its code in `crates/*/src`, so `src/` does not exist at the root — grep exits 2, the enclosing `if` reads that as "no match", and the job passes. Measured across the 64 Rust repos in the sweep: 8 (12.5%) have no root `src/` and were therefore never scanned at all. Verified on a fixture: a planted `pub const SERVICE_TOKEN: &str = "…"` in `crates/core/src/lib.rs` exits 0 under the old job. Another gate that could not fail. Now scans every `*.rs` in the tree, pruning target/ vendor/ .git/. 2. FALSE POSITIVE ON THE CORRECT PATTERN. `let.*api_key.*=.*"` matches any `let api_key =` line containing a quote — including `let api_key = env::var("CLOUDGUARD_API_KEY").ok();`, which is precisely the practice this job exists to enforce. That was the *only* rust-secrets failure in the whole sweep (cloudguard-server src/main.rs:59). A gate whose sole way to go green is to stop reading from the environment is inverted. Exemptions added mirror the four layers already documented on shell-secrets, and match on the VALUE or an explicit pragma — never on a file path, because a path exemption blinds the job to a real secret in the same file (the failure mode .gitleaks.toml was rewritten to avoid in #512): - env-lookup RHS, anchored to the assignment and stopped at `;` so a literal secret with an unrelated env::var mention later on the line still trips; - URL values (an OAuth endpoint is public, not a credential); - comment lines; - inline `// scanner-allow: rust-secrets`. Widened scope is warn-first to 2026-08-21 — the same cutoff and idiom as check-package-policy.sh and check-docs-presence.sh — so repos that were never actually being scanned get a window rather than an immediate red. The advisory branch prints the finding and says NOT YET ENFORCED; it never claims a pass. A malformed cutoff refuses to run rather than defaulting to warn forever. Verified, not assumed: - 6/6 canary cases pass against the script extracted from this YAML (not a copy): correct-pattern allowed; secret in ./src blocks; secret in crates/ advisory today and blocking after the cutoff; literal-plus-env-mention still blocks; malformed cutoff refuses. - Replayed over all 64 Rust repos in the sweep: 0 regressions today, 1 fixed (cloudguard-server). - 3 repos (echidnabot, filesoup, heterogenous-mobile-computing) carry fixture-shaped hits outside ./src and need a one-line pragma before 2026-08-21; each is listed in the run output today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8813ecf commit 5373728

1 file changed

Lines changed: 99 additions & 4 deletions

File tree

.github/workflows/secret-scanner-reusable.yml

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,50 @@ jobs:
131131
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
132132

133133
- name: Check for hardcoded secrets in Rust
134+
env:
135+
# Findings outside ./src (newly visible — see the scan-scope note
136+
# below) warn until this date, then block. Same warn-then-enforce
137+
# idiom and same cutoff as check-package-policy.sh /
138+
# check-docs-presence.sh.
139+
ENFORCE_RUST_WIDE_SCAN_FROM: '2026-08-21'
134140
run: |
141+
TODAY="${RUST_TODAY:-$(date -u +%Y-%m-%d)}"
142+
143+
# An unparseable cutoff would pick the warn branch forever, silently
144+
# disarming the widened scan. Refuse to run instead.
145+
require_date() {
146+
case "$2" in
147+
[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) : ;;
148+
*) echo "::error::rust-secrets: $1='$2' is not YYYY-MM-DD."
149+
echo "Refusing to run: an unparseable cutoff would disarm this gate."
150+
exit 1 ;;
151+
esac
152+
}
153+
require_date ENFORCE_RUST_WIDE_SCAN_FROM "$ENFORCE_RUST_WIDE_SCAN_FROM"
154+
require_date RUST_TODAY "$TODAY"
155+
135156
if ! find . -name Cargo.toml -not -path './target/*' -print -quit | grep -q .; then
136157
echo 'No Cargo.toml found — skipping Rust secrets check'
137158
exit 0
138159
fi
160+
161+
# Scan every .rs file, not just ./src. A Cargo workspace keeps its
162+
# code in crates/*/src, and the previous `grep … src/` matched
163+
# nothing there: grep exits 2 on a missing directory, which the `if`
164+
# read as "clean". Measured 2026-07-21 across the 64 Rust repos in
165+
# the estate re-pin sweep: 8 (12.5%) have no root src/ and were
166+
# therefore never scanned at all — a planted secret in
167+
# crates/core/src/lib.rs passed this job silently.
168+
mapfile -t FILES < <(find . -name '*.rs' \
169+
-not -path './target/*' -not -path '*/target/*' \
170+
-not -path './vendor/*' -not -path '*/vendor/*' \
171+
-not -path './.git/*' -print)
172+
if [ ${#FILES[@]} -eq 0 ]; then
173+
echo 'Cargo.toml present but no .rs files — nothing to scan'
174+
exit 0
175+
fi
176+
echo "Scanning ${#FILES[@]} Rust file(s)."
177+
139178
PATTERNS=(
140179
'const.*SECRET.*=.*"'
141180
'const.*KEY.*=.*"[a-zA-Z0-9]{16,}"'
@@ -145,18 +184,74 @@ jobs:
145184
'password.*=.*"[^"]+"'
146185
)
147186
148-
found=0
187+
# Exemptions, mirroring the four layers already documented on
188+
# shell-secrets. All match on the VALUE or on an explicit pragma,
189+
# never on a file path: a path-based exemption would blind the job
190+
# to a genuine secret in the same file (the failure mode the
191+
# .gitleaks.toml allowlist was rewritten to avoid, standards#512).
192+
#
193+
# 1. env-lookup RHS — `env::var("NAME")` IS the practice this job
194+
# exists to enforce, so flagging it fails the correct code. This
195+
# was the sole rust-secrets failure in the 201-repo sweep:
196+
# cloudguard-server src/main.rs:59,
197+
# `let api_key = env::var("CLOUDGUARD_API_KEY").ok();`. Anchored
198+
# to the assignment and stopped at `;`, so a literal secret with
199+
# an unrelated env::var mention later on the line still trips.
200+
# 2. URL values — an OAuth endpoint is public and not a credential.
201+
# `const MS_TOKEN_URL = "https://login.microsoftonline.com/…"`
202+
# tripped `const.*TOKEN.*=.*"` in four places in one repo.
203+
# 3. comment lines — documentation of a pattern is not a live secret.
204+
# 4. inline pragma `// scanner-allow: rust-secrets`.
205+
COMMENT='^[^:]*:[0-9][0-9]*:[[:space:]]*(//|/\*|\*)'
206+
ENV_RHS='=[^;]*(env::var|env::var_os|std::env|env!\(|option_env!\(|from_env|dotenv)'
207+
URL_RHS='=[[:space:]]*b?"(https?|wss?|ftp)://'
208+
PRAGMA='scanner-allow:[[:space:]]*rust-secrets'
209+
210+
blocking=0
211+
advisory=0
149212
for pattern in "${PATTERNS[@]}"; do
150-
if grep -rn --include="*.rs" -E "$pattern" src/; then
213+
hits="$(grep -nHE "$pattern" "${FILES[@]}" 2>/dev/null \
214+
| grep -vE "$COMMENT" \
215+
| grep -vE "$ENV_RHS" \
216+
| grep -vE "$URL_RHS" \
217+
| grep -vE "$PRAGMA" || true)"
218+
[ -n "$hits" ] || continue
219+
220+
# ./src was the only path the previous version scanned. Hits there
221+
# keep blocking exactly as before — no regression. Hits outside it
222+
# are newly visible, so they warn until the cutoff rather than
223+
# reddening repos that were never actually being scanned.
224+
old_scope="$(printf '%s\n' "$hits" | grep -E '^\./src/' || true)"
225+
new_scope="$(printf '%s\n' "$hits" | grep -vE '^\./src/' || true)"
226+
227+
if [ -n "$old_scope" ]; then
228+
printf '%s\n' "$old_scope"
151229
echo "WARNING: Potential hardcoded secret found matching: $pattern"
152-
found=1
230+
blocking=1
231+
fi
232+
if [ -n "$new_scope" ]; then
233+
printf '%s\n' "$new_scope"
234+
if [[ "$TODAY" < "$ENFORCE_RUST_WIDE_SCAN_FROM" ]]; then
235+
echo "::warning::Potential hardcoded secret outside ./src matching:" \
236+
"$pattern — becomes BLOCKING on $ENFORCE_RUST_WIDE_SCAN_FROM (today is $TODAY)."
237+
advisory=1
238+
else
239+
echo "WARNING: Potential hardcoded secret found matching: $pattern"
240+
blocking=1
241+
fi
153242
fi
154243
done
155244
156-
if [ $found -eq 1 ]; then
245+
if [ "$blocking" -eq 1 ]; then
157246
echo "::error::Potential hardcoded secrets detected. Use environment variables instead."
158247
exit 1
159248
fi
249+
if [ "$advisory" -eq 1 ]; then
250+
# Never claim a clean pass while findings are outstanding.
251+
echo "NOT YET ENFORCED: findings outside ./src are advisory until $ENFORCE_RUST_WIDE_SCAN_FROM."
252+
exit 0
253+
fi
254+
echo 'No hardcoded secrets detected in Rust sources.'
160255
161256
# Shell-specific: catch hardcoded credentials in shell scripts.
162257
# Added to canonical 2026-05-21 after default gitleaks missed a real

0 commit comments

Comments
 (0)