org-ops recorded SECURITY.md as missing against the org standard (coord, 2026-08-11) and this repository owed it for a sharper reason than "given what the repo is about": nothing here runs, so a report is never a crash — it is a detection entry that looks like it works and is not looking. SECURITY.md therefore answers what an ordinary policy does not have to: how to report that a detection-table entry is WRONG, and why a confirmed defect in extracted data is decided in the runtime it was extracted from before it is changed here. Correcting it here would make the copy disagree with the implementation it was taken from — two runtimes, two answers on one input, the exact failure this repository exists to prevent. Two classes skip that routing: a real secret in the history, and data authored here rather than extracted. Fix latency is stated plainly as bounded by the owning runtime's schedule and the consumer's pull, not by ours. secret-egress 0.1.0 -> 0.2.0 is a staleness DISCLOSURE, not a data change: all 18 patterns byte-identical, one evidence_limits entry added. llm-security reports the source table at 19 entries now; recorded as their report and not reproduced, because the commit carrying it is not on their public remote — measured at b1ba1fb today. What was measured here: none of the 18 patterns matches a legacy sk-...T3BlbkFJ... shape. A consumer vendoring this file under-matches the seed hook by one entry, and now reads that in the file. manifest 0.3.0 -> 0.3.1 corrects the secret-egress blocker. Through 0.3.0 it named gcp-service-account-json and openai-api-key-legacy together as ids "absent here". Measured against the guard at e671edb by running this file's own 18 patterns over a service-account document: a COMPLETE service-account key file is matched here at order 11, since the PEM entry's prefix group is optional and the bare PKCS#8 header matches; the same document with private_key removed matches nothing here while the guard's marker still fires. That is a cut-point difference, which is what the blocker is about, not a missing entry. openai-api-key-legacy IS a real hole and is now recorded as one. Folded into the existing blocker string rather than a sibling key, because blockers is a map from table path to text. Verified: all JSON well-formed; every non-fixture JSON has a top-level version; charter clean (no executable code); patterns[] and count byte-identical to HEAD for secret-egress; manifest key set unchanged and count still 90; 90 case directories untouched; every spec still carries its normative marker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T85QqeiWBEoBMjWaMiBnxD
150 lines
7.8 KiB
JSON
150 lines
7.8 KiB
JSON
{
|
|
"version": "0.2.0",
|
|
"id": "secret-egress",
|
|
"description": "Credential and token shapes that must never leave a machine: the fixed pattern table a pre-write guard matches against content before it is persisted. Detection data only - what to DO when one matches (block, warn, redact) is the consumer's policy and is not described here.",
|
|
"owasp": "LLM02",
|
|
"match_semantics": "first match wins; patterns are evaluated in ascending `order`",
|
|
"$comment": "Extracted without behaviour change from llm-security/hooks/scripts/pre-edit-secrets.mjs (`SECRET_PATTERNS`), delivered as operator dump 2/2 through the local coord mailbox on 2026-08-09. NOTE THE SOURCE FILE: the dump states explicitly that this is the engine-consumed hook table and NOT knowledge/secrets-patterns.md, which is a separate PCRE-flavoured agent-consumed variant that stays where it is. This repository's own extraction plan originally named the wrong one of the two; the file recorded here is the one that was actually delivered. Only the 18 fixed entries are data - the dump states that entries 19 and beyond are policy-injected custom patterns at runtime and are not part of the base table.",
|
|
"provenance": {
|
|
"source_repo": "llm-security",
|
|
"source_files": [
|
|
"hooks/scripts/pre-edit-secrets.mjs"
|
|
],
|
|
"source_exports": [
|
|
"SECRET_PATTERNS"
|
|
],
|
|
"source_delivery": "operator dump 2/2, coord message from llm-security, 2026-08-09",
|
|
"source_commit": "unknown - not supplied with the dump",
|
|
"verified": "differentially, against the dump",
|
|
"evidence_limits": [
|
|
"The dump is a transcription of the source module, not the module file itself. The checks recorded for this file prove that this JSON agrees with the DUMP; dump-to-module fidelity is llm-security's assertion, not a result reproduced here.",
|
|
"No severity, and no per-entry disposition, was supplied. The source table carries a name and a pattern and nothing else, so neither is invented here.",
|
|
"The runtime-injected custom patterns (entries 19+) are policy, not data, and are out of scope. A consumer that matches only this table matches LESS than the seed hook does when a policy is loaded.",
|
|
"STALE AGAINST ITS SOURCE, disclosed 2026-08-11 in version 0.2.0. llm-security reports having taken the source `SECRET_PATTERNS` from 18 to 19 fixed entries by adding `OpenAI Legacy API Key`. That is their report, NOT reproduced here: the commit carrying it is not on their public remote, which stood at `b1ba1fb` when this was checked. Measured here against the 18 patterns below: no legacy `sk-…T3BlbkFJ…` key shape matches any of them. So a consumer vendoring this file today under-matches the seed hook by one entry, on a live credential shape, and that is a false negative rather than a difference of opinion. It will be closed by RE-EXTRACTION from a pinned public commit — never by authoring the entry here from a coord message, which is what the behaviour-preservation rule in CLAUDE.md forbids."
|
|
]
|
|
},
|
|
"ordering": {
|
|
"normative": true,
|
|
"$comment": "Array order is part of the contract, not an artefact of serialisation. The source places 'JWT (three-part token)' last deliberately, so that a token inside an Authorization header is reported as 'Authorization header with token' rather than as a bare JWT. A consumer that reorders this table, or that reports all matches instead of the first, will label the same input differently from the seed runtime even though both detected it. The explicit `order` field on every entry exists so that reordering cannot happen silently through a JSON round-trip.",
|
|
"last_entry_is_load_bearing": "JWT (three-part token)"
|
|
},
|
|
"dialect": {
|
|
"name": "ecmascript",
|
|
"$comment": "Patterns are ECMAScript regular-expression source text exactly as the source literals spell it. Flags are declared per pattern; an entry with no `flags` key carries no flags. All 18 compile in Node with their declared flags, in Node with `u` added, and in Python `re` with the equivalent re.I.",
|
|
"flags": {
|
|
"i": "case-insensitive"
|
|
},
|
|
"features_used": [
|
|
"non-capturing groups: (?:...)",
|
|
"bounded quantifiers: {n,m}",
|
|
"word boundaries: \\b",
|
|
"character classes"
|
|
],
|
|
"translation_notes": [
|
|
"Python (`re`): compile with re.I where flags contain `i`. No rewriting needed; verified by compiling all 18.",
|
|
"Two patterns contain `\\/` - the redundant escape a JavaScript regex LITERAL requires and that `RegExp.prototype.source` preserves ('Slack/Discord Webhook URL' and 'Database connection string'). Kept byte-identical because Node bare, Node under `u` and Python `re` all accept it. Engines that reject unknown escapes (Go `regexp`, RE2) MUST report these two as unsupported rather than skip them silently.",
|
|
"The 'Generic credential assignment' and 'Authorization header with token' entries are shape matches, not proofs of a live credential. A consumer treating every match as a confirmed leak will produce false positives; that trade-off belongs to the consumer's policy, not to this table."
|
|
]
|
|
},
|
|
"normalisations": [],
|
|
"normalisations_note": "Empty by result, not by omission: all 18 patterns are byte-identical to the source, verified below. No escaping change was needed.",
|
|
"patterns": [
|
|
{
|
|
"order": 0,
|
|
"name": "AWS Access Key ID",
|
|
"pattern": "AKIA[0-9A-Z]{16}"
|
|
},
|
|
{
|
|
"order": 1,
|
|
"name": "AWS Secret Access Key",
|
|
"pattern": "(?:aws_secret(?:_access)?_key|AWS_SECRET(?:_ACCESS)?_KEY)\\s*[=:]\\s*['\"]?[0-9a-zA-Z/+=]{40}['\"]?",
|
|
"flags": "i"
|
|
},
|
|
{
|
|
"order": 2,
|
|
"name": "Azure Connection String (AccountKey/SharedAccessKey/sig)",
|
|
"pattern": "(?:AccountKey|SharedAccessKey|sig)=[A-Za-z0-9+/=]{20,}"
|
|
},
|
|
{
|
|
"order": 3,
|
|
"name": "Azure AD ClientSecret",
|
|
"pattern": "(?:client[_-]?secret|ClientSecret)\\s*[=:]\\s*['\"][^'\"]{8,}['\"]",
|
|
"flags": "i"
|
|
},
|
|
{
|
|
"order": 4,
|
|
"name": "Azure AI Services Key",
|
|
"pattern": "Ocp-Apim-Subscription-Key\\s*[=:]\\s*['\"]?[0-9a-f]{32}['\"]?",
|
|
"flags": "i"
|
|
},
|
|
{
|
|
"order": 5,
|
|
"name": "GitHub Token",
|
|
"pattern": "(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}"
|
|
},
|
|
{
|
|
"order": 6,
|
|
"name": "npm Token",
|
|
"pattern": "npm_[A-Za-z0-9]{36}"
|
|
},
|
|
{
|
|
"order": 7,
|
|
"name": "Anthropic API Key",
|
|
"pattern": "\\bsk-ant-api03-[A-Za-z0-9_-]{93}\\b"
|
|
},
|
|
{
|
|
"order": 8,
|
|
"name": "OpenAI Project Key",
|
|
"pattern": "\\bsk-proj-[A-Za-z0-9_-]{40,}\\b"
|
|
},
|
|
{
|
|
"order": 9,
|
|
"name": "GitHub Fine-Grained PAT",
|
|
"pattern": "\\bgithub_pat_[A-Za-z0-9_]{82}\\b"
|
|
},
|
|
{
|
|
"order": 10,
|
|
"name": "Google API Key",
|
|
"pattern": "\\bAIza[0-9A-Za-z_-]{35}\\b"
|
|
},
|
|
{
|
|
"order": 11,
|
|
"name": "Private Key PEM Block",
|
|
"pattern": "-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"
|
|
},
|
|
{
|
|
"order": 12,
|
|
"name": "JWT Secret",
|
|
"pattern": "JWT[_-]?SECRET\\s*[=:]\\s*['\"][^'\"]{8,}['\"]",
|
|
"flags": "i"
|
|
},
|
|
{
|
|
"order": 13,
|
|
"name": "Slack/Discord Webhook URL",
|
|
"pattern": "https:\\/\\/(?:hooks\\.slack\\.com\\/services|discord(?:app)?\\.com\\/api\\/webhooks)\\/"
|
|
},
|
|
{
|
|
"order": 14,
|
|
"name": "Generic credential assignment",
|
|
"pattern": "(?:password|passwd|secret|token|api[_-]?key)\\s*[=:]\\s*['\"][^'\"]{8,}['\"]",
|
|
"flags": "i"
|
|
},
|
|
{
|
|
"order": 15,
|
|
"name": "Authorization header with token",
|
|
"pattern": "[Bb]earer [A-Za-z0-9\\-._~+/]{20,}"
|
|
},
|
|
{
|
|
"order": 16,
|
|
"name": "Database connection string",
|
|
"pattern": "(?:postgres|mysql|mongodb|redis):\\/\\/[^\\s]+@[^\\s]+",
|
|
"flags": "i"
|
|
},
|
|
{
|
|
"order": 17,
|
|
"name": "JWT (three-part token)",
|
|
"pattern": "\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b"
|
|
}
|
|
],
|
|
"count": 18
|
|
}
|