Squashed 'scanners/commons/' content from commit 0ffee85
git-subtree-dir: scanners/commons git-subtree-split: 0ffee85a4b83b3661185488c06ed9a9994c11412
This commit is contained in:
commit
a640f43d73
183 changed files with 6245 additions and 0 deletions
251
signatures/active-content.json
Normal file
251
signatures/active-content.json
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"id": "active-content",
|
||||
"description": "Active content in persisted text: constructs that fetch, render or execute on their own when a renderer touches them. The EchoLeak class (CVE-2025-32711). Report-only data: no construct here is defined as something to remove.",
|
||||
"owasp": "LLM05",
|
||||
"detector": "active_content",
|
||||
"label_format": "active:{class}",
|
||||
|
||||
"$comment": "Extracted verbatim, without behaviour change, from llm-ingestion-pipeline-security v0.3.4 (commit 0bf0729, 2026-08-03): src/llm_ingestion_guard/active_content.py (pattern table, pass order, URL-shape analysis, defang) and src/llm_ingestion_guard/calibration.py (per-construct severities and opacity floors). That module documents itself as the canonical home of this table with the Node port sharing its severities, which is why the guard is the seed source here rather than llm-security.",
|
||||
|
||||
"provenance": {
|
||||
"source_repo": "llm-ingestion-pipeline-security",
|
||||
"source_version": "0.3.4",
|
||||
"source_commit": "0bf0729",
|
||||
"source_date": "2026-08-03",
|
||||
"source_files": [
|
||||
"src/llm_ingestion_guard/active_content.py",
|
||||
"src/llm_ingestion_guard/calibration.py"
|
||||
],
|
||||
"verified": true
|
||||
},
|
||||
|
||||
"dialect": {
|
||||
"name": "ecmascript-named-groups",
|
||||
"$comment": "Patterns are written in the ECMAScript / PCRE spelling of named groups. Flags are declared in a `flags` field and never inlined as `(?m)` / `(?i)`, because inline flags are not portable and not always position-independent.",
|
||||
"flags": {
|
||||
"i": "case-insensitive",
|
||||
"m": "multiline: ^ and $ match at line boundaries"
|
||||
},
|
||||
"features_used": [
|
||||
"named capture groups: (?<name>...)",
|
||||
"negative lookbehind: (?<!...)",
|
||||
"negative lookahead: (?!...)",
|
||||
"lookahead assertion: (?=...)"
|
||||
],
|
||||
"translation_notes": [
|
||||
"Python (`re`): rewrite `(?<name>` to `(?P<name>`. Nothing else changes; `re` supports the lookarounds used here.",
|
||||
"Engines without lookbehind (RE2, Go `regexp`, most POSIX): `markdown-link`, `data-uri` and the dot-defang rule cannot be expressed as written. A consumer on such an engine MUST report the pattern as unsupported rather than skip it silently — a skipped pattern is an invisible false negative.",
|
||||
"`\\w` appears once, in the `reference-link` scheme run. Python's `\\w` is Unicode-aware on `str`, ECMAScript's is ASCII-only. RFC 3986 restricts a URI scheme to ASCII, so no reachable input distinguishes them; the difference is recorded rather than normalised away, to keep the pattern byte-identical to its source."
|
||||
]
|
||||
},
|
||||
|
||||
"normalisations": [
|
||||
{
|
||||
"id": "redundant-quote-escape",
|
||||
"affects": ["markdown-image", "markdown-link", "raw-html", "data-uri", "defang.url_in_text_pattern"],
|
||||
"from": "\\\"",
|
||||
"to": "\"",
|
||||
"reason": "The source patterns are Python raw strings, so `\\\"` reaches the engine as backslash-quote — a redundant escape of a character that needs none. Python `re` and ECMAScript without flags accept it, but ECMAScript REJECTS it under the `u` and `v` flags (verified: `new RegExp('\\\\\"', 'u')` throws `Invalid escape`), which would lock every unicode-mode consumer out of five patterns.",
|
||||
"equivalence_evidence": "Differential match-set comparison against the source objects over an adversarial 30-input corpus (bare quotes, escaped quotes, markdown titles containing quotes, quoted and unquoted HTML attributes, quote runs of length 1-5): 5 patterns x 30 inputs, zero differences in match offsets or captured text.",
|
||||
"byte_identical_to_source": false
|
||||
}
|
||||
],
|
||||
|
||||
"redos_note": "Every character run in this table excludes the character that opens its own anchor (`[` for the markdown forms, `<` for the autolink and the raw tag). That exclusion is load-bearing, not cosmetic: without it, crafted input that repeats the anchor and never supplies the required closing literal makes every start position rescan the tail — quadratic. Measured on the source implementation before the exclusions: `<a:` repeated 100000 times cost 23.4s in the autolink pattern alone. Bounding the runs instead would also be linear but is the WRONG fix, because padding past a bound would be a one-line bypass of the very class this table detects. A consumer that rewrites a pattern here MUST re-establish this property.",
|
||||
|
||||
"pass_order": {
|
||||
"$comment": "Constructs are scanned in this order, and every match is masked out of the working text before the next pass — replaced by spaces of the same length, so line structure and later offsets survive. Masking is what makes a construct count once under its most specific class: an image is not also a link, an autolink is not also raw HTML.",
|
||||
"order": [
|
||||
"markdown-image",
|
||||
"markdown-link",
|
||||
"reference-link",
|
||||
"autolink",
|
||||
"raw-html",
|
||||
"data-uri"
|
||||
],
|
||||
"mask_with": "space",
|
||||
"mask_preserves_length": true
|
||||
},
|
||||
|
||||
"constructs": {
|
||||
"markdown-image": {
|
||||
"syntax": "",
|
||||
"pattern": "!\\[(?<alt>[^\\]\\[]*)\\]\\(\\s*(?<url>[^)\\s\\[]+)(?<title>(?:\\s+\"[^\"]*\")?)\\s*\\)",
|
||||
"flags": [],
|
||||
"url_group": "url",
|
||||
"gate": "external_target_only",
|
||||
"severity": "high",
|
||||
"severity_reason": "zero-click: a renderer auto-fetches the URL with no user action",
|
||||
"has_ordinary_form": true
|
||||
},
|
||||
"markdown-link": {
|
||||
"syntax": "[text](url \"title\")",
|
||||
"pattern": "(?<!!)\\[(?<text>[^\\]\\[]*)\\]\\(\\s*(?<url>[^)\\s\\[]+)(?<title>(?:\\s+\"[^\"]*\")?)\\s*\\)",
|
||||
"flags": [],
|
||||
"url_group": "url",
|
||||
"gate": "external_target_only",
|
||||
"severity": "medium",
|
||||
"severity_reason": "click-required: the fetch needs a human action",
|
||||
"has_ordinary_form": true
|
||||
},
|
||||
"reference-link": {
|
||||
"syntax": "[label]: destination",
|
||||
"pattern": "^(?<pre>[ ]{0,3}\\[[^\\]\\[]+\\]:\\s*)(?<url>[A-Za-z][\\w+.\\-]*:\\S+|//\\S+)",
|
||||
"flags": ["m"],
|
||||
"url_group": "url",
|
||||
"gate": "always",
|
||||
"gate_reason": "the pattern is absolute-only by construction: a footnote such as `[1]: some plain text` has no scheme and does not match",
|
||||
"severity": "medium",
|
||||
"has_ordinary_form": true
|
||||
},
|
||||
"autolink": {
|
||||
"syntax": "<scheme:...>",
|
||||
"pattern": "<(?<url>[A-Za-z][A-Za-z0-9+.\\-]*:[^>\\s<]+)>",
|
||||
"flags": [],
|
||||
"url_group": "url",
|
||||
"gate": "always",
|
||||
"gate_reason": "carries a scheme by construction",
|
||||
"severity": "medium",
|
||||
"has_ordinary_form": true
|
||||
},
|
||||
"raw-html": {
|
||||
"syntax": "<tag attr=...>",
|
||||
"pattern": "<(?<slash>/?)(?<name>[A-Za-z][A-Za-z0-9:-]*)(?<attrs>(?:[^>\"'<]|\"[^\"]*\"|'[^']*')*)>",
|
||||
"flags": [],
|
||||
"url_group": null,
|
||||
"gate": "active_tag",
|
||||
"gate_reason": "see `html`: an inherently-executing element, an event handler, or a URL-bearing attribute",
|
||||
"severity": "high",
|
||||
"has_ordinary_form": false,
|
||||
"ordinary_exempt_reason": "active whatever the URL looks like — an event handler needs no URL at all"
|
||||
},
|
||||
"data-uri": {
|
||||
"syntax": "data:...",
|
||||
"pattern": "(?<![A-Za-z0-9])data:[^\\s'\"<>)]+",
|
||||
"flags": ["i"],
|
||||
"url_group": 0,
|
||||
"gate": "always",
|
||||
"gate_reason": "the lookbehind is what keeps `metadata:` from matching; a `data:` URI is its own scheme",
|
||||
"severity": "high",
|
||||
"has_ordinary_form": false,
|
||||
"ordinary_exempt_reason": "carries its own payload; the scheme is rejected by the ordinary-URL test outright"
|
||||
}
|
||||
},
|
||||
|
||||
"gates": {
|
||||
"external_target_only": {
|
||||
"$comment": "Fires only when the URL is absolute or protocol-relative. A relative in-document link resolves against the rendering host and has no attacker-reachable endpoint; flagging it would over-block cross-linked wiki/OKF content, whose core mechanism is relative links.",
|
||||
"pattern": "^(?:[A-Za-z][A-Za-z0-9+.\\-]*:|//)",
|
||||
"flags": []
|
||||
},
|
||||
"always": {
|
||||
"$comment": "Every match is kept; the construct's own pattern already guarantees an external target."
|
||||
},
|
||||
"active_tag": {
|
||||
"$comment": "See the `html` section."
|
||||
}
|
||||
},
|
||||
|
||||
"html": {
|
||||
"$comment": "A raw HTML tag is active if it is an inherently-executing or resource-loading element, carries an event handler, or carries a URL-bearing attribute. Quoted attribute runs are consumed atomically in the tag pattern, because an attribute value may hold `>`.",
|
||||
"active_tags": [
|
||||
"a", "applet", "area", "audio", "base", "button", "embed", "form", "frame",
|
||||
"frameset", "iframe", "img", "input", "link", "math", "meta", "object",
|
||||
"script", "source", "style", "svg", "track", "video"
|
||||
],
|
||||
"event_attr_pattern": "\\bon[a-z]+\\s*=",
|
||||
"event_attr_flags": ["i"],
|
||||
"url_attr_pattern": "\\b(?:src|href|xlink:href|srcset|data|poster|formaction|action|background|cite|codebase|longdesc)\\s*=",
|
||||
"url_attr_flags": ["i"],
|
||||
"tag_name_match": "case-insensitive"
|
||||
},
|
||||
|
||||
"url_shape": {
|
||||
"$comment": "The exfiltration primitive is not a construct type — it is a URL that moves bytes to a host the attacker controls. A URL that merely NAMES a remote document carries nothing outward and is graded `ordinary_severity` instead of the construct's own severity. Without this split, an ordinary document holding one remote image graded the same as ``, which fail-secured ordinary documents (measured on the source implementation v0.3.0).",
|
||||
"ordinary_severity": "low",
|
||||
"ordinary_scheme_pattern": "^(?:https?://|//)",
|
||||
"ordinary_scheme_flags": ["i"],
|
||||
"ordinary_requires": [
|
||||
"an http(s) or protocol-relative scheme",
|
||||
"no query component",
|
||||
"no userinfo (username or password)",
|
||||
"no percent-escape in host or path",
|
||||
"no opaque host label or path segment"
|
||||
],
|
||||
"fragment_excluded": true,
|
||||
"fragment_excluded_reason": "A fragment is never sent to the server, so it cannot carry data to the host a renderer auto-fetches. `.../overview#prerequisites` is the most common shape in real documentation.",
|
||||
"host_case_preserved": true,
|
||||
"host_case_preserved_reason": "Compare on the raw authority, not a lowercased hostname: lowercasing destroys the mixed case a base64 payload smuggled into a subdomain depends on.",
|
||||
"malformed_authority": "never_ordinary",
|
||||
"token_separator_pattern": "[/._\\-~+,;:=&$!*'()]+",
|
||||
"token_separator_note": "Host labels and path segments are split on the separators that delimit a NAME. A token that survives the split and still looks like a blob is carried data.",
|
||||
"opacity": {
|
||||
"$comment": "Three signals, cheapest first. Shared with the entropy detector rather than reinvented, and a backstop rather than the main line of defence: a literal credential in a URL is caught by the secret-egress patterns in the same pass regardless of the severity assigned here.",
|
||||
"tests": [
|
||||
{
|
||||
"id": "base64-decodes-to-printable",
|
||||
"rule": "the token base64-decodes to printable text",
|
||||
"min_length": null
|
||||
},
|
||||
{
|
||||
"id": "hex-blob",
|
||||
"rule": "the token is a hex blob at or above the URL-token hex floor",
|
||||
"min_length": 32,
|
||||
"min_length_note": "Deliberately lower than the prose hex floor (64): in prose a 32-char hex run is usually a checksum, but as a whole path segment or host label it is an opaque id — the md5/uuid length an exfil path uses."
|
||||
},
|
||||
{
|
||||
"id": "shannon-entropy",
|
||||
"rule": "length-paired Shannon entropy at or above the floor",
|
||||
"min_length": 24,
|
||||
"min_entropy_bits_per_char": 4.4,
|
||||
"calibration_note": "Measured 2026-07-25 against real documentation URLs (Microsoft Learn, Wikipedia, GitHub raw, regjeringen.no): the worst legitimate token scored H=4.08 at length 44, while base64/hex payload segments scored 4.36-4.54 and random base62 averages 4.23 at length 24. The floor sits above every measured legitimate token with margin."
|
||||
}
|
||||
],
|
||||
"any_test_sufficient": true
|
||||
},
|
||||
"accepted_false_positive": "A percent-escape counts as carrying, which grades a legitimate `%20` in a path as data-carrying. Accepted deliberately: obfuscated encoding is a core exfiltration primitive, and the ambiguous case belongs on the review side."
|
||||
},
|
||||
|
||||
"defang": {
|
||||
"$comment": "Evidence hygiene: a finding's evidence carries the DEFANGED URL, so a report is safe to log and render without recreating the affordance it flagged. Idempotent — applying it twice changes nothing.",
|
||||
"danger_scheme_pattern": "^(javascript|data|vbscript|file|blob)(?=:)",
|
||||
"danger_scheme_flags": ["i"],
|
||||
"danger_scheme_action": "replace the scheme's trailing colon with `[:]`",
|
||||
"scheme_substitutions": [
|
||||
{ "pattern": "^https", "flags": ["i"], "replacement": "hxxps" },
|
||||
{ "pattern": "^http", "flags": ["i"], "replacement": "hxxp" },
|
||||
{ "pattern": "^ftp", "flags": ["i"], "replacement": "fxp" }
|
||||
],
|
||||
"scheme_substitutions_note": "Ordered, first match wins, and applied only when the danger-scheme rule did not fire.",
|
||||
"dot_pattern": "(?<!\\[)\\.(?!\\])",
|
||||
"dot_replacement": "[.]",
|
||||
"dot_note": "The lookarounds are what make the rule idempotent: a `.` already inside `[.]` is never touched.",
|
||||
"url_in_text_pattern": "[A-Za-z][A-Za-z0-9+.\\-]{0,63}://[^\\s'\"<>]+",
|
||||
"url_in_text_flags": [],
|
||||
"url_in_text_note": "Used to defang bare URLs inside an already-flagged raw HTML tag. The scheme run is bounded here — unlike the detector patterns, where bounding would be a bypass — because this is a defanger applied inside a tag that has already been flagged, and a 64+ character scheme is not resolvable by any renderer. Measured 0.185s at the 1000000-char cap; unbounded it cost 12.99s on 100000 characters."
|
||||
},
|
||||
|
||||
"evidence": {
|
||||
"redact_show_start": 16,
|
||||
"redact_show_end": 6,
|
||||
"redact_applies_when": "length > show_start + show_end + 3",
|
||||
"redact_joiner": "..."
|
||||
},
|
||||
|
||||
"finding_semantics": {
|
||||
"one_finding_per_class": true,
|
||||
"count": "number of kept matches in that class",
|
||||
"graded_by": "worst member",
|
||||
"graded_by_note": "A class collapses to one finding, so an exfil-shaped URL hiding behind an ordinary one must set BOTH the severity and the evidence — otherwise the report shows an innocent URL next to a HIGH verdict.",
|
||||
"evidence_selection": "the first data-carrying hit if any, otherwise the first hit",
|
||||
"mutates_input": false,
|
||||
"renders_disposition": false
|
||||
},
|
||||
|
||||
"known_gaps": [
|
||||
"Pure beaconing: a fetch whose only signal is that it happened. No URL shape distinguishes it from an ordinary remote image.",
|
||||
"Short opaque segments: a carried value below the opacity length floors grades as ordinary.",
|
||||
"Hex-wrapped payloads below the URL-token hex floor."
|
||||
]
|
||||
}
|
||||
177
signatures/malware-signatures.json
Normal file
177
signatures/malware-signatures.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"id": "malware-signatures",
|
||||
"description": "Known-bad-IDENTITY signatures: seven high-confidence regexes naming specific malware families - PHP webshells, reverse shells, cryptominers and offensive-security tooling. Deliberately small and tight. Complementary to the shape-based tables in this repository (entropy, taint, injection): those describe what an attack looks like, this one names things already known to be malicious. Detection data only.",
|
||||
"owasp": "LLM03",
|
||||
"$comment": "Extracted without behaviour change from llm-security/knowledge/signatures.json, read directly from the public remote at commit b0de0ca rather than transcribed. The seven rules are reproduced verbatim, key order included. What the seed runtime DOES with them - compiling every pattern case-insensitively, running each against a decode pipeline as well as raw bytes, filtering by an enabled-families policy, excluding certain paths, merging operator-supplied custom rules - is engine behaviour and is recorded under engine_behaviour_not_data rather than treated as part of the table.",
|
||||
"provenance": {
|
||||
"source_repo": "llm-security",
|
||||
"source_files": [
|
||||
"knowledge/signatures.json"
|
||||
],
|
||||
"source_consumer": "scanners/signature-scanner.mjs",
|
||||
"source_version": "1.0",
|
||||
"source_commit": "b0de0ca6d86ce697f39669d177c2c2654c280128",
|
||||
"source_remote": "ssh://git@git.fromaitochitta.com/open/llm-security.git",
|
||||
"source_bytes": 2494,
|
||||
"source_sha256": "0155368f194c51e01f48e5b253067282a9c083549401a2d374afc46bb89c66bc",
|
||||
"source_delivery": "read directly from the pinned commit; never transcribed through a dump",
|
||||
"verified": "differentially - the seven rule objects here are compared field by field against the parsed source file, and every pattern string is byte-identical, 2026-08-09",
|
||||
"evidence_limits": [
|
||||
"This is an identity table, not a coverage claim. Seven signatures do not detect malware generally; they detect these seven things. A consumer that presents a clean SIG result as 'no malware' is overstating it, and the seed runtime's own header calls the table 'deliberately tight to minimize false positives'.",
|
||||
"Three of the seven match on NAMES rather than on behaviour (`xmrig`, `mimikatz`, `meterpreter` and their kin). A document that merely discusses those tools matches. The seed runtime mitigates this by excluding knowledge/, tests/, docs/ and node_modules/ from the scan - a path exclusion, which is engine behaviour and does not travel with this table. A consumer without an equivalent exclusion will produce false positives this table's own severities do not warn about.",
|
||||
"The provenance strings on each rule name a technique family or a public cheat sheet, not a specific sample or hash. They are attribution for the pattern's origin, not evidence that a given match is that malware."
|
||||
]
|
||||
},
|
||||
"dialect": {
|
||||
"name": "ecmascript",
|
||||
"$comment": "Patterns are ECMAScript regular-expression source text exactly as the source file spells it. Unlike lexicon/injection-lexicon.json, NO rule carries a `flags` field: the engine compiles every pattern with `i` unconditionally (signature-scanner.mjs:48, `new RegExp(rule.pattern, 'i')`). Case-insensitivity is therefore engine behaviour applied to the whole table, not per-rule data. A consumer that compiles these patterns case-SENSITIVELY will silently under-match every one of them.",
|
||||
"flags_applied_by_engine": "i",
|
||||
"flags_declared_per_rule": "none - there is no flags field in this table",
|
||||
"features_used": [
|
||||
"non-capturing groups: (?:...)",
|
||||
"word boundaries: \\b",
|
||||
"character classes",
|
||||
"negated character classes: [^...]",
|
||||
"optional quantifiers: ? and *"
|
||||
],
|
||||
"translation_notes": [
|
||||
"Python (`re`): compile with re.I. No rewriting is needed; verified by compiling all seven.",
|
||||
"All seven compile in Node bare, in Node with `i`, and in Node with `iu` - 21/21 - and in Python `re` with re.I, 7/7. No pattern uses a construct that differs between the two engines.",
|
||||
"No pattern contains a non-ASCII byte, and none contains the redundant `\\/` escape that appears in the other two pattern tables in this repository."
|
||||
]
|
||||
},
|
||||
"normalisations": [],
|
||||
"normalisations_note": "Empty by result, not by omission: all seven patterns are byte-identical to the source file, verified below. No escaping change was needed.",
|
||||
"families": {
|
||||
"$comment": "The family a rule belongs to. This is data - it is the `family` field on each rule, and the seed engine both filters on it and interpolates it into the finding title, evidence and recommendation. Note the exact spellings: `reverse_shell` with an underscore, and `cryptominer` rather than `miner`. A consumer that renames a family breaks the policy key that enables it.",
|
||||
"values": [
|
||||
"cryptominer",
|
||||
"hacktool",
|
||||
"reverse_shell",
|
||||
"webshell"
|
||||
],
|
||||
"rule_counts": {
|
||||
"cryptominer": 2,
|
||||
"hacktool": 1,
|
||||
"reverse_shell": 2,
|
||||
"webshell": 2
|
||||
},
|
||||
"default_enabled": [
|
||||
"webshell",
|
||||
"reverse_shell",
|
||||
"cryptominer",
|
||||
"hacktool"
|
||||
],
|
||||
"default_enabled_note": "All four are enabled by default in the seed runtime (signature-scanner.mjs:30, DEFAULT_FAMILIES), in that order. Recorded because a consumer needs to know the table is fully active by default rather than opt-in; which families a given deployment enables is policy, not data."
|
||||
},
|
||||
"severity_vocabulary": {
|
||||
"used_here": [
|
||||
"critical",
|
||||
"high"
|
||||
],
|
||||
"contract": "schema/finding.schema.json#/$defs/severity",
|
||||
"$comment": "Only two of the five contract severities appear in this table. That is the table's own distribution, not a restriction on the vocabulary."
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"id": "SIG-WEBSHELL-001",
|
||||
"family": "webshell",
|
||||
"severity": "critical",
|
||||
"pattern": "(?:eval|assert|system|exec|passthru|shell_exec|popen|proc_open)\\s*\\(\\s*\\$_(?:POST|GET|REQUEST|COOKIE|SERVER)",
|
||||
"description": "PHP webshell: executes attacker-controlled request data",
|
||||
"provenance": "Classic PHP webshell pattern (c99/r57/b374k families)"
|
||||
},
|
||||
{
|
||||
"id": "SIG-WEBSHELL-002",
|
||||
"family": "webshell",
|
||||
"severity": "high",
|
||||
"pattern": "\\$_(?:POST|GET|REQUEST|COOKIE)\\s*\\[[^\\]]*\\]\\s*\\(",
|
||||
"description": "PHP variable-function call on request data (obfuscated webshell)",
|
||||
"provenance": "Variable-function webshell obfuscation"
|
||||
},
|
||||
{
|
||||
"id": "SIG-REVSHELL-001",
|
||||
"family": "reverse_shell",
|
||||
"severity": "critical",
|
||||
"pattern": "(?:bash|sh)\\s+-i\\s*>&?\\s*/dev/tcp/",
|
||||
"description": "Bash /dev/tcp reverse shell",
|
||||
"provenance": "PentestMonkey reverse-shell cheat sheet"
|
||||
},
|
||||
{
|
||||
"id": "SIG-REVSHELL-002",
|
||||
"family": "reverse_shell",
|
||||
"severity": "critical",
|
||||
"pattern": "\\bnc\\s+-[a-z]*e[a-z]*\\s+/(?:bin|usr/bin)/(?:sh|bash)\\b",
|
||||
"description": "Netcat -e reverse shell",
|
||||
"provenance": "Netcat reverse-shell one-liner"
|
||||
},
|
||||
{
|
||||
"id": "SIG-MINER-001",
|
||||
"family": "cryptominer",
|
||||
"severity": "high",
|
||||
"pattern": "stratum\\+(?:tcp|ssl)://",
|
||||
"description": "Cryptominer stratum pool URL",
|
||||
"provenance": "Stratum mining protocol"
|
||||
},
|
||||
{
|
||||
"id": "SIG-MINER-002",
|
||||
"family": "cryptominer",
|
||||
"severity": "high",
|
||||
"pattern": "\\b(?:xmrig|minerd|cgminer|ccminer|cpuminer)\\b",
|
||||
"description": "Known cryptominer binary reference",
|
||||
"provenance": "Common CPU/GPU miner binaries"
|
||||
},
|
||||
{
|
||||
"id": "SIG-HACKTOOL-001",
|
||||
"family": "hacktool",
|
||||
"severity": "high",
|
||||
"pattern": "\\b(?:mimikatz|meterpreter|sekurlsa::|lsadump::)\\b",
|
||||
"description": "Offensive-security tooling reference",
|
||||
"provenance": "Mimikatz / Metasploit Meterpreter"
|
||||
}
|
||||
],
|
||||
"engine_behaviour_not_data": [
|
||||
{
|
||||
"id": "case-insensitive-compile",
|
||||
"behaviour": "Every pattern is compiled with the `i` flag, unconditionally, at signature-scanner.mjs:48.",
|
||||
"why_not_data": "It is applied to the table rather than declared by it. It is recorded under dialect.flags_applied_by_engine because a consumer cannot reproduce the seed behaviour without it."
|
||||
},
|
||||
{
|
||||
"id": "decode-variant-matching",
|
||||
"behaviour": "Each pattern is tested against five variants of a file's content, not just the raw bytes: raw, normalizeForScan with embedded decoding, the same on trimmed input, homoglyph-folded, and rot13 (signature-scanner.mjs:102-115). The first variant that matches wins, and a match on a non-raw variant is reported as obfuscated.",
|
||||
"why_not_data": "It is the decode pipeline, which this repository does not own and deliberately does not specify. A consumer matching only raw bytes will miss obfuscated instances of these same seven signatures."
|
||||
},
|
||||
{
|
||||
"id": "family-filter",
|
||||
"behaviour": "Rules are filtered to the families named by the `sig.enabled_families` policy key before any file is read; if none are enabled the scanner returns no findings at all.",
|
||||
"why_not_data": "Policy."
|
||||
},
|
||||
{
|
||||
"id": "path-exclusion",
|
||||
"behaviour": "Paths under knowledge/, tests/, docs/ and node_modules/ are skipped, because a ruleset, a fixture and a document about webshells all legitimately contain these patterns.",
|
||||
"why_not_data": "Scan scoping, not detection. But see evidence_limits: without an equivalent, three of these seven rules are noisy by construction."
|
||||
},
|
||||
{
|
||||
"id": "per-file-rule-dedup",
|
||||
"behaviour": "A rule fires at most once per file, regardless of how many times it matches.",
|
||||
"why_not_data": "Reporting behaviour. It means a finding count is a count of (file, rule) pairs, not of occurrences."
|
||||
},
|
||||
{
|
||||
"id": "custom-rule-merge",
|
||||
"behaviour": "Operator-supplied rules from `sig.custom_rules_path` are compiled by the same code and appended to this table at scan time.",
|
||||
"why_not_data": "Runtime extension. A consumer matching only this table matches LESS than a seed deployment with custom rules loaded."
|
||||
},
|
||||
{
|
||||
"id": "missing-field-defaults",
|
||||
"behaviour": "The loader defaults a missing `family` to 'unknown', a missing `severity` to 'high', a missing `description` to the rule id, and a missing `provenance` to null, and drops any rule lacking `id` or `pattern` or whose pattern fails to compile.",
|
||||
"why_not_data": "Loader tolerance. Every rule in this table carries all six fields, so no default is exercised - but a consumer writing a validator should not mistake the loader's leniency for an optional-field contract."
|
||||
}
|
||||
],
|
||||
"totals": {
|
||||
"rules": 7,
|
||||
"families": 4,
|
||||
"critical": 3,
|
||||
"high": 4
|
||||
}
|
||||
}
|
||||
149
signatures/secret-egress.json
Normal file
149
signatures/secret-egress.json
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
{
|
||||
"version": "0.1.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."
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue