diff --git a/ci/static.sh b/ci/static.sh index cb46a2e..d81eda3 100755 --- a/ci/static.sh +++ b/ci/static.sh @@ -165,9 +165,32 @@ ci_step "no secrets in tracked files" # unpushed. It has to be rotated, and the window between the push and noticing # is the whole problem. # -# Deliberately narrow. Broad entropy heuristics fire on minified assets, hashes -# and base64 images, and a check that cries wolf gets commented out — which is -# worse than not having it. These are prefixes that mean one thing only. +# Two rules, at two different confidence levels. +# +# The first is content-anchored: prefixes that mean one thing only. Broad +# ENTROPY heuristics are still refused here — they fire on minified assets, +# hashes and base64 images, and a check that cries wolf gets commented out, +# which is worse than not having it. +# +# The second is name-anchored, and closes a hole the first cannot see. A prefix +# list matches the shape of a VALUE, so it is blind to any credential that does +# not carry a vendor's marker: an api_key assigned twenty opaque characters +# walks straight past it. This is not an entropy heuristic — it fires only +# where the code itself names the value a secret, which is why it does not +# touch a minified asset. Obvious placeholders are excluded so a settings +# default does not cry wolf. +# +# ⚠️ It DOES fire on a documentation example, and there is no bypass marker on +# purpose — an unexercised escape hatch is its own hollow check. The first +# version of this comment spelled the example out in full and the rule flagged +# its own explanation, which is the one false positive measured so far. Write +# such an example in prose, or break the literal up. +# +# 🔴 A count cannot tell you a pattern is not INERT. Proven the hard way in a +# sibling plugin: it planted an AWS key, the file count rose, and the scan +# passed green — that prefix was simply missing from its list. So every shape +# claimed below is planted and run through this whole script, not grepped in +# isolation, and a lookalike control is checked not to fire. # # 🔴 And it says how many files it read. A pushed key is compromised even after # the commit is deleted, so this scan silently examining nothing is not a weak @@ -178,14 +201,26 @@ ci_step "no secrets in tracked files" # partially wrong, so the only way it misreports is by coming out empty, and # any higher number would just be a guess about the plugin's size. The count on # the success line is what catches a list that shrank without emptying. +secret_value_pat='(sk-ant-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{32,}|ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{50,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----)' + +# The code calls it a secret and then hands it twenty-odd opaque characters. +secret_name_pat="['\"](api[_-]?key|secret|password|passwd|token|auth[_-]?token|private[_-]?key)['\"][[:space:]]*(=>|=|:)[[:space:]]*['\"][A-Za-z0-9/+_-]{20,}['\"]|define\([[:space:]]*['\"][A-Z0-9_]*(SECRET|PASSWORD|TOKEN|API_KEY)['\"][[:space:]]*,[[:space:]]*['\"][A-Za-z0-9/+_-]{20,}['\"]" + +# A default nobody has filled in yet is not a leak. Without this the check goes +# red on the one thing a settings screen is guaranteed to contain. +placeholder_pat='REPLACE|YOUR[_-]|EXAMPLE|CHANGE[_-]?ME|TODO|FIXME|PLACEHOLDER|xxxxx|XXXXX|\*\*\*\*' + before=$fails secrets_scanned=0 while IFS= read -r -d '' f; do [ -f "$f" ] || continue secrets_scanned=$((secrets_scanned + 1)) - if LC_ALL=C grep -nE '(sk-ant-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{32,}|ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{50,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----)' "$f"; then + if LC_ALL=C grep -nEI "$secret_value_pat" "$f"; then note_fail "$f looks like it contains a credential. It cannot be unpushed — rotate it." fi + if LC_ALL=C grep -nEI "$secret_name_pat" "$f" | LC_ALL=C grep -vE "$placeholder_pat"; then + note_fail "$f assigns a long opaque value to something the code calls a secret. If it is one, it cannot be unpushed — rotate it." + fi done < <(git ls-files -z) if [ "$secrets_scanned" -lt 1 ]; then note_fail "The credential scan examined no files at all. git tracks nothing here, so this check proved nothing."