feat(analyzer): add ApiKeyRecognizer for provider-issued credentials - #2203
Open
developer0hye wants to merge 4 commits into
Open
feat(analyzer): add ApiKeyRecognizer for provider-issued credentials#2203developer0hye wants to merge 4 commits into
developer0hye wants to merge 4 commits into
Conversation
Adds an API_KEY entity covering AWS access key IDs and secret access keys, GitHub tokens, Google API keys, Slack tokens, Stripe live secret and restricted keys, and JSON Web Tokens. Every pattern is anchored on a vendor-documented, case-sensitive prefix rather than on string length or entropy, so a match is driven by an unambiguous structural marker. Two details this depends on: - Matching is case-sensitive. PatternRecognizer defaults to including re.IGNORECASE, which would make markers such as AKIA and eyJ fire on ordinary lowercase text. - The AWS secret access key has no structure of its own, so it is anchored on the credential name AWS documents (aws_secret_access_key / AWS_SECRET_ACCESS_KEY). PatternRecognizer matches with the regex module, whose variable-length lookbehind keeps the anchor out of the reported span. The IAM prefix table lists AKIA, ASIA, ABIA and ACCA as credentials; AIDA, AROA, ANPA and the rest identify users, roles, groups and policies and are excluded. Stripe publishable keys are excluded because Stripe documents them as safe to expose. Patterns were cross-checked against gitleaks and detect-secrets. Two divergences are deliberate: the AWS access key body follows detect-secrets' [0-9A-Z] rather than gitleaks' narrower base32 [A-Z2-7], which risks false negatives; and the GitHub token body excludes the underscore detect-secrets allows, since the GitHub post specifies base62. Slack and Stripe test fixtures are assembled from fragments so that the test file does not itself trip secret scanners. Supersedes the approach in data-privacy-stack#1339, whose single high-entropy pattern does not compile on Python 3.11+ and cannot be made precise. Closes data-privacy-stack#2202 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cross-checking against detect-secrets showed the Slack pattern accepted xoxa/xoxr/xoxs, which are legacy prefixes absent from Slack's current token-types documentation, while missing xwfp (workflow tokens), which is documented. Narrows the pattern to xoxb/xoxp/xoxe, adds a separate xwfp pattern, and covers both directions in the tests. Keeps every pattern traceable to a current vendor document, which is the premise of this recognizer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ghs_ pattern took its 36-character minimum from a lookahead over
[A-Za-z0-9._-], but the consuming part refuses to end on ".", so
backtracking gives those characters back after the lookahead has counted
them. The reported span could fall below the documented minimum: "ghs_A"
followed by 35 dots was returned as a credential, a one-character body.
Spell the minimum as {35,} plus one final character so the length is
counted on what is actually reported. A structured fuzz run over dotted
inputs now bottoms out at exactly 36.
The final character stays [A-Za-z0-9_-] rather than alphanumeric. A
base64url signature may legitimately end with "-" or "_", so pinning it
to alphanumeric would clip a real stateless token by one character. Only
"." is excluded, because it doubles as sentence punctuation: GitHub's
recommended regex is written for validation, where no right boundary is
needed, but Presidio reports a span, and a greedy match otherwise
reported the period ending a sentence as part of the credential.
The AWS secret value keeps the full documented base64 alphabet including
"=". Thirty random bytes would encode to 40 characters with no padding,
which would make "=" impossible in the value, but AWS documents the
length only, not the generation algorithm, and its own credential
scanning guidance has used [A-Za-z0-9/+=]{40}. Narrowing the set on an
inferred format would trade a rare false positive for a false negative
on a real secret, which is the worse error for a credential recognizer.
Tests now pin the exact-length right boundary instead, which enforces the
documented length without assuming the alphabet.
Also:
- Assert the (?-i:) scope encloses each whole pattern instead of only
checking the prefix, so a group that closes early cannot pass. The
scope check is itself unit tested.
- Point _case_sensitive at the RecognizerListLoader flag assignment that
makes it necessary, so it is not removed in isolation later.
- Grow the stateless-token fixture signature to 152 characters. At 150
(150 % 4 == 2) the final character carries only 2 significant bits, so
canonical unpadded base64url could not end in "-" or "_" there and the
new fixtures would not have represented reachable tokens.
- Drop a noqa: E501 that the previous commit's reformatting made dead,
and the registry-flag implementation detail from the entity table.
Co-Authored-By: Claude Opus 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.
Change description
Adds
ApiKeyRecognizer, a generic recognizer for provider-issued credentials under a newAPI_KEYentity. This targets credentials commonly pasted into logs, support tickets, chat transcripts, notebooks, and configuration snippets, and implements #2202.The recognizer uses vendor-documented, case-sensitive prefixes or credential names. Length and character classes narrow a match, but are not used alone except for the AWS secret after its documented setting/environment-variable name.
Supported formats
AKIA,ASIA,ABIA, orACCAplus the identifier bodyaws_secret_access_key/AWS_SECRET_ACCESS_KEY, followed by an exactly 40-character secretghp_,gho_,ghu_, andghr_token formats;ghs_GitHub App installation tokens are handled separately so both classic and stateless forms are acceptedgithub_pat_formatAIzaformatxoxb-,xoxp-), refresh (xoxe-), rotated access (xoxe.xoxb-,xoxe.xoxp-), app-level (xapp-), and workflow (xwfp-) formatssk_live_,sk_test_,rk_live_,rk_test_)eyJReview follow-up
A source-backed review of the initial implementation found and fixed these correctness gaps:
re.IGNORECASE, andRecognizerListLoaderassigns its global flags to each pattern recognizer after construction. Every credential regex therefore embeds a scoped(?-i:...)group instead of relying only on constructor flags. A registry-level regression test covers this behavior. SeePatternRecognizerandRecognizerListLoader.ghs_JWT form. The installation-token rule now accepts GitHub's documented[A-Za-z0-9.\-_]character set with a minimum of 36 body characters, so the classic opaque token and the stateless form both match. How that match ends is refined in the second pass below.xoxe-) and rotated bot/user access tokens (xoxe.xoxb-/xoxe.xoxp-) are distinct patterns.sk_test_andrk_test_are detected; publishablepk_keys remain excluded.Second review pass
A cross-check of the commit above found one defect and one over-reach. Both are fixed in
fix(analyzer): count installation-token length on the reported span.[A-Za-z0-9._-], but the consuming part refuses to end on., so backtracking returned characters the lookahead had already counted.ghs_Afollowed by 35 dots was reported as a credential — a one-character body. The floor is now written as{35,}plus one final character, so it is counted on what is actually returned. A structured sweep over dotted inputs now bottoms out at exactly 36.=from the value, reasoning that 30 random bytes encode to 40 characters without padding. That arithmetic holds, but the premise is inferred rather than documented: AWS states the length only, not the generation algorithm, and its own credential-scanning guidance has used[A-Za-z0-9/+=]{40}. Narrowing on an inferred format trades a rare false positive for a false negative on a real secret, which is the worse error for a credential recognizer. The documented alphabet is kept, and tests pin the exact-length right boundary instead — that enforces the documented length without assuming the alphabet.Span boundaries deliberately left as they are: a Slack token or a JWT signature may legitimately end in
-or_, so no terminal-character rule is safe for either. Only.is excluded from the end of an installation token, because it doubles as sentence punctuation — GitHub's recommended regex is written for validation, where no right boundary is needed, whereas Presidio reports a span.Two test-quality fixes came out of the same pass:
(?-i:)invariant is now asserted structurally: the check walks parenthesis depth to confirm the scope encloses the whole pattern, since a prefix check would accept a group that closes early. The check is itself unit tested.150 % 4 == 2) the final character carries only 2 significant bits, so canonical unpadded base64url could end only inA,Q,gorwthere — the-/_fixtures did not represent reachable tokens.Deliberate exclusions
pk_) keys are excluded because Stripe documents them as safe for client-side use.sk_orgprefix but does not specify a stable body character set or length, so this PR does not guess a regex for it.eyJ….eyJ….…subset; broadening this regex without an equally precise textual marker would create avoidable false positives.Files changed
presidio_analyzer/predefined_recognizers/generic/api_key_recognizer.py— recognizer and source-backed patternspredefined_recognizers/__init__.py,generic/__init__.py— exportspresidio_analyzer/conf/default_recognizers.yaml— default registrationdocs/supported_entities.md—API_KEYdocumentationtests/test_api_key_recognizer.py— positive, negative, boundary, span-exactness, and default-registry regression coveragePer the changelog policy added in #2200,
CHANGELOG.mdis not changed.Verification
60 passed— API-key recognizer tests209 passed— API-key recognizer plus the pattern-recognizer, recognizer-registry and analyzer-engine suitesruff check— passed for the modified Python filesruff format --check— passed for the modified Python filesgit diff --check— passedSecret-shaped fixtures are assembled from fragments so GitHub push protection does not mistake synthetic test data for live credentials.
Design choices for maintainers
API_KEYentity, with the provider/format retained in the pattern name and analysis explanation. Per-provider entities would enable different anonymization policies but substantially expand the supported-entity surface.Issue reference
Fixes #2202
Checklist