Merge pull request #17 from kristianwind/credential-scan-coverage
CI / Static checks (push) Successful in 11s
CI / WordPress latest + WooCommerce latest on PHP 8.3 (push) Successful in 52s

Close the inert-pattern hole in the credential scan
This commit is contained in:
Kristian Wind
2026-09-17 00:22:11 +02:00
committed by GitHub
+39 -4
View File
@@ -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."