Capabilities
000a12ffb6c5dfcc0fada157d40500965861ca5e63d4f44fa4a679f7024c6f69 is a 7.5 KB .NET Downloader.exe. MalwareBazaar labels it StormKitty; ClamAV calls it Trojan.Downloader. Its whole job is fetching stage two, and the two strings that say so were both extracted — the URL out of the #US user-string heap, the download primitive out of the metadata name heap:
https://cdn.discordapp.com/attachments/1053623990066876427/1054133763724021820/Xumrybezs.dat
HttpWebRequest
Nine capabilities fire on that file, and eight name only plumbing: .NET assembly load and invoke, HTTP request, response and send, thread suspend, gzip compression. The ninth is the rule below, which did not exist before August 2026. Until it did, the nearest rule was c2-endpoint-uri, which requires a URI template — a scheme plus at least two format-string placeholders (%s or %@) in one string, because a config-driven C2 endpoint is assembled at run time. One concrete literal URL matched nothing.
Adding it moved no verdict, and was not meant to: cdn-hosted-payload.fetch-remote-payload fires while the file still scores Benign at total 0, empty ledger, zero scored indicator hits. A capability is a named behavior a binary can perform, matched from static facts by a TOML rule and reported as a fact carrying no weight of its own.
| Question | Answer |
|---|---|
| What is in the catalog | 923 entries in 76 TOML files, embedded into the engine at build time |
| Where the rules come from | 813 machine-transpiled from Mandiant capa, 110 hand-authored |
| How far a hit can be trusted | 128 Sound, 785 Heuristic, 10 Superset |
| What reaches the malware score | 35 curated rows; the other 888 are enrichment-only |
| How it compares to capa | 46 of capa's 57 capability-classed rules on curl for Windows |
| What is not covered | basic-block scope enforcement, functions past the 6,000-per-analysis budget, runtime rule authoring, YARA, dynamic analysis |
Worked example: the cdn-hosted-payload rule
cdn-hosted-payload requires an abused-hosting download URL and a download primitive together, and fires on 8 of 1,729 corpus samples and 0 of 82 benign binaries:
id = "cdn-hosted-payload"
family = "communication"
attack = ["T1105", "T1102"] # Ingress Tool Transfer; Web Service
mbc = ["B0030"]
[[capability]]
name = "fetch-remote-payload"
scope = "file"
summary = "Hardcoded payload URL on abused public hosting plus a download primitive"
all = [
{ regex = '(?i)https?://(cdn\.discordapp\.com/attachments/|media\.discordapp\.net/attachments/|api\.telegram\.org/(bot|file/bot)|pastebin\.com/raw/|raw\.githubusercontent\.com/|gist\.githubusercontent\.com/|transfer\.sh/|anonfiles\.com/|bitbucket\.org/[^\s"]+/downloads/)' },
{ any = [
{ regex = '(?i)(DownloadString|DownloadData|DownloadFile|DownloadFileAsync|Invoke-WebRequest)' },
{ substring = "System.Net.WebClient" },
{ substring = "WebClient" },
{ substring = "HttpClient" },
{ substring = "HttpWebRequest" },
{ api = "URLDownloadToFileA" }, { substring = "URLDownloadToFileA" },
{ api = "URLDownloadToFileW" }, { substring = "URLDownloadToFileW" },
{ api = "InternetReadFile" }, { substring = "InternetReadFile" },
{ api = "WinHttpReadData" }, { substring = "WinHttpReadData" },
{ api = "curl_easy_perform" },
] },
]
Re-measured 4 August 2026 by raw byte scan — ASCII plus UTF-16LE at both parities — over the 1,729 samples in the malware training corpus and the benign fixture tree, 82 of whose 119 files carry a binary magic. Seven of the eight corpus hits carry a family label across six families — njrat twice, StormKitty, RemcosRAT, AsyncRAT, BQTLock, DiskWriter — and one is unlabelled.
Three properties do that work.
The host list is path-anchored, never host-anchored. It names cdn.discordapp.com/attachments/, not discord — a Discord client, a docs link, and a CSP allowlist all name the domain, and only a direct-download URL carries the attachment path segment.
The conjunction is mandatory. 17 corpus samples carry an abused-hosting URL; requiring a download primitive alongside it cuts those 17 to 8. Regression tests pin the rule in both directions: an abused-host URL alone does not fire, and a download primitive alone does not fire, since ordinary updaters carry one. On the witness the primitive arm is satisfied by HttpWebRequest; System.Net.WebClient is absent at every parity.
A third, looser arm was dropped. That arm was an abused CDN host plus a payload-shaped file extension. It added exactly one malware sample the download-primitive arm already caught, and it was the only arm that produced benign matches: a shell script and a README pulling pinned test fixtures from raw.githubusercontent.com. Neither is a binary, so neither would have reached the scanner in practice; the arm was dropped anyway, because it is exactly the shape a legitimate updater has.
The witness first read as a non-match. A .NET #US user-string heap stores UTF-16LE, frequently at an odd file offset; a scan that stepped only even byte offsets walked straight past the URL. Both parities are scanned now — the measurement was wrong before the rule was.
Rule format
A capability entry carries a name, a scope, a plain-language summary, ATT&CK technique ids, MBC behavior ids, and a match tree — and no weight field. A rule states what the binary can do; whether that costs the binary anything is decided elsewhere, by an explicit table of 35 promotions.
Capabilities vs indicators
Indicators are precision-tuned and scored; capabilities are breadth-tuned and fire on benign software by design — curl legitimately sends HTTP requests, and recording that is the point. Indicators are the curated in-tree baseline rules feeding the malware scorer's category aggregation directly, and are tuned for precision because of it. Both surfaces emit into the same eleven-category taxonomy and the same hit shape; at scan time the baseline runs first and wins on rule_id collision, and the catalog adds coverage on top as a separate enrichment stream that is never folded into the scored set.
The rule catalog
923 capability entries across 76 TOML files under rules/capabilities/, embedded into the engine at build time so every engine binary carries a self-contained ruleset.
| Origin | Entries | Notes |
|---|---|---|
| Machine-transpiled from capa | 813 | in the 30 files carrying the transpiler's generated header; 328 come from capa's nursery/ staging tier — community rules not yet graduated upstream — and are clamped to the lowest confidence tier however specific their predicate |
| Hand-authored | 110 | in the other 46 files, where the catalog leaves capa's vocabulary |
| Total | 923 | across 76 files |
A declared scope decides what a rule is evaluated against, and five of the seven scopes are not binaries at all:
| Scope | Entries | Evaluated against |
|---|---|---|
function | 630 | every recovered function in turn — one hit row per matching function |
file | 144 | the whole-binary surfaces, with no function attribution |
basic-block | 144 | function scope; the block boundary is not enforced |
extension | 2 | a browser-extension manifest — permissions and host permissions |
document | 1 | an OLE/VBA document — auto-exec symbols, macro source, stream names |
lnk | 1 | a Windows shortcut — target path, arguments, invoked interpreter |
installer | 1 | an installer package's signature state |
The last five are container-provider rules. A binary never supplies a container surface, so they are dispatched against an empty one and yield zero hits on an executable rather than being skipped by an exception.
The top-level rule directory fails the build when it is missing, rather than degrading at run time into a smaller built-in list. The capabilities subdirectory is weaker: absent, it compiles to an empty embedded catalog, caught by a test asserting the embedded file list is non-empty rather than by the build. A parse failure does panic loudly on first load, and the same test turns that into a CI failure instead of a shipped binary.
Hand-authored rules
The hand-authored slice covers what capa's vocabulary does not reach: Go-ecosystem rules keyed on module and package names, shellcode execution, EDR-tamper shapes, wiper and ransom families, and the five superset predicates capa's feature extractors cannot express.
The recovered-string surface
All seven macOS stealer rules carry surface = "recovered", meaning they match only strings a family decryptor recovered because they existed on disk encrypted — so they fire zero on benign input by construction. The modifier is not a macOS speciality — 11 capabilities carry it, the seven stealer rules plus four XOR-obfuscated-content rules reading the same decoded partition for URLs, onion addresses, HTTP headers, and persistence paths. A benign browser's cleartext Login Data, Cookies, and login.keychain live in the ordinary string partition those rules never see, and the AMOS decryptor abstains on benign input, measured empty on 6,730 third-party and system Mach-O binaries. The malicious tell is not the string; it is that the string had to be decrypted to exist.
Across the 13 corpus samples the decryptor does fire on:
| Recovered token | Samples (of 13) |
|---|---|
osascript | 13 |
wallet | 8 |
cookies | 8 |
keychain | 4 |
capa coverage manifest
A coverage manifest beside the rules classifies every rule in the pinned capa snapshot — commit aed45e25, 19 June 2026 — 1,031 rows.
| Disposition | Rules | Why |
|---|---|---|
| Native TOML detector | 828 | reproduced in the catalog format |
| Routed to fingerprinting | 157 | they identify what a binary is rather than what it does |
| Structural Rust detector | 11 | not expressible in the TOML format; the 11 rows register 9 detectors, process hollowing carrying three (inject PE, inject DLL, inject thread) |
| Not ported | 35 | 7 capa pseudo-rules plus 28 with a mechanical blocker |
| Total classified | 1,031 | every rule in the snapshot |
A test asserts, separately for the authoritative and nursery tiers and in both directions, that the manifest and the vendored snapshot name exactly the same rules — so the accounting cannot drift from upstream without failing the build.
Unported capa rules
Of the 35 unported rules, 7 are capa's own (internal) … file limitation pseudo-rules, which are housekeeping rather than detections. The other 28 each name a mechanical blocker in the manifest:
| Blocker | Rules | What it means |
|---|---|---|
mnemonic-unmapped | 14 | Keys on a specific x86 mnemonic or addressing idiom the IL abstracts away — hash data using djb2, decrypt data using TEA, check for trap flag exception, reference the VMWare IO port |
instruction-co-location | 5 | Needs "these instructions in the same basic block" adjacency — compute adler32 checksum, decompress data using aPLib, encrypt data using chaskey |
rule-ref-unresolvable | 4 | Depends on another unported rule as a sub-match — steal KeePass passwords using KeeFarce, overwrite DLL .text section to remove hooks |
count-inner-unsupported | 2 | Requires counting matches of an inner sub-clause — contain anti-disasm techniques |
no-family | 2 | Nursery-tier with no authoritative detector family to attach to |
characteristic-unmapped | 1 | encrypt data using RC4 PRGA — keys on a low-level characteristic absent from the feature vocabulary |
None is a dataflow or reachability gap. 22 of the 28 are instruction- and structure-level primitives; the six rule-ref-unresolvable and no-family rows are not blocked by the matcher at all, and unblock by porting something else rather than by adding a primitive.
The matching engine
The catalog compiles once per process behind a lazy static, then evaluates per binary: a literal prefilter first, a predicate tree second.
The literal prefilter
An Aho-Corasick pass over the string surfaces, imports, and section bytes yields the set of present literals before any rule is evaluated. Every literal substring, api, and bytes leaf in the compiled catalog joins one automaton, and every compilable regex leaf joins one combined regex set; a rule descends to tree evaluation only when the index cannot rule it out. The prune is a pure over-approximation — it never drops a rule that would have matched, and the same bitset answers an absent leaf in constant time, so a surviving rule pays only for its present leaves. An unpruned oracle path is retained, and a test asserts the two produce identical hits and identical evidence text.
The prefilter fails open, not closed, and silently. The combined regex set is built against a 64 MB ceiling — roughly 550 regex leaves already exceed the regex crate's 10 MB default — and a set that overflows it disables the regex prune entirely, leaving every regex leaf a candidate for every rule; an Aho-Corasick build failure does the same for literals. Results stay correct; the O(rules × functions) cut that makes them affordable is what disappears.
The predicate tree
Leaves name a static fact recovered from the binary; combinators state how the leaves are required together.
| Leaf | Matches |
|---|---|
substring, regex | literal and pattern text, with base64 and XOR encoding variants |
api, imports, exports | named calls and linkage |
| sections | section presence and bytes |
| numbers, offsets | constants recovered from the lift |
| IL-op classes | operation categories in the lifted code |
il_op + operand together | an op class and an immediate co-occurring on one instruction — cmp reg, 0xCC fires only when a cmp carries 0xCC, where a flattened conjunction of "some cmp" and "some 0xCC" would over-match |
| OS / arch / format guards | validated against closed vocabularies at load time |
class, namespace, member | .NET class, namespace, and member-reference presence over CLR metadata |
| Go module and package names | Go build metadata |
| entitlements, ObjC selectors | Mach-O signature and runtime metadata |
| container leaves | auto-exec symbol, macro source, OLE stream name, LNK target and argument, invoked interpreter, extension manifest permission, host permission, package signature state — supplied only by a document, shortcut, extension, or installer provider, inert on every binary surface |
| Combinator | Semantics |
|---|---|
all / any / not | conjunction, disjunction, negation |
| occurrence-count range | how many times a leaf matched, with optional lower and upper bounds |
min = N over an any list | tallies distinct matching branches rather than total matches; the evidence it emits is the union of every satisfied branch's matched text, not the text of one |
Seven shared fragment files hold the primitive sets more than one rule needs — process spawn, HTTP client, file write, injection, exfiltration, ransom note, and the capa cross-references — and they are inlined at load, so the detectors that share them cannot drift apart.
Superset leaves
Five leaves express what capa structurally cannot:
| Leaf | Supplies |
|---|---|
reaches | call-graph reachability to a capability symbol |
taint_flow | source-to-sink dataflow |
api_arg | a resolved argument value at a call site |
dynload | dynamic-resolution facts |
xpc | macOS XPC service names |
api_arg reads recovered call signatures precisely rather than approximating with capa's "a number near an API" idiom:
[[capability]]
name = "open-raw-socket"
scope = "function"
summary = "Opens a raw socket (AF_INET / SOCK_RAW) — packet crafting / sniffing"
attack = ["T1095"]
api_arg = { api = "socket", arg = "type", value = "SOCK_RAW" }
Hit attribution
A hit carries its rule id, the evidence kind, the reverse-cross-referenced functions the match fired in, and — when the evidence had an address — the matched virtual address, which opens in the decompiler. capa's JSON records that a rule matched; it does not emit per-function attribution as a first-class field.
Neither field is guaranteed, and they are independent. On curl for Windows 36,006 of 36,253 hits carry a matched VA; on the .NET witness only one of nine does, the other eight matching CLR metadata rather than an addressed string — and that one is the file-scope cdn-hosted-payload hit, which by construction attributes no functions.
One rule emits one row per matching function, and nothing collapses them. Deduplication happens only across the two scan paths — the analyze-side pass and the engine window agree by rule id, engine winning — never within a single rule's function fan-out. curl for Windows produces 36,253 rows from 63 distinct rule ids: six byte-pattern rules over .rdata (Camellia, Blowfish, SHA-256, CRC-32, zlib inflate/deflate, Salsa20/ChaCha) contribute exactly 6,000 rows each, because section-global byte evidence is re-attributed to every function evaluated and 6,000 is the per-analysis function budget. The served total counts rows, so it reads 36,253 where the interesting number is 63.
Confidence tiers
Each rule is classified by a pure function of its own predicate tree, scope, and file — no match data, no binary involved.
| Tier | Rules | Assigned when | How to read a hit |
|---|---|---|---|
Sound | 128 | a required leaf, reachable through an all-only spine, is specific enough to trust alone: a byte pattern, a .NET type, an imported symbol, a long anchored substring, a rare constant, or an API conjunction | trustworthy on that single required leaf |
Heuristic | 785 | everything else; every nursery-origin rule is clamped here however specific its shape, because a staging-quality rule is never presented as sound | expected background on benign software |
Superset | 10 | the rule uses one of the five superset leaves | a claim capa's feature extractors could not have produced |
Run over the whole catalog, the classifier reports:
capability confidence histogram: total=923 sound=128 heuristic=785 superset=10
low_confidence=5 att_ck_tagged=516
516 rules carry at least one ATT&CK id and 407 carry none; the 516 name 184 distinct ids, which collapse to 99 parent techniques once sub-technique suffixes are dropped.
The low_confidence flag is the frida-matches-"Friday" shape, and it keys on regexes specifically. A rule is flagged when all four hold:
- file or function scope
- no required leaf
- every leaf a string leaf
- at least one standalone regex whose core is 6 characters or shorter after stripping a leading
(?i)and a trailing(\.exe)?, carrying no\b,^, or$
A substring leaf never trips it however short and unanchored it is. Only the disjunctive frontier is examined — leaves reachable through any and min spines, stopping at all and not — so run-powershell-expression is not flagged despite its short pwsh leaf, because that leaf is conjoined with a word-boundary-anchored discriminator and cannot match on its own.
All five flagged rules are anti-analysis string lists, all five measure zero on the benign tree today as a raw-byte upper bound, and the flag is served so a consumer can drop them:
| Rule | Regex that trips the flag |
|---|---|
anti-analysis.reference-analysis-tools-strings | (?i)regmon(\.exe)? |
anti-analysis.reference-anti-vm-strings | (?i)A M I |
anti-analysis.reference-anti-vm-strings-targeting-qemu | (?i)BOCHS |
anti-analysis.reference-anti-vm-strings-targeting-vmware | (?i)VMWare |
anti-analysis.reference-anti-vm-strings-targeting-virtualbox | (?i)VBOX |
Worked example: the qemu anti-VM branch
A bare unanchored substring is the dominant false-positive shape, and capa's reference anti-VM strings targeting Qemu — which keys on a bare qemu string — is the measured case.
| Branch | Corpus hits (of 1,416) | Benign hits (of 115) | Coincidental |
|---|---|---|---|
bare qemu substring | 15 | 12 | 13 of the corpus hits and all 12 benign hits |
QEMU HARDDISK, QEMU DVD-ROM, QEMU MICRODRIVE, QEMU Virtual CPU | 1 | 0 | the single hit is a publisher, not a detector |
Measured 27 July 2026, when the malware corpus stood at 1,416 samples and the benign fixture tree at 115 files — the same tree the cdn-hosted-payload measurement counts as 82, filtered to the files carrying a binary magic. The benign twelve were eleven instances of the Zig build system's enable_qemu symbol and one sentence from ripgrep's allocator:
<jemalloc>: (This is the expected behaviour if you are running under QEMU)
A bare qemu branch therefore published "ripgrep references anti-VM strings targeting Qemu" because ripgrep's allocator carries a warning message. The corpus noise was the same class: Qt's QEmulationPaintEngine symbol in four samples, garbled Go symbols (HiqEMu, JANofQeMu5, VHqeMU3), and base64 blobs. Word boundaries do not fix it — ripgrep's QEMU) has boundaries on both sides while a real sample's aqemu has none on the left.
The branch was replaced with the device and SMBIOS artifact strings al-khaser actually compares against. That one remaining match is BirdMiner/LoudMiner (904ad9bc506a09be, Mach-O), which bundles a compiled QEMU to run a Linux miner image, so the tokens come from QEMU's own IDE model-string table: the emulator publishing the names, not a detector comparing against them. Static string evidence cannot separate publisher from detector. The branch is retained on vendor grounds with the benign zero as its guarantee, not on corpus evidence — the corpus carries no genuine QEMU anti-VM check at all.
Fixing one branch did not retire the shape. The qemu rule kept a bare (?i)BOCHS alternative for the ACPI OEM table id — exactly the short-unanchored-regex pattern the confidence classifier flags — so the rule carrying this worked example is itself one of the five low_confidence rules tabled above. The flag, not a rewrite, is what carries that class now.
Parity with capa
Detection parity on curl for Windows is 46 of capa's 57 capability-classed rules, and it is the only kind of parity measured against samples — rule-coverage parity is the manifest classification above, a static accounting. Detection parity runs capa 9.4.0 and the engine on the same binary, against the same vendored rules and FLIRT signatures, and diffs the fired capabilities through the manifest's name mapping. A capability counts as a match when both tools fire a rule the manifest maps to the same capa name; the denominator is capa's capability-classed matches only, excluding matches the manifest routes to fingerprinting or to a Rust detector.
| Binary | capa capability-classed matches | Reproduced | Rate |
|---|---|---|---|
| curl for Windows x86-64 — the richest single sample measured | 57 | 46 | 81 % |
| jq for Windows | 25 | 14 | — |
The curl overlap was 38 until four systematic defects were found and fixed, each unblocking a class of rules rather than one rule:
- module-prefix stripping on PE IAT calls
- Mach-O import-stub resolution
- pure-byte crypto scope demotion
- Win32
A/Wsuffix normalization
These are illustrative single-binary figures. No corpus-wide recall rate is stated because none is measured; a gated corpus benchmark does not exist yet.
Most of curl's remaining gap is deliberate. FLIRT-identified statically-linked library code is separated from the binary's own code, so a program that embeds OpenSSL or zlib does not light up as a crypto implementation. On curl that is measurable as 117 library-attributed matches against capa's 1 library function — capa counts those capabilities; here they are labelled library_provided, reported in a separate section rather than on the threat surface, and hard-excluded from scoring. Subtract that class before calling anything a miss.
One asymmetry has no number at all: capa's vivisect backend handles PE and ELF only, so it cannot analyze Mach-O. A direct diff is structurally impossible there, and no Mach-O parity percentage is claimed.
Promotion to the malware verdict
A capability reaches the Malware verdict through a curated promotion table of exactly 35 rows, and the table is frozen. 34 of those rows exist because a bespoke Rust detector was retired into a faithful TOML rule, admitted only after fire-count parity on the benign corpus and recall parity on the malware corpus were proven, and the promoted capability carries the retired detector's exact evidence band. Promoted capabilities sharing a family collapse to one ledger row carrying the highest band that fired, which both prevents multi-row inflation and reproduces a multi-band detector's logic.
The remaining 888 rules are enrichment-only. Uniform consumption would reopen the individual-scoring false-positive path the verdict was recalibrated to remove.
Promotion is a pure refactor with zero detection value — the capability and the retired detector fire on the same facts, which is the retirement gate, so no sample that was missed becomes caught — while a looser capability rule carries real false-positive risk against a benign binary the tighter Rust detector's corroboration would have spared. The downside strictly dominates the upside. Each existing row was proven on an ad-hoc local run that left no regression fence behind it, so no further row is added until a standing full-corpus recall and benign-false-positive gate runs in CI — 1,421 samples when the freeze was declared in July 2026. About 35 further Rust detectors were candidates for promotion; they stay in Rust.
The 35th row is the exception. impact-inhibit-recovery.config retires no detector: it is a third shape inside an already-promoted family, added 25 July 2026 because the family's two command-literal shapes scored 0 of 21 on the MedusaLocker cluster, which drives shadow-copy deletion from an operator-config key rather than an embedded command line. It is the one row that catches samples the promoted set provably missed, and the missing-gate objection is answered for it by an in-tree test fencing the fire and both non-fire shapes — bare token and clean baseline — rather than by an ad-hoc run.
Two guardrails sit around the table.
Library-provided exclusion. A capability whose attributed functions all fall inside identified library code is marked library_provided and excluded; a file-scope hit, having no function attribution, is a whole-file fact and never gets that label. That library set is signature-matched code plus the part of the Go runtime and standard library go build compiles in whether the author wanted it or not — and it deliberately stops there. crypto/aes and os/exec are Go's code but the author's decision, so they stay scoring-eligible and are labelled rather than excluded.
Platform-signed abstention. On a verified platform-signed binary every hit is marked ineligible for scoring while remaining a recorded fact — the abstention is a presence filter, never a negative weight.
API, MCP, and SARIF output
GET /api/v1/binary/{sha256}/capabilities returns the inventory grouped by MITRE tactic, free and ungated beyond the binary's own visibility. Beside the tactic buckets sit an unattributed bucket, for hits whose rule names no resolvable tactic, and a library_provided array held out of the tactic view entirely. Each entry carries:
- Identity —
rule_id,category,summary, plusmbcbehavior ids andreferences— the analyst citation URLs from the source capa rule — joined from the catalog by rule id. - Confidence —
confidence,supersetandsuperset_kind,low_confidence. - Scoring eligibility —
scoring_eligible,library_provided, andprovided_by, which on a Go-built image names which standard-library package implements the attributed code (crypto/aesand the like). - Evidence and attribution —
evidence, resolved ATT&CKtechniques,matched_vawhere the evidence had one, and afunctionsarray of virtual addresses and names.
Three tiers arrive on the wire as two labels. confidence is "sound" or "heuristic"; a superset rule is labelled "sound" and identified by the superset boolean, and an unclassified rule is labelled "heuristic" — the conservative direction. The summary block counts the same way:
| Summary field | Counts |
|---|---|
total | rows, not distinct rules |
sound | every non-heuristic row, superset rows included |
heuristic | heuristic-labelled rows |
superset | rows whose rule uses a superset leaf |
tactics | tactic buckets present |
capa_parity | rows capa could in principle have produced — total minus superset |
beyond_capa | the superset hits it could not |
library_provided | library-attributed rows, held outside total |
The get_capabilities MCP tool serves the same payload to agents; get_rule resolves any rule_id back to its definition. SARIF export emits each capability as an informational note result carrying the ATT&CK taxonomy, in the same log as findings and CVEs.
Limits
Five limits are structural: a declared scope that is not enforced, a per-analysis function budget, a leaf class inert off macOS, no runtime rule authoring, and no dynamic analysis.
Basic-block scope is declared but not enforced. 144 rules carry scope = "basic-block" and all 144 are evaluated at function scope. check-for-outputdebugstring-error requires SetLastError, GetLastError, and OutputDebugString — capa requires the three inside one basic block, meaning the error-code sandwich around a single call; here they need only appear somewhere in the same function, which any Windows program with an error-handling helper can satisfy. Every basic-block rule is that much looser than its capa original.
Only the first 6,000 functions are evaluated. Feature extraction stops at 6,000 functions per analysis, the same cap the interprocedural taint pass takes on the shared lift. curl for Windows presents 6,393 functions, so 393 — 6 % of the binary — are never evaluated against any of the 630 function-scoped rules. The program's own code claims the budget before statically-linked library code, so what falls off the end is library-attributed first, and the cap logs a warning naming both numbers rather than truncating silently.
Resolved-argument rules are largely inert outside macOS. api_arg reads recovered call signatures, and that recovery is macOS- and AArch64-centric, so open-raw-socket rarely fires on ELF or PE — the same posture as the class and namespace leaves, which need CLR metadata to exist at all.
No rule authoring against a running engine, and no YARA. The catalog is TOML in the repository, compiled into the binary; changing a capability rule means shipping a new engine. The capability catalog has no override path at all — it resolves to the embedded files and nothing else. The one runtime override belongs to the findings topic catalog next door: a directory installed once at startup appends topics on top of the embedded catalog, which is always the floor and can never be replaced or shrunk; a malformed override returns an error so the caller refuses to boot rather than silently running a smaller ruleset, and a later call is ignored because the active catalog caches on first read. None of it reaches a capability.
No dynamic analysis. Excluding its shared library rules, capa's snapshot declares a dynamic scope of call on 271 rules and span of calls on 410; nothing here approximates a runtime trace.
Related briefs
Indicators is the curated baseline this catalog sits beside, and Malware is the scorer the 35 promoted rows reach. Findings is a separate rule vocabulary over the same lift, asking whether the code is wrong rather than what it can do — a capability and a CWE finding can name the same function and disagree about whether it is a problem. Answers covers the agent-facing side of the same facts.