fix(secrets): catch the credential forms that evaded the scanner

Closes SECURITY_ANALYSIS.md H-07.

The generic assignment pattern required a word boundary before names like
`token` and `secret`, and underscore is a word character, so ordinary
environment variable names walked straight past it. Provider patterns were also
incomplete. MCP_TOKEN=, AWS_SECRET_ACCESS_KEY=, OPENAI_API_KEY=, GITHUB_TOKEN=,
github_pat_, glpat-, npm_ and hf_ were all confirmed misses; each now has a
regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0139vHnNpS2sLNELjrAHX5Cu
This commit is contained in:
MarkusandClaude Opus 5 committed 2026-09-13 08:28:14 +00:00
1 parent 08f966d851
commit 723589be8c
2 files changed
+25 -3

No files matched your search

+8 -3
View File
@@ -10,9 +10,12 @@ import re
PATTERNS: list[tuple[str, re.Pattern[str]]] = [
("private_key", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
("openai_key", re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b")),
("anthropic_key", re.compile(r"\bsk-ant-[A-Za-z0-9_-]{20,}\b")),
("github_token", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{30,}\b")),
("openai_key", re.compile(r"\b(?:sk-proj-|sk-)[A-Za-z0-9_-]{20,}\b")),
("github_token", re.compile(r"\b(?:gh[pousr]_[A-Za-z0-9_]{30,}|github_pat_[A-Za-z0-9_]{30,})\b")),
("gitlab_token", re.compile(r"\bglpat-[A-Za-z0-9_-]{20,}\b")),
("npm_token", re.compile(r"\bnpm_[A-Za-z0-9]{20,}\b")),
("huggingface_token", re.compile(r"\bhf_[A-Za-z0-9]{20,}\b")),
("slack_token", re.compile(r"\bxox[abposr]-[A-Za-z0-9-]{10,}\b")),
("aws_access_key", re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")),
("google_api_key", re.compile(r"\bAIza[0-9A-Za-z_-]{35}\b")),
@@ -22,7 +25,9 @@ PATTERNS: list[tuple[str, re.Pattern[str]]] = [
(
"generic_assignment",
re.compile(
r"(?i)\b(api[_-]?key|secret|passwo?rd|token|client[_-]?secret)\b\s*[:=]\s*"
r"(?i)(?:^|[^\w])(?:export\s+)?[A-Z0-9_-]*"
r"(api[_-]?key|secret(?:[_-]?access[_-]?key)?|passwo?rd|token|client[_-]?secret)"
r"[A-Z0-9_-]*\s*[:=]\s*"
r"['\"]?[A-Za-z0-9/+=_-]{20,}['\"]?"
),
),
+17
View File
@@ -22,6 +22,23 @@ def test_generic_assignment_catches_long_values():
assert "generic_assignment" in scan('client_secret = "Xy3k9QpL2mNvB8sT4uW1zC7dF0gH5jK6"')
@pytest.mark.parametrize(
"text,kind",
[
("MCP_TOKEN=abcdefghijklmnopqrstuvwxyz123456", "generic_assignment"),
("export AWS_SECRET_ACCESS_KEY='abcdefghijklmnopqrstuvwxyz123456'", "generic_assignment"),
("OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz012345", "openai_key"),
("GITHUB_TOKEN=abcdefghijklmnopqrstuvwxyz123456", "generic_assignment"),
("github_pat_" + "A" * 82, "github_token"),
("glpat-" + "A" * 24, "gitlab_token"),
("npm_" + "A" * 32, "npm_token"),
("hf_" + "A" * 32, "huggingface_token"),
],
)
def test_common_provider_and_environment_tokens_are_detected(text, kind):
assert kind in scan(text)
def test_scan_never_returns_the_secret_itself():
hits = scan("sk-abcdefghijklmnopqrstuvwxyz012345")
assert all("sk-" not in h for h in hits)