Repository navigation
Gate name-based hardcoded-secret rules on the literal's value shape - #7
Merged
Merged
Conversation
The Tier-2 literal scan (FrameScanner._scan_literals) reported CWE-259/
321/798 whenever a credential-named target (e.g. matching
`(crypt|secret|private|signing)[._]?key`) was bound to any string
literal, without looking at the value. Constants that merely name,
label or locate a secret were flagged as hardcoded keys, e.g. (from
Kubernetes) `SSHAuthPrivateKey = "ssh-privatekey"`,
`ServiceAccountPrivateKeyName = "sa.key"`,
`ECPrivateKeyBlockType = "EC PRIVATE KEY"` and
`BootstrapTokenSecretKey = "token-secret"`. Reproduced in Python
(module and function level), Java locals, JS function-scope consts and
C# locals.
Rule A now asks `_is_nonsecret_value(target, value)` first, shared by
all name-based rules. A literal is suppressed only if, in order:
1. it is not key material and carries no credential prefix (veto):
a PEM block with a base64 body, known formats (AWS AKIA..., GitHub
ghp_/github_pat_, GitLab glpat-, Slack xox?-, Google AIza, Stripe
sk_live_, JWT), a high-entropy token (16+ non-whitespace token
chars; 32+ hex, or upper+lower+digit with Shannon entropy >= 3.5),
or any credential prefix at any length is always reported;
2. it has a strict non-secret shape: a PEM block-type label ending in
KEY/CERTIFICATE/REQUEST/..., a bare PEM armor line, a key/cert
filename, a filesystem path, or a placeholder (<...>, ***, xxx,
$UPPER_VAR, ${...}, $(...)); or
3. it is identifier-shaped (lowercase words joined by - _ . or
UPPER_SNAKE) AND tied to the target name: the name carries a
descriptor token (env, var, name, field, header, label, type, file,
path, dir, prefix, suffix, param, attr, column, prop), or every
value token equals a name token, a concatenation of adjacent name
tokens, or a structural word (id, name, field, ref, data), with at
least one matching the name. Names are tokenised on camelCase and
separators; there is no substring matching.
Everything else is reported as before, so realistic low-entropy
secrets such as `password = "hunter2pass"`,
`SECRET_KEY = "super-secret-key"` or
`password = "correct-horse-battery-staple"` still fire.
Literal values reach the SIL with escapes unprocessed, so the PEM-body
match accepts both real newlines and a literal backslash-n. The
connection-string rule (Rule B) already inspects the value and is
unchanged. No existing test changes outcome.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
5 of 6 tasks
More false positives from the name-based hardcoded-secret rules, seen on a real Go codebase and in Kubernetes: - `actionExportPrivateKey = "export-certificate-private-key"`, an ABAC action name. `action`, `permission` and `annotation` are now name descriptors. `role`, `event` and `metric` are deliberately left out, because they often name a real credential (DB_ROLE_PASSWORD, WEBHOOK_EVENT_SECRET, METRICS_AUTH_TOKEN). - `authToken = "Authorization"`, a standard header name. A value that exactly matches a small list of HTTP auth header and scheme names (case-insensitive, exact match only) is silent, except under a password-named target, where "cookie" or "basic" is a plausible weak password. - `csiNodeExpandSecretKey = "csi.storage.k8s.io/node-expand-secret-name"`. A `<dns-subdomain>/<name>` key is judged by its name part under the existing tie-to-name test, so an unrelated name part still fires. The key-material and credential-prefix veto still runs first. Every existing must-fire row still fires, along with new ones such as `password = "Authorization1"`, `token = "authorization-secret-value"`, `api_key = "X-Api-Key-123abc"` and `password = "Cookie"`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A descriptor anywhere in the name, with its trailing "s" stripped, also silenced CI secrets named after where they are used. These fired on main but were silent after fc10b55: ACTIONS_SECRET, GH_ACTIONS_SECRET, GITHUB_ACTIONS_TOKEN, actions_token, DEPLOY_ACTION_TOKEN, PERMISSIONS_TOKEN `action` and `permission` now count as descriptors only when they are the first name token, exact and singular. That is the Go `ActionX`/`PermissionX` idiom, and it still covers `actionExportPrivateKey`, `ActionExportPrivateKey`, `ACTION_EXPORT_PRIVATE_KEY` and `PermissionReadSecretKey`. Nine must-fire rows are added in Python and JS positions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Tier-2 literal scan (
FrameScanner._scan_literals) reports CWE-259/321/798 whenever an assignment target's name looks like a credential, for example(crypt|secret|private|signing)[._]?key. It never looks at the value. As a result, constants that only name, label or locate a secret are reported as hardcoded keys. These examples come from Kubernetes:Each one is reported as CWE-321 in Python (module and function scope), Java locals, JS function-scope
constand C# locals. Real key material, such as a PEM block with a base64 body, is reported correctly, but it gets lost among these false positives.Fix
The name-based rules now share
_is_nonsecret_value(target, value). A literal is suppressed only when the steps below say so, in this order:Veto: always reported. Key material or any credential prefix. Key material means a PEM block with a base64 body, a known format (AWS
AKIA…, GitHubghp_/github_pat_, GitLabglpat-, Slackxox?-, GoogleAIza…, Stripesk_live_, JWT), or a high-entropy token (16+ non-whitespace chars; 32+ hex, or upper+lower+digit with entropy ≥ 3.5). A credential prefix vetoes at any length, sosk_live_xis reported too.Strict non-secret shapes: suppressed. A PEM block-type label (
… KEY,… CERTIFICATE, …), a bare PEM armor line, a key or certificate filename, a filesystem path, or a placeholder (<…>,***,$UPPER_VAR,${…},$(…)).Identifier-shaped values tied to the target name: suppressed. This covers lowercase words joined by
-/_/., or UPPER_SNAKE, but only in one of two cases:env,var,name,field,header,label,type,file,path,dir,prefix,suffix,param,attr,column,prop,annotation).actionandpermissioncount only when they are the leading name token (see the update below).id/name/field/ref/data.Names are split on camelCase and separators.
SSHAuthPrivateKey = "ssh-privatekey"is silent, whileSECRET_KEY = "super-secret-key"andpassword = "correct-horse-battery-staple"still fire.Header / auth-scheme names and Kubernetes qualified keys: suppressed (added in the update below).
Everything else is reported, as before. Human passwords have low entropy, so the check never requires positive proof that a value is a secret.
Literal values reach the SIL with escapes unprocessed, so the PEM-body match accepts both real newlines and a literal
\n. The connection-string rule already inspects the value and is unchanged.Tests
tests/test_secret_value_shape.pywith 374 tests.super-secret-key,django-insecure-…,dev-client-secret,PROD_DB_PASS_2024,$ecret,TOP SECRET, …) and short credential-prefixed values.benchmarks/endor_corpus/ground_truth.pooled.json, fetched at the pinned SHAs, the hit set is identical before and after.Update: action names, HTTP header names, Kubernetes qualified keys
These false positives were still showing up on a real Go codebase and in Kubernetes. They are now silent. The veto from step 1 still runs first.
annotationis now a descriptor.action/permissioncount only as the leading name token, exact and singular (the ActionX/PermissionX idiom), which silencesactionExportPrivateKey = "export-certificate-private-key"(an ABAC action name).GITHUB_ACTIONS_TOKEN,DEPLOY_ACTION_TOKEN,ACTIONS_SECRETandPERMISSIONS_TOKENstill fire.role,eventandmetricare deliberately left out because they often name a real credential, as inDB_ROLE_PASSWORD,WEBHOOK_EVENT_SECRETandMETRICS_AUTH_TOKEN.Authorization,Proxy-Authorization,WWW-Authenticate,Proxy-Authenticate,Cookie,Set-Cookie,X-Api-Key,X-Auth-Token,X-Access-Token,X-CSRF-Token,X-XSRF-Token,BearerandBasic. Matching is case-insensitive, exact-match only, and never used for password-named targets. SoauthToken = "Authorization"is silent, whilepassword = "Cookie",password = "Authorization1",token = "authorization-secret-value"andapi_key = "X-Api-Key-123abc"still fire.<dns-subdomain>/<name>is judged by its name part under the same tie-to-name test as step 3. SocsiNodeExpandSecretKey = "csi.storage.k8s.io/node-expand-secret-name"is silent, whilesecret_key = "example.com/my-app-secret"still fires.Known tradeoffs
secret_key = "secret-key") is treated as a name and stays silent.…_NAME,…_ENV,…_FIELD, …) holding an identifier-shaped value stays silent. The prefix and entropy vetoes still apply.token = "Bearer",secret = "cookie",api_key = "X-Api-Key"), except under password-named targets.secret_key = "vault.corp.io/secret-key").dbPasswordorclientSecretare not matched by the password and secret name rules. That is also true on main, and this PR does not change the name rules.🤖 Generated with Claude Code