feat(signatures): add the malware identity table, drawn from source not memory
The last missing data file. Seven known-bad-identity signatures over four families - webshell, reverse_shell, cryptominer, hacktool - reproduced verbatim from llm-security/knowledge/signatures.json at b0de0ca, key order included. The file was generated from the parsed source rather than typed, and provenance pins the source's byte length (2494) and SHA-256 so the claim is checkable rather than asserted. Note the family spellings: reverse_shell with an underscore, and cryptominer rather than miner. The working note this file was planned from had both wrong. They are policy keys - the engine filters on them and interpolates them into every finding title - so a rename is a breaking change, which is exactly why the table was read instead of recalled. The rules were the easy half. The substance is engine_behaviour_not_data, which draws the line between the table and the runtime around it. No rule carries a flags field, because the engine compiles every pattern with `i` unconditionally at signature-scanner.mjs:48 - so a consumer compiling these case-sensitively silently under-matches all seven, and the dialect block records that where a reader will hit it. Also engine, not data: matching against five decode variants rather than raw bytes, the enabled-families policy filter, per-file rule dedup, custom-rule merging, and a loader that defaults four missing fields instead of rejecting a rule. Two limits are stated as evidence limits rather than left implied. Seven signatures are not malware coverage; a clean SIG result is not "no malware", and the seed runtime's own header calls the table deliberately tight. And three of the seven match on names - xmrig, mimikatz, meterpreter - so a document discussing those tools matches. The seed runtime hides that by excluding knowledge/, tests/, docs/ and node_modules/, which is scan scoping and does not travel with the table. Verified: 7/7 rule objects field-identical to source including key order, no non-ASCII bytes, all seven compile in Node bare, i and iu (21/21) and in Python re (7/7). Charter guard clean - no executable code in the repository. README and CHANGELOG updated: the file moves out of "planned, not in v0.1.0" and out of "not included". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
This commit is contained in:
parent
7e2e92eecf
commit
47760d2264
3 changed files with 204 additions and 3 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -32,6 +32,31 @@ change. **Not yet tagged** — see *Not included* below.
|
|||
16-prefix key set.
|
||||
- `calibration/calibration.json` — risk-score tier constants, verdict thresholds, risk-band
|
||||
cutoffs, posture grade thresholds.
|
||||
- `signatures/malware-signatures.json` — the known-bad-identity table for the `SIG` class:
|
||||
seven signatures over four families (`webshell`, `reverse_shell`, `cryptominer`,
|
||||
`hacktool`), reproduced verbatim from `knowledge/signatures.json` at `b0de0ca`, key order
|
||||
included, with the source file's byte length and SHA-256 pinned in `provenance`.
|
||||
|
||||
The rules were the easy half. The file's substance is the line between the table and the
|
||||
engine, drawn in `engine_behaviour_not_data`: **no rule carries a `flags` field**, because
|
||||
the engine compiles every pattern with `i` unconditionally — so a consumer that compiles
|
||||
these case-sensitively silently under-matches all seven. Each pattern is also run against
|
||||
five decode variants, not just raw bytes; rules are filtered by an enabled-families policy;
|
||||
a rule fires once per file; operator rules are merged at scan time; and the loader defaults
|
||||
four missing fields rather than rejecting a rule. None of that travels with the data, and
|
||||
all of it changes what a consumer sees.
|
||||
|
||||
Two honesty notes are in `evidence_limits` rather than in prose. Seven signatures are not
|
||||
malware coverage — a clean `SIG` result is not "no malware", and the seed runtime's own
|
||||
header calls the table "deliberately tight". And three of the seven match on **names**
|
||||
(`xmrig`, `mimikatz`, `meterpreter`), so a document *discussing* those tools matches; the
|
||||
seed runtime papers over this by excluding `knowledge/`, `tests/`, `docs/` and
|
||||
`node_modules/` from the scan, which is engine behaviour and does not come with the table.
|
||||
|
||||
Verified: 7/7 rule objects field-identical to source including key order, no non-ASCII
|
||||
bytes, and all seven compile in Node bare, `i` and `iu` (21/21) and in Python `re` (7/7).
|
||||
Note the exact family spellings — `reverse_shell`, not `reverse-shell`, and `cryptominer`,
|
||||
not `miner`; they are policy keys, and the working note that seeded this file had both wrong.
|
||||
|
||||
### Verification
|
||||
|
||||
|
|
@ -186,7 +211,6 @@ checks that were not run instead of attaching a caveat to a pass.
|
|||
|
||||
### Not included
|
||||
|
||||
- `signatures/malware-signatures.json` — seed data not yet delivered.
|
||||
- `spec/decode-pipeline.md` — needs the decode implementation. A normative spec inferred
|
||||
from a data dump would be worse than an absent one.
|
||||
- `conformance/` — still absent, with half the blocker cleared. 105 of the guard's 134
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@ and keeping it that small is the point.
|
|||
| [`lexicon/injection-lexicon.json`](lexicon/injection-lexicon.json) | Prompt-injection pattern lexicon: 83 patterns in four **severity** families (`critical`, `high`, `medium`, `hybrid`), each with a stable `id` and per-runtime aliases. The thematic class (`override:`, `evasion:`, `hitl-trap:`, …) is the id prefix, not the family. |
|
||||
| [`codepoints/carriers.json`](codepoints/carriers.json) | Invisible and deceptive carriers: zero-width characters, BIDI controls, Unicode Tag block ranges, and the homoglyph map. |
|
||||
| [`signatures/secret-egress.json`](signatures/secret-egress.json) | Credential and token shapes that must never leave a machine, in a portable regex dialect. |
|
||||
| `signatures/malware-signatures.json` | **Planned, not in v0.1.0.** Signature set for the malicious-code class (`SIG`). The seed data has not been delivered yet. |
|
||||
| [`signatures/malware-signatures.json`](signatures/malware-signatures.json) | Known-bad **identity** for the malicious-code class (`SIG`): seven tight signatures over four families — PHP webshells, reverse shells, cryptominers, offensive tooling. Seven signatures are not malware coverage, and the file says so. |
|
||||
| [`signatures/active-content.json`](signatures/active-content.json) | Active content that renders or fetches on its own — Markdown images, links, reference definitions and autolinks, `data:` URIs, active HTML. The EchoLeak class. |
|
||||
| [`calibration/calibration.json`](calibration/calibration.json) | The numbers a detector must not invent: risk-score tier constants, verdict thresholds, risk-band cutoffs, posture grade thresholds. Transcribed from a prose summary, not differentially verified — the file says so itself. |
|
||||
| [`mapping/owasp-map.json`](mapping/owasp-map.json) | Finding-id prefix → OWASP taxonomy entry (LLM / ASI / AST / MCP). |
|
||||
| [`schema/finding.schema.json`](schema/finding.schema.json) | **Normative.** The finding contract, plus the SARIF and JSONL output profiles. |
|
||||
| [`schema/finding.schema.json`](schema/finding.schema.json) | **Normative.** The finding contract — closed against its producer, ten properties — plus the SARIF output profile. The JSONL profile is recorded as `not applicable`, with the reason. |
|
||||
| `spec/decode-pipeline.md` | **Planned, not in v0.1.0.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. |
|
||||
| `conformance/` | **Planned, not in v0.1.0.** One directory per case: `input.txt` in, `expected.json` out. Ground truth. |
|
||||
| [`docs/extraction-plan.md`](docs/extraction-plan.md) | Informative: where each file was seeded from, and what v0.1.0 promised. |
|
||||
|
|
|
|||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue