Sign in

Findings (CWE)

A scan of the statically linked, stripped curl in the test corpus (ELF x86-64) produces 1,909 findings. 323 of them sit in a function the forward walk from the binary's entry roots actually reaches. 1,579 sit in functions no entry-rooted path reaches — static initialisers, dead-but-linked library code, handlers registered but never called. Seven are binary-wide posture facts with no calling function at all. The list an auditor reads first is 323 long, not 1,909, and the distance between those two numbers is most of what a static CWE scanner has to earn.

Each finding is a record keyed on a MITRE CWE id, carrying a stable rule identifier, a severity tier, a confidence grade, a one-sentence summary, and — when the detector can resolve a caller — the address of the call instruction and the function containing it. The output is a list of facts. Whether any of them is a vulnerability in production depends on the deployment, the reachability from untrusted input, and the threat model, none of which the engine knows.

QuestionShort answerSection
Which findings to read first?the ones whose caller sits in the entry-root closure — 323 of that curl build's 1,909Reachability triage
What can a detector prove?one of eight signal classes, from symbol presence up to total constant arithmeticDetector signal classes
How much is covered?72 CWE ids, 71 with a live detector, 18 with a contrastive fixture that firesThe CWE catalog; Quality gates
Where does it run?four format-and-architecture combinations carry per-call-site addresses; the taint lattice runs far wider, and most byte-aware argument resolution runs on arm64 onlyFormat and architecture coverage
What does one detector do, and where is it wrong?mechanism, rules, and measured limits, one entry per CWEPer-CWE reference
Is a finding a vulnerability?no — it is a fact about the file, not a judgment about a deploymentLimits of a finding

Worked example: a heap out-of-bounds write

A three-line C bug with no symbol naming it still resolves to a proof. This is a positive fixture from the labelled binary corpus — real C, compiled, checked in, and read by the detector that has to catch it:

char *b = malloc(8);          /* 8-byte buffer */
int i = argc & 0x1f;          /* i in [0, 31] */
b[i] = (char)argc;            /* OOB write when i >= 8 */

Built with zig cc -target x86_64-linux-gnu -O0 -fno-sanitize=undefined, that becomes:

10014b6:  movl    $0x8, %edi
10014bb:  callq   0x1001590 <malloc@plt>     ← allocation site, size 8
10014c0:  movq    %rax, -0x18(%rbp)          ← pointer spilled to a stack slot
10014c4:  movl    -0x8(%rbp), %eax
10014c7:  andl    $0x1f, %eax                ← index constrained to [0, 31]
10014ca:  movl    %eax, -0x1c(%rbp)          ← index spilled
10014cd:  movl    -0x8(%rbp), %eax
10014d0:  movb    %al, %dl                   ← the byte to store
10014d2:  movq    -0x18(%rbp), %rax          ← pointer reloaded
10014d6:  movslq  -0x1c(%rbp), %rcx          ← index reloaded
10014da:  movb    %dl, (%rax,%rcx)           ← the out-of-bounds store

No symbol names the bug. There is no memcpy, no strcpy, no fortified _chk twin, nothing an import-table scan or a string scan can see. The finding is assembled from three recovered facts: the store's address resolves to the abstract object minted at 0x10014bb; that object's size is the literal 8; the index interval reaching the store is [0, 31]. Since 31 exceeds 7, the write can land 23 bytes past the allocation. Rule bounds.oob-write.object, CWE-787.

The spill matters more than the arithmetic. Between the malloc and the store, the pointer round-trips through -0x18(%rbp) and the index through -0x1c(%rbp). An analysis keyed on SSA value identity loses both at the reload, because a reloaded value is a new SSA name for the same thing. Keying the object on its allocation-site address instead of on an SSA name is what survives the -O0 spill — and the interval [0, 31] only survives it because the value-range domain carries ranges through stack slots. Both are properties of the substrate, not of the rule.

Reachability triage

Every finding whose caller is known carries reachable_from_main, a three-state field derived from a forward walk of the recovered call graph. It matters more at scan volume than the catalog does: the reference binaries below return between 25 and 2,629 findings each, and no auditor reads the tail of any of them.

StateWhat it assertsCases
truea static edge chain runs from an entry root to this caller
falsethe closure is trustworthy and does not contain the callerinitialiser-only paths, unregistered callbacks, dead-but-linked code
absentno answer, left empty rather than guessedbinary-wide posture rules, which have no calling function at all; every finding in a binary whose closure is itself untrustworthy

The walk needs roots, and seven tiers contribute them. None is a fallback for another — the walk runs from their union, not from the first tier that resolves.

TierRoot resolved fromWhat it anchors
0Mach-O LC_MAIN or LC_UNIXTHREAD, PE AddressOfEntryPoint, ELF e_entrythe format-supplied entry — present on every executable image and never a guess
1main or _main in the symbol tableany binary that still names its entry point
2the function that calls xpc_main, dispatch_main, NSApplicationMain, CFRunLoopRun, or __CFRunLoopRunstripped Apple daemons that export no main
3every direct callee of a __libc_start_main call sitePIE ELF startup shims, where main arrives as a register argument
4static initialisers and TLS callbacks — ELF .init_array and DT_INIT, Mach-O __mod_init_func, PE TLS callbacksloader-run code outside any main-rooted chain. Constructors only; destructors run at exit() and are excluded
5every defined, non-re-exported entry in the export tablea .so, .dll, or .dylib export has external callers and often no internal one
6ELF .gcc_except_table catch and cleanup handlersC++ landing pads the unwinder enters during exception propagation, outside any static call chain

A small closure is declared untrustworthy, not inflated with extra roots. There is no size-based fallback tier. When the union of those roots reaches under 10% of the call graph's caller nodes, every finding the closure does not contain gets an empty reachable_from_main rather than a false. Findings it does contain still read true: an edge chain exists regardless of how much of the program the closure covers.

That gate bites on exactly the daemon class the field is for. Of airportd's 102 findings, 43 have no calling function at all and 40 more have a known caller but sit below the closure floor — so on a stripped ObjC daemon the third state is the majority, not a residue.

Measured on the checked-in reference binaries:

BinaryFindingsReachable from an entry rootProvably notNo caller, or no answer
ripgrep/macos-aarch64251024
airportd10219083
curl/macos-aarch64153121266
jq/macos-aarch6434212918924
ripgrep/linux-aarch64158137201
curl/linux-x86_641,9093231,5797
curl/linux-aarch642,6293222,3007

Limits. Resolved indirect dispatch is followed; each resolved edge is added alongside the original dispatcher edge. Unresolved dispatch is where the walk stops.

TraversedNot traversed
an Objective-C send the devirtualiser resolves to a concrete method implementationa class chosen from a runtime-computed name
a C++ virtual call resolved to a vtable slota function pointer read from a table the lifter could not recover
an Objective-C, GCD, or XPC block invocationa jump table whose entries did not decode
a switch jump table the lifter recoveredevery other unresolved indirect branch

This is a triage hint; a suspected dynamic-dispatch path has to be checked by hand.

A second closure, reachable_before_auth, runs the same forward walk from network-listener roots — any function calling recv, recvfrom, recvmsg, accept, accept4, or nw_listener_create — and stops at the first function whose outgoing calls include a recognised authentication verifier. true is the pre-auth remote-code-execution shape. It is absent on every finding of all four Mach-O arm64 reference binaries, airportd included, because no listener root resolves in any of them. It earns its keep on network daemons and firmware.

Two demotion passes run before the composer, never after. The origin pass down-ranks a finding whose calling function resolves through the merged signature tables to library, runtime, or glue code rather than to the binary's own. The dead-library pass floors heuristic memory-safety findings inside an identified library function no entry root reaches — statically linked compiler-rt copy loops, libssp _chk helpers — the dominant per-binary false-positive source on statically linked ELF. Neither pass deletes a finding: both only lower severity, so a real bug in a bundled vulnerable library stays auditable instead of becoming a silent miss.

The companion enrichment forward_calls lists the resolved names of every call site reached from the finding's caller — what the suspect function does after the bad primitive fires.

The finding schema

A finding carries seven fields, and none of them is a remediation.

FieldMeaning
cwe_idAnchor in the MITRE CWE catalog.
rule_idStable detector-and-shape identifier (dangerous-call.strcpy, xpc.listener.no-code-signing-requirement, oob-write.memcpy-chk-proven). Survives across releases, so consumers can group, filter, baseline, and suppress on it.
detectorThe mechanism: StaticRule (curated symbol or string match), Heuristic (structural pattern over the call graph), SymbolicExec (the SSA-taint lattice), plus Ml and Manual — both reserved in the schema, neither constructed anywhere in the engine.
severityPer-instance hint at the runtime context around the source-level bug.
confidenceHow trustworthy the detection signal is — not how exploitable the finding is.
summaryOne sentence of user-facing prose: what fired, why it matters, and the audit pivot. Every rule ships one.
locationWhere the finding anchors, itself a record of nine fields. Absent for binary-wide posture findings.

The location record is where triage happens:

Location fieldMeaningPopulation
vaaddress of the call instruction or the anchoring symbolwherever the bridge populated a per-call-site address
symbol, dylibthe dangerous primitive being called and the library that provides itnamed-import findings
caller_va, caller_namethe function in this binary that invokes the primitive; the name falls back to sub_<va> when the symbol table cannot supply onewherever the call graph resolved a caller
forward_callsresolved names of the call sites reached from the callerevery finding with a caller
reachable_from_mainthe three-state entry-closure verdictevery finding with a caller and a usable call graph
reachable_before_auththe pre-auth-closure verdictonly when network-listener roots resolve — zero of the four reference binaries
flow_paththe taint witness: an ordered Source → … → Sink chain of addresses, each tagged source, waypoint, or sink and carrying the resolved primitive name where one is knownonly the taint detectors that retain a source anchor. 26 of curl's 153 findings, 25 of jq's 342, 1 of airportd's 102, 0 of ripgrep's 25

flow_path answers why a sink is reachable, where an address answers only where. It is absent on every static-rule and heuristic finding, and on the interprocedural summary detectors too: those prove that parameter i reaches a sink class without recording an ordered per-operation witness. It exports as a SARIF codeFlow.

There is no remediation field. A finding carries upstream facts only.

Severity is per-instance, not per-CWE. A __strcpy_chk call is the same CWE-120 as strcpy but lands at Info, because the fortified runtime aborts before the overflow corrupts anything. A system() call whose argument resolves to a constant string with no shell metacharacters is the same CWE-78 as system(user_input) but lands at Info too.

TierWhat it assertsDefault for
Criticalunconditionally exploitable source patternraw strcpy, a real PEM private-key body, a proven stack-frame overrun
Highdangerous; only a missing condition makes it saferaw libc buffer primitives
Mediummainstream tools use this legitimately; the auditor decides
Lowneeds corroborationsymbol-only detection of niche functions
Infothe compiler or runtime mitigates, or the match is a parser template rather than a bugfortified _chk twins, constant-argument system()

Confidence tracks the mechanism.

SignalGradeWhat the grade claims
Direct symbol importHighthat the binary references that symbol — not that the call is reachable, that the argument is attacker-controlled, or that no unseen check bounds the length
Constant-arithmetic proof at a specific call siteHigha mathematical claim: the constants are in the binary and the arithmetic is total
Call-graph co-occurrence patternLow to Mediuma structural signal, not provable

Compound findings. A composer runs after every other detector and cross-correlates by calling-function address. Two findings sharing a caller emit an additional compound.* finding one severity tier above the most severe constituent, capped at Critical, with confidence set to the maximum of the constituents. A function that calls recv() and then __memcpy_chk() with a length the chk variant proves overruns is the Heartbleed shape, and it surfaces as one named claim rather than two unrelated rows.

Two suppressions drop that escalation back to the plain maximum, each calibrated against a named binary:

SuppressionFires whenWhy, and where it was calibrated
Large callerthe calling function has 45 or more outgoing callsthe same-function-pair detectors (double-free.same-function, use-after-free.same-block, null-deref.unchecked-alloc-use) fire by construction in a method that big. configd's -[AgentController processScopedProxyChanges:] makes 457 objc_release calls and was elevating a three-detector stack to Critical with no constituent strong enough to support the claim
All-Low stackevery constituent is Low confidenceconvergence of dataflow-less heuristics is structural co-occurrence, not corroboration. FreeBSD ssh-keysign, a benign config parser, stacked a double-free heuristic, a non-literal format string, and an execv import into a High compound with no dataflow proof anywhere

A compound's strength scales with the kind of co-occurrence, not the count: a two-detector stack combining an SSA-taint flow with a chk-family proof at the same site is two independent analyses converging, while a many-detector stack on an interactive shell's main loop (popen plus system plus getenv plus unsafe-open plus a format-string rule plus a double-free rule) is breadth, not depth — each detector fires correctly on its own pattern and the function legitimately does many sensitive things.

There is no cross-detector confidence promotion. A taint finding and a static-rule finding at the same caller compose into a compound but never re-grade each other. The one place a detector rewrites another's grade runs the opposite direction: dangerous-call.realpath is demoted to Info when every realpath site in that caller provably passes NULL as the destination — the safe allocating form.

Detector signal classes

Detectors fall into eight signal classes, and the class determines what a finding can prove.

ClassReadsStrongest available claim
Importsthe import symbol tablethis binary references this symbol
Strings__cstring, __objc_methname, CFString surfacesthis byte sequence is present in the file
Call-graphthe per-function outgoing-call setA and B co-occur, or A precedes B with nothing in between
Byte-awareraw instruction bytes alongside the call graphthis constant provably reached this call site
IL+SSA dataflowthe lifted IL, reduced to SSAa typed source flows to an argument-typed sink with no dominating sanitiser
IL object modelspoints-to plus lifecycle over the SSA ILthis heap object's size, liveness, or null-ness at this use site
Binary-widethe binary's hardening surfacea build-time protection is absent
Composerthe other detectors' findingstwo independent detectors fired on one calling function

Imports — direct symbol-table match against the binary's import set. Proof-grade for "this binary references this symbol" and nothing more.

Strings — regex or token scan over __cstring, __objc_methname, and CFString surfaces. Proof-grade for presence of an embedded JWT, a %n template, a hardcoded key; not for use, since the string may be inert metadata.

Call-graph — walks the outgoing-call set per function. Set-membership asks whether a function calls both A and B (the predictable-seed and chroot-without-chdir detectors). Site-ordered asks whether it calls A before B with no intervening sanitiser (TOCTOU, double-free, use-after-free, null-deref). Site-ordered rules need a non-zero per-call-site address on every edge; where the IL bridge leaves that at the sentinel zero the detector skips the whole caller silently rather than reason from an unordered set.

Byte-aware — walks raw instruction bytes alongside the call graph. A strict register tracker resolves the constant reaching a target register at a specific call site by walking back through MOVZ and register-copy chains, aborting on any intermediate write, so a successful resolve is a guarantee that the constant reached the call. A literal resolver computes the address loaded via ADRP + ADD or ADR. Both decoders read arm64 instruction bytes; six detector modules route through a portable seam instead, and the rest are arm64 in practice — see Format and architecture coverage. Two sub-classes:

IL+SSA dataflow — forward taint over the lifted IL, reduced to SSA and propagated to a fixed point per function. Three source classes, each carrying its kind so the wrong taint cannot reach the wrong sink (an allocator return arriving at a memcpy size argument is not a CWE-1284 candidate): external input (read, recv, getenv, argv), allocator returns (malloc, calloc, realloc), and must-check-return symbols across libc, Security.framework, and IOKit. Sinks key off both the callee and the argument position:

Sink kindAt argument ofEmitsRule prefix
Quantitysize arg of memcpy / memmove / strncpy / read / writeCWE-1284taint.flow.quantity-arg
IndexedWritedestination buffer of a sized primitiveCWE-787taint.flow.dest-arg
Readsource buffer of a memcpy-class primitiveCWE-119taint.flow.src-arg
AllocSizesize arg of malloc / calloc / reallocCWE-789taint.flow.alloc-size
Derefload or store through an allocator-return pointerCWE-476taint.flow.deref
ShellArgcommand string of system / popenCWE-78taint.flow.shell
ProcArgpath or argv of the execve familyCWE-78taint.flow.exec
DlPathpath arg of dlopenCWE-94taint.flow.dlpath
FormatStringformat arg of the printf familyCWE-134taint.flow.fmt-arg
Pathpath arg of open / fopen / unlink / rename / chmod and the POSIX-2008 *at familyCWE-22taint.flow.path-arg
RegistryNamename or sub-key of RegOpenKeyEx / RegSetValueEx / RegCreateKeyExCWE-99taint.flow.registry-name
RegistryDatadata buffer of RegSetValueEx / RegSetKeyValueCWE-15taint.flow.registry-data
SsrfURL or host arg of a Win32 HTTP / Internet primitiveCWE-918taint.flow.ssrf
SqlInjectionquery-string arg of sqlite3_exec / mysql_query / PQexecCWE-89taint.flow.sql-injection
ProcessInjectionpayload, start-routine, or APC arg of WriteProcessMemory / CreateRemoteThread / NtMapViewOfSectionCWE-94taint.flow.process-injection
UseAfterFreefreed pointer reaching a use or a second freeCWE-416, CWE-415 on double-freetaint.flow.uaf
ClobberedReturn / IsolatedReturnmust-check return overwritten unread, or never comparedCWE-252taint.flow.unchecked-return

Fortified _chk sinks are demoted one severity tier, because the runtime bounds check aborts before exploitable corruption. Three sanitiser shapes are recognised by dominator analysis, and a candidate is suppressed when every member of its source set is dominated by a matching sanitiser's safe edge: ConstantBound (the source is MIN'd with a literal before the sink), MeasuredBound (the source is compared against the same length argument the sink receives), RealpathNullCheck (a realpath return is null-checked before downstream use). Confidence is High when no shape touched any source, Medium when shapes matched but none dominated, Low when the source set saturates.

Three extensions widen what that lattice can see beyond a single function's SSA graph.

Alias-aware seeding closes the recall gap the worked example above illustrates. The seed-point SSA value dies within a few operations when a reload produces a different name; the alias pass identifies every SSA value holding the same logical pointer at the seed — through stack-slot spills, register-copy chains, and slot reads — and seeds them all, bounded to the current function and its dominator-reachable predecessors, with a 16-member cap so a saturating alias set cannot flood.

Interprocedural sink-wrapper transparency closes the same gap from the sink side. The intra-procedural lattice only fires when the call target is itself a named sink, so a local wrapper — void run_cmd(char *c){ system(c); } — looks like an ordinary direct call and the flow is invisible. A single bottom-up fixpoint over the call graph's strongly-connected-component condensation computes, per function, "parameter i provably reaches a sink of class k", directly or through callees; any caller passing tainted data into such a parameter fires. The summary is a positive reachability witness only — indirect and unsummarised callees contribute no edge — so it under-approximates: it can miss a flow, but it cannot manufacture a false positive the intra-procedural baseline would not also produce. Rules are taint.flow.interproc.<sink>, keyed to the same sink classes as the lattice.

Interprocedural callee-access propagation is a distinct extension: when a function passes a parameter into a callee that dereferences or frees it, the callee's recovered access pattern reflects back into the caller's taint state, firing null-deref.callee-dereferences (CWE-476), use-after-free.callee-uses (CWE-415 / CWE-416), and taint.flow.callee-derefs.<source> (CWE-476, carrying the source kind through). It walks bottom-up over the same condensation with a fixpoint only on recursion, and it is off by default — enabling it changes the recovered argument-access signatures every reconstruct-mode detector reads, so the default path stays byte-identical to the pre-propagation behaviour until the promotion is opted into.

IL object models — points-to plus lifecycle analysis over the SSA-form IL. Where the taint lattice tracks values flowing into named sinks, the object models track heap objects keyed on their allocation-site address through mint → alive → freed states, which is why a pointer that round-trips through a stack slot is still the same object at the use site. Four detectors ride the shared state: null-deref.unchecked-object (an object minted by a may-return-NULL allocator, dereferenced, never null-checked anywhere in the function), resource.leak.unreleased-object (an owning allocation never freed on any path and never escaping by call argument, return, or global store), bounds.oob-write.object / bounds.oob-read.object (an access whose offset provably exceeds the object's size), and the object-model half of the use-after-free family. The posture is bias-to-silence: an unknown points-to target, a may-freed state, or any escape asserts nothing. Low recall traded for near-zero false positives.

Two of these carry an extra architecture gate. null-deref.unchecked-object and resource.leak.unreleased-object run only on x86_64, arm64, and arm64e. An intraprocedural pass cannot tell a may-return-NULL allocator from an abort-on-null wrapper — jq's allocator wrapper does bl malloc; cbz x0, abort, so its result is never NULL, but the check lives inside a function the pass does not enter — and there is no interprocedural allocator summary to settle it. On the three measured architectures the benign rate is zero; elsewhere it is unmeasured, and the gate is the conservative answer to that. The bounds detectors carry no such gate: they are proof-carrying, so on an architecture where the lift cannot resolve an object they stay silent rather than flood, and that is what makes the precise overflow proof available on the MIPS, PowerPC, and ARM32 firmware corpus where out-of-bounds writes are the dominant bug class.

Binary-wide — one finding per missing protection: PIE, stack canaries, FORTIFY, RELRO, executable stack, RWX segments, hardened runtime, library validation, CS_RESTRICT, dangerous entitlements, shipped debug info, a leaked dSYM path, PE ASLR / NX / CFG / GS / CET / force-integrity, ELF branch-target and pointer-authentication hardening. Keyed off the binary's hardening surface, not any call site. A posture audit, not a per-bug claim.

Composer — described above; runs last.

The CWE catalog

Seventy-two CWE ids appear across the binary detectors, and the numbers around that figure do not reconcile on their own:

CountWhat it counts
72CWE ids appearing across the binary detectors
71of those, ids with a detector that can emit them
61rows in the hand-curated registry that supplies a finding's CWE name
60of those rows that correspond to one of the 72. The 61st is CWE-1325, registered for the process-injection chain and named inside the CWE-94 summaries, but never a finding's own cwe_id
12emitted ids with no registry row — 125, 215, 319, 330, 332, 369, 401, 434, 601, 782, 829, 1395

CWE-285 is the one registered with no binary detector and the catalog's one pending entry: the shared rule catalog's CWE-285 rows are gated on a source-level "the argument is not a literal" check the binary import scan cannot enforce, so the binary path skips them by design.

CWE-829 is the expensive omission: the id the UEFI/SMM callout detector emits, the firmware finding with the highest single-finding impact in the catalog, renders with a null CWE name.

The sixteen CWEs the taint lattice can emit are all present in the name registry. Findings for the twelve missing ids render with the id, the MITRE link, and a null name. The registry's tests pin uniqueness, non-empty names, and MITRE-link format, plus a per-detector spot check; there is no exhaustive detector-to-registry parity test, which is exactly how twelve ids went missing.

FamilyCWEs
Memory safety119, 120, 121, 122, 125, 129, 401, 415, 416, 476, 787, 825
Dataflow-proven22, 1284
Cryptography and randomness295, 319, 326, 327, 330, 332, 337, 338, 347, 916, 1240
Injection and code execution78, 89, 94, 99, 134, 502
Web, server, and cloud346, 434, 601, 611, 918
Authentication and authorization285, 287, 306, 862, 863
Identity, privilege, permissions269, 273, 312, 345, 693, 732, 782, 798
Filesystem, environment, races15, 59, 243, 276, 367, 377, 426, 560, 668
Integer and allocation correctness190, 195, 197, 789
Coding hygiene, posture, crashes209, 215, 242, 252, 369, 467, 479, 676
Firmware and supply chain829, 1395

Twenty of these are in MITRE's Top-25: CWE-22, 78, 89, 94, 119, 125, 190, 269, 276, 287, 306, 416, 434, 476, 502, 787, 798, 862, 863, 918.

Sixteen of them can be reached through the SSA-taint lattice (15, 22, 78, 89, 94, 99, 119, 134, 252, 415, 416, 476, 787, 789, 918, 1284), which means their tight catches require a flow-proven source reaching an argument-typed sink rather than co-occurrence anywhere in the call graph.

Format and architecture coverage

Four format-and-architecture combinations carry a real per-call-site address on every call-graph edge:

FormatArchitecturesWhere the symbols come from
Mach-Oarm64, arm64ethe nlist table plus Objective-C metadata
Mach-Ox86_64the nlist table plus Objective-C metadata
PE32+x86_64IAT entries and bare names
ELFarm64, x86_64PLT stubs, .symtab, .dynsym

That address is what lets a finding name the specific call instruction inside its calling function — the IDE-jumpable case.

Everywhere else the lift bridge leaves the field at a sentinel zero, and 40 detector modules — every rule that resolves an argument at a call site, and every site-ordered call-graph rule — skip that call target rather than reason from an unordered set. The silence is total and unreported: the detector runs, finds nothing it can attribute, and emits nothing. This is why the CWE-476 call-graph rule was effective on Mach-O arm64 alone for as long as it was the only bridge populating the field.

Static-linked ELF binaries lose the import-keyed detectors, not the findings. With no dynamic imports, import-match rules have nothing to bind to. On the statically linked, stripped curl/linux-aarch64 fixture not a single dangerous-call.* rule fires, despite strcpy in its source. What that binary does produce is 2,629 findings, every one of them from a name-independent IL detector:

CWEFindingsDominant rules
7871,519bounds.oob-write.object 943, oob-write.loop-bounded.fixed-buffer 316
1251,032oob-read.loop-bounded.fixed-buffer 708, bounds.oob-read.object 297
36938divide-by-zero.unchecked-divisor
78933excessive-alloc.stack
6936ELF hardening posture
7981hardcoded-secret.pem.marker-only

Static linkage alone is not the blinding condition: the dangerous-calls detector walks resolved call-graph symbols rather than gating on the import table, so a statically linked binary that keeps its symbol table is still covered. Blindness needs static and stripped together. String-based detectors work throughout; closing the remainder needs string-based heuristics over the linked-in libc, which do not exist.

Twenty-three detectors skip Rust binaries outright, across twenty-four modules. Each skip trades a real class of miss for a measured flood avoided:

DetectorThe Rust shape it collides withSpurious findings on ripgrep before the skip
Taint latticethe conservative-call default marks any unmodelled callee's result tainted, and Rust binaries are dominated by unmodelled callees — every Clone::clone, every Copy, every derive-emitted helper, every monomorphisation2,326 (0 after)
sizeof()-on-a-pointerClone of a pointer-sized type compiles to mov w2, #8; bl _memcpy, exactly the shape the C sizeof(ptr) bug produces42 — 40 memcpy, 2 memmove
Divide-by-zerochecked-arithmetic helpers compile to the same division shape everywhere
Link-following, W^X, null-deref, loop-bounded read and write, array-index, signed-guard, truncation, memory-leak object modelstd::fs opens files without O_NOFOLLOW throughout, so the link-following rule fired on every Rust binary's ordinary I/O

The two bounds object models are the exception: bounds.oob-write.object and bounds.oob-read.object carry no Rust skip and fire 77 and 41 times respectively on ripgrep/linux-aarch64. They are proof-carrying — an offset provably past a sized object — rather than pattern-keyed, so the Rust idioms that flood the heuristics never reach them.

The cost of the skips is real and it compounds: a Rust binary that FFI-calls memcpy with a genuine pointer-size confusion, that reaches a C sink through unsafe, or that has a real unchecked open in its own code, is missed by all of them at once.

Apple-runtime surfaces are Mach-O-only by construction. The XPC client-identity (CWE-345), keychain at-rest protection (CWE-312), TCC entitlement mismatch (CWE-732), and DYLD-injection-tolerant (CWE-693) detectors read the Mach-O entitlement plist, Objective-C class-ref imports, and the code-signature blob. Other formats have none of those.

Byte-aware argument resolution is arm64 by default; six modules escape it. The strict register tracer and the ADRP + ADD literal resolver both decode arm64 instruction bytes, the literal resolver by construction. A portable seam reads the constant arguments the ELF and PE call-graph builders recover from the lifted IL instead, and six detector modules route through it.

RulesWhere the constant comes fromRuns on
fortified _chk proofs (CWE-787 / CWE-125), the setuid-zero rule (CWE-269), the umask rule (CWE-560), the byte-aware format-string passon the arm64 family, the original strict byte tracer unchanged; everywhere else, the lifter-recovered constant arguments the ELF and PE bridges both populateevery bridged format
CWE-190 multiply-into-malloc, CWE-467, the anti-debug shapes, the CWE-369 divisor windowa separate x86_64 register tracer (System V on Mach-O and ELF, MS x64 on PE, forward decode)arm64 and x86_64
every other call-site argument rule — CWE-59 link following, CWE-668 O_CLOEXEC, CWE-377 O_EXCL and predictable temporary paths, CWE-276 create modes, CWE-732 chmod modes, CWE-611 libxml2 options, CWE-916 PBKDF2 iteration counts, CWE-295 libcurl verify flags, CWE-330 and CWE-337 seeds, CWE-273 privilege-drop verification, CWE-789's heap arm, CWE-78's literal command re-grading, CWE-94's dlopen path split, CWE-15 and CWE-426 environment-variable names, hardcoded-credential comparisons, signal-handler resolution, jailbreak string scans, the dyld image scan, the CWE-209 os_log pass, the CWE-121 stack-frame overrun proof, CWE-326 key lengths, the CWE-825 and W^X argument resolution, PT_DENY_ATTACHthe arm64 strict tracer or the arm64 literal resolver, called directly with no fallback patharm64 only

That third row is most of the byte-aware catalog. On the dynamically linked ripgrep/linux-aarch64 fixture it produces fd-leak-on-exec.no-cloexec.open, dangerous-call.realpath, and search-path.exec-unclean.setuid-helper; on the x86-64 builds of the same programs, none of them fire.

The taint matrix is wide, but half of it needs a non-default build.

FormatArchitectures the lattice runs onReached by a default build
Mach-Oarm64, arm64e, x86_64, the PowerPC family, 32-bit ARM variantsyes
ELFarm64, x86_64, x86, MIPS, PowerPC, RISC-V, ARMyes
ELFthe embedded register-machine set — SuperH, SPARC, m68k, s390x, Xtensa, AVR, MSP430, Z80, H8/300, V850, TriCore, Hexagon, NDS32, LoongArch64no — one opt-in feature each
PEx86_64, x86yes

The analysis crate's default feature set is PE, Windows, ELF, Linux, BSD, Solaris, MIPS, ARM, eBPF, PowerPC, RISC-V, and CIL, so a default build never lifts the fourteen in that third row far enough to reach the matrix at all. Combinations outside the matrix return an empty taint vector and still get import, string, call-graph, byte-aware, and posture coverage.

The WebAssembly arm is unreachable; the Dalvik arm is pinned shut. The WASM arm admits the format without an architecture check — linear memory makes the loop-OOB, heap-overflow, and value-range passes semantically valid, and the lift resolves WASM function indices to import and export names so name-keyed sinks match. It sits behind a detection-crate feature nothing forwards: the analysis crate's own WASM feature enables the loader and the architecture tables but not the detection arm, and the engine crate has no WASM pass-through at all. No build configuration in the workspace reaches it.

Dalvik is shut twice over. The lift does resolve the register VM's constant-pool tokens back to smali method, field, and string names, which is what a name-keyed sink lookup needs — but dalvik is not one of the format-and-architecture pairs in the taint matrix, and a test pins it out explicitly, so enabling the feature would still return an empty taint vector. CIL, JVM, pyc, and luac are excluded by the same matrix and for the stated reason: their calls are token-keyed and their memory is object- and reference-based, so the out-of-bounds and pointer-taint sink classes are a category mismatch.

Per-CWE reference

Each entry states the detector's mechanism first, then the rules it emits, then — where one has been measured — its known limit.

CWE-15 — External Control of System or Configuration Setting

Byte-aware register trace at getenv and secure_getenv call sites, resolving the literal variable name and classifying it. App-custom debug or config variables — an all-uppercase identifier containing DEBUG, LOG, CONFIG, TRACE, or VERBOSE — fire at Low / Medium: the binary's behaviour is steerable through the environment, and the auditor confirms whether that surface is documented config or an undocumented back door. Standard system variables (PATH, HOME, TMPDIR, USER) are deliberately not flagged, because every Unix tool calls getenv("HOME") and emitting on them would flood the queue. Loader-hijack variables fire under CWE-426 instead.

The write side runs the same classifier over setenv / putenv / unsetenv; the loader-variable writes again land under CWE-426. The registry-data half of the Windows surface reaches this CWE through the taint lattice's RegistryData sink.

Rulesenv-var-use.app-config, env-var-set.standard-override, env-var-set.app-config.

CWE-22 — Path Traversal

The lattice's Path sink class, plus a cross-format archive-extraction shape. When an external-input source (read, recv, fread, getenv, argv) is proven to flow into the path argument of open, openat, fopen, creat, unlink, unlinkat, rename, renameat, chmod, fchmodat, chown, lchown, fchownat, mkdir, mkdirat, rmdir, symlink, symlinkat, link, linkat, truncate, access, faccessat, stat, lstat, or fstatat without a dominating sanitiser, the detector emits on it. The POSIX-2008 *at family is in scope because the path component still flows from input — the directory descriptor argument does not change the traversal exposure. The recognised sanitiser is RealpathNullCheck, the canonical canonicalisation gate.

The second detector catches the Zip-Slip shape: a libarchive, libzip, libtar, minizip, or miniz consumer that reads entry pathnames without importing a realpath canonicaliser. The entry name comes out of the archive header, so it is attacker-controlled, and it flows straight to the extraction-side open and write.

Rulestaint.flow.path-arg.<callee>; archive-extract.<lib>.no-path-validation, with an unknown variant when the library cannot be identified.

Byte-aware flag-mask gate. Fires when the flags argument of open / openat / open_nocancel sets at least one write, create, truncate, or append bit while clearing both O_NOFOLLOW (0x0100) and O_SYMLINK (0x20_0000) — a privileged write through a path an attacker can swap for a symlink. Read-only opens are deliberately not flagged: link following only matters when the code writes. CWE-61 (symbolic) and CWE-62 (hard link) are variant children of the same gate.

Ruleslink-follow.open-without-nofollow.{open,openat,open_nocancel}.

CWE-78 — OS Command Injection

Calls into system, popen, execlp, execvp, execv, and execle flag at import-match time. execve and posix_spawn are not in the import table — they reach this CWE only through the taint lattice. The byte-aware variant resolves the command string at the call site and re-grades: a literal with no shell metacharacters demotes to Info, a literal containing $, a backtick, |, <, >, &, or ; fires a shell-metacharacter rule at High confidence, a bare filename passed to an exec*p variant fires a PATH-search rule, and a non-literal argument keeps the Medium-confidence import finding so the unresolved case stands out rather than disappearing. That re-grading reads arm64 bytes directly. The flow-proven version is the lattice's ShellArg and ProcArg sinks.

Rulescommand-exec.{system,popen,execlp,execvp,execv,execle}, plus the per-call-site .literal-safe, .literal-shell-meta, .path-search, and .literal-absolute variants.

CWE-89 — SQL Injection

Import-match audit prompt over the SQL-execution surface, six rules. sqlite3_exec, sqlite3_get_table, mysql_query, mysql_real_query, and libpq's PQexec run a full SQL string rather than a parameterised statement, and fire at Medium / Medium. The detector cannot see whether the string is concatenated from attacker input, so the finding is a surface map: the binary executes dynamic SQL, audit how the string is built.

PQexecParams is the sixth rule rather than an exclusion. It binds parameters separately from the SQL text, so injection is structurally prevented unless the query template itself is attacker-built — it fires at Info severity, Medium confidence, purely as attack-surface inventory. The other safe alternatives, sqlite3_prepare_v2 and mysql_stmt_prepare, carry no rule at all. MITRE Top-25 #3.

Rulessql-exec.<callee>.

CWE-94 — Code Injection via Dynamic Loading

dlopen, dlsym, and NSClassFromString against attacker-influenced paths or names. The byte-aware variant resolves the path argument and splits the outcome: a literal under /usr/lib/ or /System/Library/ is a benign system load (dynamic-code.dlopen.system-path), a literal absolute path elsewhere, a relative path, an @rpath-relative path, or a path under a writable directory each get their own rule and escalate. A non-literal dlopen argument is the canonical load-from-an-attacker-influenced-string shape and emits at High severity. dynamic-code.macho-from-memory covers in-memory image loading.

Two remote-eval companions extend the class past dynamic loading. The Lua rule fires when luaL_loadstring and lua_pcall co-occur with a network-read source — the Redis Lua sandbox-escape shape (CVE-2022-0543). The Python rule fires on PyRun_String, PyRun_SimpleString, PyEval_EvalCode, Py_CompileString, or PyMarshal_ReadObjectFromString reachable from a network read — the data-science-platform pre-auth RCE shape — while excluding the load-from-disk variants PyRun_File and Py_CompileFile. Both demote to a local-only rule when no network source reaches the eval.

The Win32, Linux, and Mach process-injection sinks also emit CWE-94, naming CWE-1325 in the summary.

Rulesdynamic-code.dlopen*, dynamic-code.dlsym, dynamic-code.macho-from-memory, code-injection.{lua,python}-eval.{network-reachable,local-only}.

CWE-99 — Resource Injection

The lattice's RegistryName sink class: external input proven to reach the name or sub-key argument of RegOpenKeyEx, RegSetValueEx, RegCreateKeyEx, and the rest of the Win32 registry family without a dominating sanitiser. Whoever controls the key path controls which resource the privileged process opens or writes. The data-buffer side of the same calls fires under CWE-15.

Rulestaint.flow.registry-name.*.

CWE-119 — Buffer Operations Out of Bounds

The class umbrella for the variants (120, 121, 122, 125, 787). A detector emits the variant when the specific shape is identifiable; the umbrella covers a banned function whose interface admits no safe use under any caller — gets, getwd, wcscpy. On the dataflow side, the lattice's Read sink at the source argument of a memcpy-class primitive emits CWE-119 as the read-side twin of the CWE-787 destination sink.

CWE-120 — Classic Buffer Overflow

strcpy, strcat, stpcpy, sprintf, vsprintf, gets, and the scanf family — primitives with no destination-size argument — detected at import-match. The fortified twins carry the same CWE at lower severity under a .fortified rule suffix: the runtime aborts on overflow, but the source-level pattern is unchanged.

Rulesdangerous-call.<callee> and dangerous-call.<callee>.fortified.

CWE-121 — Stack-based Buffer Overflow

Two detectors, one heuristic and one proof-grade. The heuristic flags functions carrying a stack canary whose only outgoing call is the canary-failure stub — the manual-copy-loop shape the symbol-keyed rules cannot see — at Low confidence, and it is narrow on purpose: a function that mixes a manual loop with any other libc call slips through. It is also, by volume, the loudest rule in the catalog: 115 of jq's 342 Mach-O arm64 findings come from this one rule. Low confidence is doing real work there.

The proof-grade detector reads three constants out of the binary at a memcpy call site — the frame size from the prologue's sub sp, sp, #F, the destination offset from add x0, sp, #O, and the copy length from the resolved x2 — and fires when O + N > F is total. No taint, no flow, no aliasing: given the call site is reached, the overflow is unconditional. High confidence, Critical severity. It reads the arm64 prologue shape and is arm64-only today.

The overrun predicate additionally requires the destination to start inside the frame. A base at or beyond the frame top — the sp+0x3000-into-a-0xa70-frame class — is a decode artefact, not a buffer, and is rejected even though its offset plus length exceeds the frame size.

Rulesmanual-buffer-copy.canary-only-leaf, stack-overflow.proof.constant-frame-overrun.

Limits. Every fire of the proof rule on ripgrep is a false positive: 9 times on the macOS arm64 build and 12 on the Linux arm64 build, all Critical, all in Rust's panic-helper path where the compiler statically constructs error buffers. Those counts are pinned as caps rather than driven to zero, because the arithmetic is genuinely total in each case — what is wrong is the premise that a statically constructed panic buffer is a copy destination. Every other fixture in the matrix — airportd, and the Mach-O, ELF, and PE builds of curl, jq, and ripgrep-on-x86_64 — emits exactly zero.

CWE-122 — Heap-based Buffer Overflow

The dedicated detector catches the allocation-size-versus-copy-length mismatch directly over the IL — p = malloc(S); … memcpy(p, src, L) with L > S — which is the shape name-keyed taint sinks miss when neither value is attacker-tainted. The MSVC pattern of allocating with one size and copying with another motivated it. The fortified CWE-787 detector additionally proves overrun against the dst_size argument regardless of whether the destination is stack or heap, and the lattice's IndexedWrite sink covers the tainted cross-function shape.

Rulesheap-overflow.copy-exceeds-alloc.

CWE-125 — Out-of-bounds Read

Top-25 entry, sibling to CWE-787, four detectors. The fortified-proof rule fires on __memcpy_chk / __memmove_chk / __bcopy_chk where the source resolves to an embedded string literal and the byte count exceeds strlen(literal) + 1 — the runtime reads past the literal's NUL into adjacent constant-string bytes, the info-disclosure shape behind Heartbleed-class bugs. The string-family _chk variants (__strncpy_chk, __strncat_chk, __strlcpy_chk, __strlcat_chk) are excluded because they stop at NUL by definition. High / High.

Three IL-side companions extend the read side past named symbols: a loop-shaped read whose bound is attacker-tainted (the read twin of the CWE-787 loop family), the heap-object proof that a read offset exceeds the allocation, and the kernel user-copy rules where a copyout-family length exceeds the source object. The attacker-indexed read shape fires under CWE-129.

Rulesoob-read.memcpy-chk-literal-overrun, oob-read.loop-bounded.fixed-buffer, bounds.oob-read.object, oob-read.kernel-copy, oob-read.kernel-copy-exceeds-alloc.

CWE-129 — Improper Validation of Array Index

IL detector over scaled-index accesses. An attacker-controlled value that reaches a table[idx] access with no dominating bound check on any path emits at CWE-129 for the read shape and at CWE-787 for the write shape. Detection is deliberately narrow: the index must be a direct external-input source — a value merely derived from input does not seed a candidate — and any recognised comparison on the index suppresses. Precision-first until a corpus diff justifies loosening. Composes with CWE-195, which catches the bound check that exists but compares signed.

Rulesarray-index.unvalidated-attacker-index, array-index.unvalidated-attacker-index-write.

CWE-134 — Externally-Controlled Format String

The byte-aware detector resolves the format-string argument at each printf-family call site and emits per-callee rules in two shapes: .non-literal when the format argument does not resolve to a read-only literal at all, and .writable-global when it resolves to an address in a writable section. The family is wider than the POSIX core:

SurfaceCallees
POSIXprintf, fprintf, sprintf, snprintf, asprintf, dprintf, syslog, vsyslog, the v-prefixed variants, and glibc's fortified __*_chk twins
Windowssprintf_s, swprintf_s, vsprintf_s, vswprintf_s, _snprintf, _snwprintf, _snprintf_s, _snwprintf_s, _vsnprintf_s, _vsnwprintf_s, wsprintfA / wsprintfW, wvsprintfA / wvsprintfW
Linux kernelprintk, vprintk, seq_printf, dev_err, dev_warn, dev_info, dev_notice, dev_crit
BSDerr, errx, verr, verrx, warn, warnx

The flow-proven version is the lattice's FormatString sink.

Rulesformat-string.<callee>.non-literal, format-string.<callee>.writable-global.

Limits. Only the ADRP + ADD shape is resolved — the direct page-plus-offset form. ADRP + LDR through the global offset table is skipped, because the GOT slot is writable but its pointee almost always lives in a read-only section, so resolving it would need a pointee read. A binary that genuinely stores the format string itself in the GOT is missed.

CWE-190 — Integer Overflow into Allocation

The malloc(count * size) shape. The byte-aware detector reads the multiplier and multiplicand out of the call site by strict register trace; when both are constants and the product wraps in 64-bit unsigned arithmetic, it fires. High confidence. The wrap chains into CWE-787 on subsequent indexed writes into the undersized buffer. This is one of the rules carried onto x86_64 by the dedicated register tracer rather than the arm64 byte tracer.

Rulesinteger-overflow.mul-into-{malloc,calloc,realloc,reallocarray,alloc}.

CWE-195 — Signed to Unsigned Conversion Error

The signed/unsigned confusion family. A bound enforced only by a signed comparison — if (len > MAX) fail; with int len — admits every negative value, which the subsequent unsigned index or size arithmetic reinterprets as huge (-1 becomes 0xFFFF…). Two site kinds carry the check: scaled-index accesses and size/length sinks such as copyin and malloc. Structural and narrow by design — it fires only when the guard provably compares signed and the use is provably unsigned.

Rulessignedness.signed-check-unsigned-index, signedness.signed-check-unsigned-index-write, and the size-sink variants.

CWE-197 — Numeric Truncation Error

A wide length narrowed before it reaches an allocation or a copy: the u64 header field assigned into a u32 before memcpy(dst, src, n) or malloc(n), so the allocation or copy size disagrees with the value the surrounding code validated. For every resolvable call whose callee has a size or length argument, that argument's backward definition closure is checked for an implicit width-narrowing conversion. CWE-681 is the conversion-error parent.

Rulestruncation.size-narrowed-before-alloc, truncation.size-narrowed-before-copy.

CWE-209 — Information Exposure via Error Messages

String scan over __cstring and __objc_methname for stack-trace template fragments (%@\n%@, at line %d), leaked build paths (/Users/, /private/var/), and format-string pointer leaks. Also flags os_log and NSLog call sites whose format string carries %@ against a non-redacted public argument — the case where the redacted-by-default convention was deliberately bypassed. Medium confidence. This is the loudest family on a real Apple daemon: 39 of airportd's 102 findings are os_log disclosure hits, split 22 pointer-redacted, 13 public-data, 3 public-error-context, 1 public-integer.

Rulesinfo-disclosure.format-pointer-leak, info-disclosure.format-write-n, info-disclosure.os-log.{pointer-redacted,public-data-leak,public-pointer-leak,public-integer,public-error-context}.

CWE-215 — Sensitive Information in Debugging Code

Binary-wide posture across three Mach-O surfaces.

RuleFires whenWhat it means
posture.debug-info-shippeda release binary still carries a __DWARF segmentsymbol names, source paths, type information, and inline metadata leak directly
posture.symtab-unstrippedfunction-name density in the symbol table exceeds the stripped-binary norma forgotten strip; the threshold is tuned to let stripped-with-export-list pass
posture.dsym-path-leakthe constant strings contain an embedded dSYM bundle paththe fingerprint of a release binary linked against a debug archive

Mach-O only; the ELF equivalent lives in the ELF posture family.

Rulesposture.debug-info-shipped, posture.symtab-unstripped, posture.dsym-path-leak.

CWE-242 — Use of Inherently Dangerous Function

gets specifically — removed from C11 because no caller can use it safely. Import-match. CWE-676 covers the broader banned set; this is the variant for the one function with no safe use at all.

CWE-243 — chroot Jail Without Changing Working Directory

Set-membership call-graph rule. A function that calls chroot without a following chdir("/") leaves the calling thread's working directory outside the new root, and .. traversal from that directory escapes the jail. The correct idiom is chroot(jail); chdir("/") in that order, and the correct order suppresses entirely.

Ruleschroot-jail.no-chdir-or-priv-drop for chroot-only, High / Medium, because it asserts pair existence rather than ordering; chroot-jail.chdir-before-only for the reversed order, Medium / Low, because it approximates control-flow reachability over an unordered call list, which a branch carrying the post-chroot chdir defeats.

CWE-252 — Unchecked Return Value

Two detectors over the same curated must-check set: libc primitives whose return code signals failure (read, write, recv, send, fork, pthread_create), Security.framework (SecKey*, SecItem*), IOKit (IOServiceOpen, IOConnectCall*), and the kernel family. The byte-aware variant classifies the first post-call access to the return register: a clobbering write before any read fires; a check-and-branch is a clean read. The lattice variant raises the same shape over SSA with alias-aware seeding for stack-spilled return values — ClobberedReturn when the value is overwritten before being read on any path, IsolatedReturn when it never reaches a comparison or a branch. The two run independently; neither promotes the other's confidence.

Rulesunchecked-return.{libc,security-framework,iokit,kernel} and the taint.flow.unchecked-return.* parallels.

CWE-269 — Improper Privilege Management

Byte-aware register trace at setuid, seteuid, and setresuid call sites. A uid argument resolving to literal zero fires at High confidence; other constant uids surface at Medium with the resolved value carried in the finding. The setgid family is excluded pending false-positive measurement — the gid-zero shape is rarer than uid-zero, and a noisy rollout costs auditor trust that a detector cannot buy back. The SUID- and SGID-bit variants of chmod also carry CWE-269 in their summaries and fire under CWE-732.

Rulesprivilege-escalation.<callee>.zero.

CWE-273 — Improper Check for Dropped Privileges

Byte-aware register trace across setuid, seteuid, setresuid, setreuid, and setregid. Gated on the target uid resolving to a non-zero constant — a real privilege drop, not the escalation CWE-269 catches — and fires when the same caller never subsequently calls getuid, geteuid, or getresuid to confirm the kernel honoured it. The correct shape is setuid(uid); if (getuid() != uid) _exit(1);. Sibling of CWE-252 (which checks the return value) and CWE-269 (the class parent).

Rulesprivilege-drop.no-verify.<callee>.

CWE-276 — Incorrect Default Permissions

Byte-aware resolution of both the flags and the mode argument at open, openat, open_nocancel, creat, open64, and openat64. Fires when O_CREAT is set and the mode is world-writable, 0o777, or carries a SUID or SGID bit. Sibling of CWE-732, which covers the chmod and mkdir family; both can co-fire when code opens with a loose mode then tightens it, and they are describing different windows — the open-mode finding flags the default leak, the chmod finding flags a post-create mistake. MITRE Top-25 #25.

Rulesinsecure-create-mode.{open,openat,creat}.{world-writable,world-everything,setuid-bit,setgid-bit}, plus an unknown variant.

CWE-285 — Improper Authorization

No binary detector emits CWE-285 — the catalog's one registered-but-pending entry. It is registered for the kernel-side authorization-bypass shape. Userland coverage today is bounded by the CWE-862 CGI detector; kernel kauth-listener and IOKit access-control-bypass patterns are out of scope until a kernel-extension bridge lands. The shared rule catalog carries a CWE-285 setuid / setgid topic, but every one of its rows is gated on a source-level "the argument is not a literal" check the binary import scan cannot enforce, so those rows fire only in the source-C walker.

CWE-287 — Improper Authentication (JWT alg:none)

Two-way import gate. Positive: the binary imports a JWT decoder (jwt_decode, cjwt_decode). Negative: it imports no signature-verification primitive (jwt_verify, jwt_verify_signature, cjwt_verify). Decoder without verifier is the alg: none bypass — an attacker forges a token declaring no algorithm, the decoder accepts the unsigned payload as authentic identity, authentication is gone. CVE-2024-54150 (cjwt none-algorithm), CVE-2024-25638 (cjwt key confusion), CVE-2022-39227 (python-jwt), plus the router web-UI and IoT-firmware JWT-cookie tail. Medium confidence. MITRE Top-25 #13.

Rulesimproper-auth.jwt.decode-no-verify.

Limits. Proving the decoded token is used for authorization rather than for displaying claim metadata needs control-flow analysis this detector does not do, so the audit step is to confirm the payload reaches an identity or role check.

CWE-295 — Improper Certificate Validation

The Apple TLS-validation-bypass surface, across imports, strings, and call-graph plus Objective-C class signals:

These compose additively with unchecked-return.security-framework; both can fire on the same caller.

A libcurl companion walks curl_easy_setopt call sites and resolves the option enum and its value: CURLOPT_SSL_VERIFYPEER set to zero fires Critical (peer-certificate trust off — a machine-in-the-middle with any self-signed certificate succeeds); CURLOPT_SSL_VERIFYHOST set to zero fires High (hostname check off — any valid certificate for any host succeeds). This is the dominant binary-detectable certificate-validation-disable shape in consumer router firmware.

Rules — the six cert-validation.* families above, plus tls-verify.curl.{verifypeer,verifyhost}-disabled.

CWE-306 — Missing Authentication for Critical Function

Three-way import gate, and the conjunction is the whole signal — each gate alone fires on ordinary software.

GateRequiresWhat it establishes
Positivebind plus listen plus accept or accept4the binary serves network requests
Positivea child-process spawn primitive — fork, execve, posix_spawn, system, popenthe critical capability that, reachable pre-auth, is remote code execution
Negativeno authentication primitive anywhere in the imports, across a wide cross-vendor set — crypt, PBKDF2, HMAC and EVP_DigestVerify*, bcrypt, argon2, scrypt, libgcrypt, Apple Security verification, libjwt, krb5, GSS, ldap_simple_bind_s, pam_authenticateno authentication happens in-process

The negative gate is permissive on purpose: any one auth import suffices, because authentication is usually delegated, and a binary carrying any of them at least knew it might need to authenticate. All three holding is a pre-auth-RCE capability by construction. 94 Critical CWE-306 CVEs in the 2026 corpus, including pre-auth RCE in Marimo, Langflow, and SmarterMail, plus the IoT camera and container-orchestration management-API tail. MITRE Top-25 #20.

Rulesmissing-auth.network-server.spawn-no-auth.

CWE-312 — Cleartext Storage of Sensitive Information

Apple's deprecated keychain accessibility constants leave keychain items decryptable while the device is locked — and, for the non-device-limited form, even before the first unlock after reboot. Detected by direct symbol-import match against the imported global from Security.framework, gated on the binary actually calling SecItemAdd, SecItemUpdate, or SecItemCopyMatching. The data cross-reference index attributes the finding to the function that loads the GOT slot for the constant, so the report names a calling function rather than just the binary.

Ruleskeychain.accessible-always (Critical) and keychain.accessible-always-this-device-only (High).

CWE-319 — Cleartext Transmission

Catalog-driven import match: dangerous-call.SSL_CTX_set_verify flags OpenSSL's SSL_CTX_set_verify, the knob that, called with SSL_VERIFY_NONE, disables peer-certificate validation. The import scan cannot resolve the mode argument, so the call shape is surfaced for audit at High severity rather than claimed as a bypass. This rule comes from the shared TOML catalog with both the binary engine and the source-C walker declared as consumers — one entry fanning across two surfaces. The source-side siblings (plaintext ftp_*, http:// client construction) stay source-only.

Rulesdangerous-call.SSL_CTX_set_verify.

CWE-326 — Inadequate Encryption Strength

Byte-aware constant resolution at CCCrypt call sites: AES with a key length below 16 bytes (128 bits) and 3DES below 24 bytes (192 bits), both floors from NIST SP 800-131A. Arm64-only today — it is one of the remaining MOVZ-window rules.

Rulesweak-crypto.cccrypt-aes-short-key, weak-crypto.cccrypt-3des-short-key.

CWE-327 — Broken or Risky Cryptographic Algorithm

DES, 3DES, RC2, RC4, Blowfish, CAST5, MD2, MD4, MD5, and SHA-1 detected at import-match across CommonCrypto, OpenSSL's one-shot and EVP interfaces, and mbedTLS — 49 rules, so a finding names which library's binding was called, not just which algorithm. CCCrypt with the algorithm argument resolved as DES at the call site fires the proof-grade weak-crypto.cccrypt-des at High confidence; the constant-argument ladder runs signature lookup, strict trace, optimistic trace, then a MOVZ-window scan.

Rulesweak-crypto.<algorithm>.<library>[.<mode>], plus weak-crypto.cccrypt-des.

CWE-330 — Insufficiently Random Values

Two shapes, both proof-grade at the call site. predictable-seed.literal-constant fires when a seeding call's argument resolves to a literal — srand(1) produces the same stream on every run of every copy. predictable-rng.bound-zero and predictable-rng.bound-one fire on arc4random_uniform(0) and arc4random_uniform(1), whose output range contains exactly one value: the call is either dead code or a hard-coded bound where a real one was intended. High confidence for both. The low-entropy-source shape is CWE-337; the never-seeded shape is CWE-332.

Rulespredictable-seed.literal-constant, predictable-rng.bound-zero, predictable-rng.bound-one.

CWE-332 — Insufficient Entropy

Import-table heuristic. Fires when the binary imports a libc PRNG output primitive (rand, random, lrand48, mrand48, nrand48, jrand48) but no seeding primitive (srand, srandom, srand48, seed48, lcong48). With no seed call anywhere, the generator runs from its compile-time default state and emits the same byte stream on every process invocation.

Rulesunseeded-prng.no-seed-call.

CWE-337 — Predictable Seed in PRNG

Call-graph co-occurrence: a function that calls a seeding primitive (srand, srandom, srand48, seed48) and a low-entropy source (time, clock, getpid, mach_absolute_time). Rules name both halves.

Rulespredictable-seed.srand-time, predictable-seed.srandom-pid, predictable-seed.srand48-clock, and so on.

Limits. The rule checks co-occurrence, not control-flow ordering or dataflow. A function that calls time() for a log line and separately calls srand(secure_seed) still fires. Tightening this to "the time return value reaches the seed argument register" is what the lattice's return-value flow tracking is for.

CWE-338 — Cryptographically Weak PRNG

rand, random, drand48, lrand48, mrand48, nrand48, and the srand family at import-match. The correct alternatives (SecRandomCopyBytes, arc4random, getrandom, RAND_bytes) are explicitly excluded so a properly written binary does not fire.

Rulesweak-random.<callee>.

CWE-345 — Insufficient Verification of Authenticity

The macOS XPC client-identity surface. NSXPCConnection and NSXPCListener do not validate the peer's code-signing identity by default, so a privileged service exposing a listener without calling setCodeSigningRequirement: (macOS 13+) or hand-rolling validation through the audit token paired with SecCodeCopyGuestWithAttributes accepts method calls from any local process — the canonical macOS local-privilege-escalation primitive.

The detector recognises the listener role from a class-ref import of NSXPCListener plus selector stubs (setExportedInterface:, setExportedObject:, listener:shouldAcceptNewConnection:), and the client role from NSXPCConnection plus initWithMachServiceName:, initWithListenerEndpoint:, setRemoteObjectInterface:, remoteObjectProxy. Strong validators (setCodeSigningRequirement:, auditToken, valueForEntitlement:) suppress the finding entirely. The weak validator processIdentifier — spoofable by forking after connecting — downgrades severity by one tier and annotates the summary rather than suppressing, because silently absorbing pid-only validation would credit a known-broken pattern as if it worked. Per-call-site attribution names the function hosting the unvalidated listener.

Rulesxpc.listener.no-code-signing-requirement (Critical, High under weak-only) and xpc.connection.no-code-signing-requirement (High, Medium under weak-only).

CWE-346 — Origin Validation Error

Shape detector over the origin-comparison surface. Fires when a substring search (strstr, strcasestr, memmem, wcsstr) runs against an embedded trusted-origin literal — the broken check that trusted.com.attacker.io passes because the trusted host appears as a substring of the attacker's origin. The correct check is a full-string or suffix-anchored comparison. CVE-2026-20643 (WebKit Navigation API same-origin bypass) is the reference shape.

Rulesauth.origin-substring-bypass.{strstr,strcasestr,memmem,wcsstr,other}.

CWE-347 — Improper Verification of Cryptographic Signature

Three-way import gate. Fires when the binary imports both dlopen (dynamic code load) and a network-fetch primitive (curl_easy_perform, recv on an accepted socket, an NSURLSession data task) but imports no signature-verification primitive across the major crypto libraries — OpenSSL EVP_DigestVerify* / RSA_verify / ECDSA_verify / ED25519_verify, Apple SecKeyVerifySignature / SecTrustEvaluate*, GPG gpgme_op_verify. The shape is the update-mechanism machine-in-the-middle: download code or data and run it without verifying a signature on the way in. Sparkle CVE-2016-4655 is the historical example; the 2026 corpus carries 46 High-or-above CWE-347 CVEs across auto-updaters, plugin systems, package managers, and IoT firmware loaders. Distinct from CWE-345 (the broader parent) and CWE-494 (the narrower download-without-integrity variant).

Rulessignature-verify.missing.dynamic-load.

CWE-367 — Time-of-check Time-of-use

Site-ordered call-graph detector with eleven shape variants, each naming its pair. A check-family call followed by an open, create, unlink, rename, or rmdir on the same path, where the second call is reachable from the first with no intervening atomic-create gate. The ordering requires a non-zero call-site address on every edge. The temporary-file detector chains in an additional variant when a hardcoded /tmp/ path reaches a sink whose preceding check could be raced.

A separate IL detector covers the kernel double-fetch variant: within a function, every user-to-kernel copy-in is grouped by the SSA identity of its user-address argument, and the same user pointer fetched from two or more distinct call sites is the hand-audit-hard TOCTOU — a concurrent userspace thread can change the value between fetches, so the check on the first fetch does not bind the second. Purely structural over the IL, no taint, and sound toward under-reporting: a re-loaded address produces a different SSA expression and raises no candidate.

Rulestoctou.stat-then-open, toctou.lstat-then-fopen, toctou.access-then-creat, toctou.realpath-then-open, toctou.fstatat-then-openat and the rest of the eleven; predictable-tmpfile.toctou-pair; toctou.user-pointer-double-fetch.

CWE-369 — Divide By Zero

Byte-aware shape detector across arm64 UDIV / SDIV and x86_64 DIV / IDIV. For each division, the preceding 16-instruction window is scanned for a zero-check on the divisor register — CBZ / CBNZ on arm64, TEST r,r or CMP r,0 followed by a conditional jump on x86_64 — and the rule emits when none is found. The divisor filter is deliberately narrow and applies identically on both architectures: only divisors already sitting in the argument-passing window fire, x0x7 on arm64 and RDI, RSI, RDX, RCX, R8, R9 on x86_64, so internal computed divisors from hash mixing and container block-size constants do not swamp the signal. Rust binaries are skipped entirely.

Rulesdivide-by-zero.unchecked-divisor.

Limits. The window scan misses any guard that proves the divisor positive without a literal compare against zero. Two real patterns fire wrongly: cmp x1, #0x1; b.lt skip, which proves x1 >= 1 and is common in C overflow-detection helpers such as SQLite's sqlite3MulInt64; and the subs / ccmp / b.hi chain the compiler emits when it folds two range checks into one conditional compare, as in SQLite's initAvgEq. Both are correct guards. Low confidence is calibrated for exactly this — a divide-by-zero finding is a lead, not a claim.

CWE-377 — Insecure Temporary File

Three independent detectors, each catching a different way a temporary path stops being private.

DetectorFires whenWhy that is unsafe
Import-matchthe binary imports mktemp, tmpnam, or tempnam; the safe replacements mkstemp, mkdtemp, and mkostemps are explicitly excludedthe path is returned before the file exists, leaving a race window
Byte-aware openthe resolved flags argument at open, openat, creat, or fopen sets O_CREAT without O_EXCLwithout O_EXCL the kernel does not reject a pre-created path
Predictable-paththe resolved path argument at eleven call families — open, openat, fopen, freopen, creat, unlink, remove, rename, stat, lstat, access — is a literal under /tmp/, /var/tmp/, or /private/tmp/ with no XXXXXX randomness markera fixed path another process can pre-create or symlink-attack

Both byte-aware variants fire at High confidence when the strict tracer resolves the constant.

Rulesunsafe-open.create-without-excl.<callee>, predictable-tmpfile.<callee>.static-path.

CWE-401 — Missing Release of Memory

The heap-object model's total-leak query. Fires when an allocation minted by an owning allocator is provably never freed on any path — the whole-function lifecycle meet stays alive — and provably never escapes the function by any route: call argument, return value, global store, or store through an unresolved pointer. The object is keyed on its allocation-site address, so the leak is recognised even when the pointer only ever lives in a stack slot. Low severity (resource exhaustion, not memory unsafety), Medium confidence. Runs on x86_64, arm64, and arm64e only.

Rulesresource.leak.unreleased-object.

Limits. The error-path leak — freed on some paths but not all — is a deliberate deferral: it is the false-positive-prone case that needs path-sensitive return analysis. Any escape excludes the object, because ownership may pass to code the intraprocedural pass cannot see. Low recall, near-zero false positives.

CWE-415 — Double Free

Site-ordered call-graph heuristic: a function that frees the same register or stack-slot value twice with no intervening pointer assignment. Distinguished from CWE-416 by the second operation being a free rather than a use. The lattice's UseAfterFree sink catches the cross-block and aliased shape, emitting at CWE-415 when the second-side callee is itself a deallocator.

Two suppressions carry almost all of the precision. Adjacent free; free; free — struct-field teardown — is excluded, so the rule fires only when non-trivial work separates the two frees. And the malloc-rich shape (three or more allocator calls with exactly one free) is suppressed, because that is buffer-pool and dynamic-array growth code where the free targets a scratch slot while the following allocation operates on a still-live buffer. Before the curation the uncurated "any subsequent call after a free" rule produced 272 hits across four Mach-O arm64 binaries, on safe rebind and cleanup chains: free-then-malloc, free-then-curl_slist_free_all, free-then-fileno, free-then-strdup. Current measurement on the same four: airportd 0, curl 15, jq 16, ripgrep 0, plus one flow-proven taint.flow.uaf.double-free on curl.

Rulesdouble-free.same-function, taint.flow.uaf.double-free.

Limits. The cost of those suppressions is stated plainly: a genuine double-free that happens to be adjacent, or that sits inside a malloc-rich function, will not fire.

CWE-416 — Use After Free

A layered family. The call-graph heuristic flags a free followed by a use of the same value as a pointer argument to another libc primitive (use-after-free.same-block, Low confidence, broad recall). The Objective-C variant catches the ARC-elided pattern — a release followed by a message send on the same receiver in the same block (use-after-free.objc-release). The lattice's UseAfterFree sink raises the tighter dataflow claim: the freed pointer seeds the lattice, alias-aware seeding propagates it across spills and register copies, and a later use as a pointer argument to a memcpy-class, strcpy-class, objc_msgSend*, or Itanium C++ member call emits taint.flow.uaf.<callee>.

Five further IL rules cover shapes the intra-function lattice cannot see: taint.flow.uaf.object (the heap-object variant, keyed on the allocation site rather than an SSA value, which is what catches the stack-reload and global-escape cases); the interprocedural and wrapper-free variants, which make a local free-wrapper transparent by reflecting the callee's free into the caller; taint.flow.uaf.dangling-global, the cross-entry kernel shape where a pointer is freed in one entry path and used in another, communicating only through a shared global or long-lived struct field; taint.flow.uaf.use-after-realloc, which treats a realloc return as invalidating the old pointer; and taint.flow.uaf.refcount-release, which flags a dereference after a refcount release — a possible free, calibrated accordingly.

Rulesuse-after-free.same-block, use-after-free.objc-release, taint.flow.uaf.<callee>, taint.flow.uaf.object, taint.flow.uaf.dangling-global, taint.flow.uaf.use-after-realloc, taint.flow.uaf.refcount-release.

Limits. Objective-C's objc_release is deliberately not in the deallocator set for the syntactic rule. Of 78 airportd hits when it was, 71 were a release of A followed by a message to B where B was not A — the ordinary safe ARC pattern. Receiver-alias tracking in the lattice is what makes ObjC support possible without that noise.

CWE-426 — Untrusted Search Path

Two detectors. The search-path detector reads the Mach-O load commands: LC_RPATH entries pointing at writable or attacker-influenced directories, the install names a dylib publishes, and a setuid helper executing with an inherited, unsanitised PATH.

The environment detector fires when a binary itself calls getenv on a loader variable (DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, LD_PRELOAD, LD_LIBRARY_PATH, the DYLD_FRAMEWORK_* family). The dynamic loader reads those before the program starts, so a binary that checks them itself is making runtime decisions from attacker-influenceable state — High confidence, Medium severity, promoted by the composer when paired with a privileged-execution posture finding. The write direction is strictly worse: setting a loader variable configures the loader for the process's own children. unsetenv on the same variables is the opposite, defensive act, recorded rather than flagged.

Rulessearch-path.rpath.{writable,unsafe-relative,absolute}, search-path.install-name.{writable,relative,absolute}, search-path.exec-unclean.setuid-helper, env-var-use.loader-hijack, env-var-set.loader-injection, env-var-set.loader-scrub.

CWE-434 — Unrestricted Upload of File with Dangerous Type

Site-ordered shape. Fires when a file written from a network or request source is followed by a chmod that sets an execute bit on the same path — the web-shell-drop primitive. file-upload.write-chmod-exec is the stronger claim; file-upload.write-chmod-no-exec flags a writable upload sink for the auditor. Co-fires CWE-862 when the handler has no preceding authorization check. MITRE Top-25 #10.

Rulesfile-upload.write-chmod-exec, file-upload.write-chmod-no-exec.

CWE-467 — sizeof() on a Pointer Type

Byte-aware shape detection across nine sized primitives — memcpy, memmove, memcmp, strncpy, strncat, strncmp, wmemcpy, wmemmove, wmemcmp — plus fortified twins for four of them: __memcpy_chk, __memmove_chk, __strncpy_chk, __strncat_chk. Fires when the size argument resolves to a literal 4 or 8 — pointer width on 32-bit and 64-bit targets.

memset and bzero are not in the table, so the textbook form — memset(p, 0, sizeof(p)) where the author meant sizeof(*p) — does not fire. What fires is the copy-and-compare form of the same confusion: memcpy(dst, src, sizeof(ptr)).

Rulessizeof-pointer.<callee> and sizeof-pointer.<callee>.fortified.

Limits. The detector cannot tell whether the literal 4 or 8 is a pointer size or a string length. JSON keyword recognition and four-byte magic comparisons — "true", "this", "\xCA\xFE\xBA\xBE" — fire sizeof-pointer.strncmp legitimately, and that is exactly what the single hit on curl and the single hit on jq are. Ripgrep produces zero, which is the Rust skip working rather than the detector being right. A Low-confidence finding here means: look at the call, and decide whether the literal is a pointer width or a string length.

CWE-476 — NULL Pointer Dereference

Three detectors, and the split between them is instructive. The site-ordered call-graph rule walks every function for a may-return-NULL call (malloc, calloc, realloc, strdup, fopen, fdopen, popen, opendir, getenv, dlopen) immediately followed by a use of its return value as a pointer argument before any branch on null. Medium confidence — but it needs real per-call-site addresses, and where the IL bridge leaves those at the sentinel zero the detector skips the caller entirely, which historically made it Mach-O-arm64-effective only. On the Mach-O arm64 fixtures where it does run it produces 6 findings on curl and 11 on jq.

The object-model detector closes that gap: it tracks the heap object from a may-return-NULL allocation site through spills and reloads and fires at any load or store whose address provably resolves to a live object the function never null-checks on any path. Medium confidence, bias-to-silence — an unknown points-to target asserts nothing. Enabling it on arm64 initially surfaced 67 jq and 2 curl false positives, all with a single root cause: the points-to query resolved a reused register base to the freshly minted allocation, because a cell that last held a strdup result was read without regard to which SSA version of the register the store used. Switching the query to a version-aware form took jq, curl, and ripgrep on arm64 to zero while the positive fixture still fired.

The lattice's Deref sink raises the flow claim: allocator-return taint reaching a load of the tainted register with no dominating null-check. The interprocedural extension adds the case where the caller hands the pointer to a callee that dereferences it.

Rulesnull-deref.unchecked-alloc-use, null-deref.unchecked-object, taint.flow.deref.load, taint.flow.deref.store, null-deref.callee-dereferences.

CWE-479 — Signal Handler Use of a Non-reentrant Function

Resolves the handler argument at signal, bsd_signal, and sysv_signal call sites to a code address, walks that handler's forward call list, and fires when any callee is in the POSIX non-async-signal-safe set — around forty rules, one per callee, covering malloc, free, printf, fopen, exit, syslog, strdup, ctime, asctime, the stdio family, and the environment functions. Calling a non-reentrant function from a handler is the classic re-entrancy, deadlock, and heap-corruption bug, and an exploitation primitive when the handler interrupts the allocator. A direct sub-pattern of CWE-362.

Rulessignal-handler.non-async-signal-safe.<callee>, with a generic fallback.

CWE-502 — Deserialization of Untrusted Data

Three deprecated NSKeyedUnarchiver selectors — unarchiveObjectWithData:, unarchiveObjectWithFile:, unarchiveTopLevelObjectWithData:error: — plus the older NSUnarchiver equivalent. Apple deprecated all of them in macOS 10.13 / iOS 11 because they cannot enforce secure coding. The detector recognises both modern Xcode's per-selector message-send stub imports (Tier 1, High confidence) and older toolchains' bare-selector dispatch through generic objc_msgSend paired with the class-ref import (Tier 2, Medium confidence). Self-implemented methods with the same names are filtered out, and the secure replacements (unarchivedObjectOfClass[es]:fromData:error:) are explicitly excluded.

Rulesdeserialization.nskeyedunarchiver.*, deserialization.nsunarchiver.*.

CWE-560 — umask() with a chmod-style Argument

Byte-aware register trace at umask call sites. A resolved constant sitting in the chmod-permission space — above 0o177 and not exactly 0o777 — means the author almost certainly confused the umask convention (bits to clear) with chmod's (bits to set). The common confusions get their own rules.

Rulesumask.chmod-600, umask.chmod-644, umask.chmod-666, umask.chmod-755, with umask.chmod-style for the rest.

CWE-601 — Open Redirect

String-shape detector over HTTP redirect construction. Fires when a Location: header is built from a format template interpolating a runtime value — the unvalidated redirect target that phishing and OAuth-token-theft chains rely on. A surface map, not a claim: the binary builds redirect targets dynamically, audit whether the destination is checked against an allow-list.

Rulesopen-redirect.location-format-template.

CWE-611 — XML External Entity

Per-call strict register trace of the options argument to libxml2's xmlReadMemory, xmlReadFile, xmlReadDoc, xmlReadFd, and the xmlCtxtRead* variants. Severity reads off the bit combination of XML_PARSE_NOENT (entity expansion), XML_PARSE_DTDLOAD (external DTD), and XML_PARSE_NONET (network block):

Resolved optionsSeverityExposure
entity expansion, networking allowedCriticalfull XXE, including server-side request forgery and arbitrary file read
entity expansion, networking blockedHighlocal file read
external DTD, networking allowedHighDTD-driven exfiltration

libxml2 is the dominant cross-platform XML parser — Apple's SDK, every Linux distribution, most embedded firmware. CVE-2024-25062 and the SOAP / SAML / XSLT tail are the reference cases.

Rulesxxe.libxml2.{noent-with-network,noent-local-only,dtdload-with-network}.

CWE-668 — Exposure of Resource to Wrong Sphere

Gated on the binary importing an exec-family primitive, every open, openat, and open_nocancel call site is checked for the O_CLOEXEC bit (0x0100_0000 on Darwin) in its resolved flags. A missing O_CLOEXEC means the descriptor is inherited by any spawned child — an authenticated socket, a private file, or a privacy-gated resource opened by a privileged daemon and handed to an unprivileged child.

Rulesfd-leak-on-exec.no-cloexec.{open,openat,open_nocancel}.

CWE-676 — Use of Potentially Dangerous Function

The banned-function umbrella: alloca, _alloca, getwd, wcscpy, wcscat, wcsncpy, wcsncat, and relatives — primitives banned wholesale because their interface admits no safe use under caller invariants the compiler cannot see. Distinct from CWE-242, which is gets alone.

CWE-693 — Protection Mechanism Failure

Four surfaces. The hardening posture detector emits one finding per missing build-time mitigation, in three format-specific families:

FormatPosture rules
Mach-Oposture.no-pie, posture.no-stack-canaries, posture.no-fortify, posture.no-hardened-runtime, posture.no-library-validation, posture.no-restrict, posture.no-heap-nx, posture.rwx-segment, posture.text-writable, posture.allow-stack-exec, posture.unsigned, posture.adhoc-signed, plus the dangerous-entitlement set — get-task-allow, disable-library-validation, allow-unsigned-executable-memory, disable-executable-page-protection, allow-dyld-environment-variables, allow-jit
ELFposture.elf.no-pie, no-relro, partial-relro, no-stack-canaries, no-fortify, exec-stack, no-gnu-stack, no-cet-ibt, no-cet-shadow-stack, no-arm64-bti, no-arm64-pac-returns
PEposture.pe.no-aslr, no-nx, no-cfg, no-gs-cookie, no-high-entropy-va, no-cet-shadow-stack, no-force-integrity, unsigned, signature-invalid

The DYLD-injection-tolerant compound fires when a Developer-ID-signed binary lacks all three of hardened runtime, library validation, and CS_RESTRICT. Each signal alone is sometimes acceptable — a small internal CLI tool; the combination on a Developer-ID-signed binary that ships to users is the canonical macOS local-privilege-escalation surface. Apple-signed and unsigned or ad-hoc binaries are excluded, because the loader policy differs and the per-mitigation findings already cover them.

The W^X detector resolves the protection-flags argument at mmap, mprotect, vm_protect, and mach_vm_protect and fires when the resolved literal carries both write and execute ((prot & 6) == 6). Distinct from the load-time posture findings — this catches the runtime establishment of a writable-and-executable page. Legitimate JIT engines exist; the auditor decides whether the binary's threat model warrants it. High / High.

The anti-debug detector covers a wide primitive set with per-shape rules, one per platform family:

PlatformProbes detected
macOSanti-debug.ptrace.deny-attach (the macOS-specific request value 31, the fingerprint of jailbreak detection, anti-tamper, and anti-RE machinery) and anti-debug.ptrace.self-trace; two sysctl companions, one keyed on the (CTL_KERN, KERN_PROC) argument-vector fingerprint and one on the post-call trace-bit mask test that proves the probe's result is actually read; Mach exception-port and task-port probes; csops status queries; core-dump suppression via setrlimit; the orphan check via getppid
LinuxPTRACE_TRACEME, /proc/self/status reads, tracer-pid probes, prctl dumpable suppression
WindowsIsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess, NtSetInformationThread hide-from-debugger — each with a .branch-on-return variant that fires only when the result is actually branched on

Defensive use is legitimate; the finding records the site for the auditor to weigh.

The jailbreak-indicator rule scans constant strings and Objective-C method names against a curated set of iOS jailbreak artefacts — Cydia, Sileo, MobileSubstrate, checkra1n, unc0ver, Taurine, RootHide, Dopamine. A binary embedding these is almost always running an in-app jailbreak check (legitimate banking or DRM defence) or, less often, fingerprinting its host. Medium / Medium, and the matched indicator is surfaced so the auditor can tell which.

Rules — the posture families above, dyld.injection-tolerant, wx-protection.{mmap,mprotect,vm-protect}.write-exec, the anti-debug.* set, anti-debug.jailbreak-check.

CWE-732 — Incorrect Permission Assignment

Two surfaces. The byte-aware permission detector resolves the mode argument at chmod, fchmod, fchmodat, mkdir, and mkdirat and classifies it in priority order:

PriorityResolved modeSeverity
1SUID bitCritical
2SGID bitHigh
30o777Critical
4world-writablenext in the ladder

The SUID case earns Critical because a non-root caller that chmods a file to SUID-root creates a persistent escalation primitive — the attacker re-creates the file with their own payload after the chmod.

The TCC entitlement-versus-API mismatch detector fires on sandboxed apps that import privacy-sensitive class refs — AVCaptureDevice, CNContactStore, EKEventStore, CLLocationManager, CBCentralManager — without the matching entitlement. The reverse direction, an entitlement claimed with no corresponding API use, emits a lower-severity finding across camera, microphone, contacts, calendars, reminders, photos, location, Bluetooth, accessibility, screen capture, Apple Events, and the documents folder. Per-call-site attribution names the function that loads the class-ref GOT slot. A third rule fires when a non-Apple binary writes directly to the TCC privacy store — the bypass primitive that grants itself privacy permissions with no consent prompt.

Rulesinsecure-permissions.<callee>.<shape>, tcc.sandbox.<resource>-no-entitlement, tcc.<resource>-claimed-no-api-use, tcc.db.direct-write.

Limits. The standard create-the-user-dotdir-if-missing idiom passes mkdir(path, 0777) and relies on the process umask to clamp the on-disk mode to 0o755. The detector reads the mode literal but does not check whether the calling thread reset its umask beforehand, so it fires regardless. Scanning for a umask(0) predecessor in the same function would suppress it; today the auditor confirms by hand.

CWE-782 — Exposed IOCTL with Insufficient Access Control

Import-match audit prompt over the kernel-IPC surface. ioctl opens a device-control surface whose access control lives in the kernel-side driver, and the detector cannot see what gate the corresponding IOUserClient::externalMethod or character-device callback enforces — so the finding is calibrated as a surface map, not a vulnerability claim, at Info severity and Medium confidence. IOServiceOpen opens a user-client RPC connection to a kernel IOKit driver; the audit step is to confirm the driver enforces entitlement, sandbox, or Mach-port checks on the connection request and on each external-method dispatch. The IOConnectCallMethod family is deliberately not in this table: those symbols already fire under unchecked-return.iokit with a tighter claim, and double-firing on every user-client RPC site would dilute both.

Rulesdangerous-call.ioctl, dangerous-call.iokit-userclient.open.

CWE-787 — Out-of-bounds Write

Top-25 #1. Two proof-grade rules over the fortified libc family, three name-independent IL rules, and the dataflow sink.

oob-write.memcpy-chk-proven fires on the four-argument fortified family (__memcpy_chk, __memmove_chk, __bcopy_chk, __strncpy_chk, __strncat_chk, __strlcpy_chk, __strlcat_chk) when both the byte count and the destination size resolve to compile-time constants and the count exceeds the size. The fortified runtime aborts on that path, so given the call site is reached, the source-level copy is unconditionally out of bounds. The compiler emitted the _chk variant precisely because it could not prove safety; the detector inverts the direction of that proof.

oob-write.strcpy-chk-literal-overflow fires on the three-argument family (__strcpy_chk, __strcat_chk) when the source resolves to a string literal and strlen(literal) + 1 exceeds the destination size.

Both are High / Critical and resolve their arguments through the dual-dispatch seam, so they are no longer arm64-only.

Two IL detectors cover the writes that never touch a libc symbol. oob-write.loop-bounded.fixed-buffer keys on the IL shape of a write inside a natural loop — a strided indexed store, or a pointer-walk store the loop advances, which is the inlined rep movs or SSE form — with taint reaching the loop bound. That is the media- and codec-parser kernel-RCE shape (CVE-2022-22675 in AppleAVD's parseHRD, and the CoreAudio and SMB header-supplied-count families), invisible to any symbol-keyed sink because the copy is inlined or buried in a stripped helper. bounds.oob-write.object is the heap-object variant — the rule that proves the b[argc & 0x1f] store above lands 23 bytes past an 8-byte allocation.

The lattice's IndexedWrite sink emits when external input reaches the destination of a memcpy-class primitive or a sized strncpy or read without a dominating sanitiser; the parallel Read sink at the source argument emits CWE-119. The attacker-indexed write shape fires from CWE-129.

Rulesoob-write.memcpy-chk-proven, oob-write.strcpy-chk-literal-overflow, oob-write.loop-bounded.fixed-buffer, bounds.oob-write.object, taint.flow.dest-arg.<callee>.

CWE-789 — Memory Allocation with Excessive Size

Three rule shapes plus the dataflow sink. excessive-alloc.heap fires at allocator call sites whose size argument resolves to a compile-time constant above 1,000,000 bytes. excessive-alloc.heap-range fires on the same surface when the size is not a literal but the interval domain bounds it above the same figure. excessive-alloc.stack fires at function prologues allocating more than 7,500 bytes, read off the arm64 sub sp, sp, #F shape. Both thresholds are taken from cwe_checker's shipped defaults so cross-tool comparison is direct — 7,500 sits between PATH_MAX-class buffers around 4 KB and page-class allocations around 8 KB. The lattice's AllocSize sink covers the dynamic case the constant thresholds cannot see: attacker-controlled input reaching an allocator's size argument, the buffer-sized-from-an-attacker-length-prefix shape.

Rulesexcessive-alloc.heap, excessive-alloc.heap-range, excessive-alloc.stack, taint.flow.alloc-size.

CWE-798 — Hard-coded Credentials

Two detectors. The string-scan variant flags shaped tokens with per-provider rules — AWS access key IDs and secret keys, GitHub and GitLab tokens, Slack, Stripe live keys, Discord bot tokens, Twilio, SendGrid, Heroku, npm, Google, OpenAI, Anthropic, JWT signatures, PEM private-key bodies, URL-embedded basic auth, and JDBC or Azure connection strings — at Medium confidence.

The MII prefix is what separates a real PEM key from a parser template. PEM keys are stored with embedded newlines and the strings extractor splits on them, so a -----BEGIN marker alone cannot distinguish the two; PKCS#1 and PKCS#8 RSA private keys always start with MII, so its presence somewhere in the strings means a real key and its absence means a template.

The byte-aware variant fires on strcmp, memcmp, and strncmp call sites where one argument resolves to a literal credential and the other is a runtime value — the compare-attacker-token-against-embedded-secret shape.

Ruleshardcoded-secret.pem.private-key, hardcoded-secret.pem.marker-only (Info), hardcoded-credential.use.{known-prefix,structural,uuid}.

CWE-825 — Expired Pointer Dereference / NULL-page Mapping

The W^X detector's second shape. When mmap is called with a NULL address, MAP_FIXED set, and a non-zero length, the mapping silently claims the NULL page — usually the first sign of a process that intends to make NULL dereferences executable. Resolved by strict register trace of the address and flags arguments. Critical / High. The same call site often co-fires the CWE-693 W^X finding when the protection is also write-plus-execute, and the composer then emits a single stack at elevated severity. Arm64-only today.

Ruleswx-protection.mmap.null-page-fixed, wx-protection.mmap.fixed-address for the non-NULL fixed case.

CWE-829 — Inclusion of Functionality from Untrusted Control Sphere (SMM callout)

The UEFI/SMM firmware detector, built on a dedicated platform model. An SMI handler runs at ring −2 inside SMRAM; if it — or anything reachable from it — calls through a Boot Services or Runtime Services pointer, the target code lives outside SMRAM, and an attacker who reclaims that memory after ExitBootServices executes code in System Management Mode. Detection is reachability plus call-target classification, not taint: SMI handler roots are discovered, the intra-binary call graph is walked from each root, and every reachable call is classified for its SMRAM side against the recovered services-table globals. Conservative throughout — an unresolvable call is unknown and never flagged. See VulHunt vs openbinary for the comparison against the firmware-specialist tools.

Rulesefi.smm-callout.

CWE-862 — Missing Authorization

Two modules. missing-auth.cgi-without-auth-verifier flags a CGI handler that executes a shell command with no preceding authentication-verifier call — the firmware-router cluster. missing-auth.dbus.no-polkit flags a Linux D-Bus interface registered with no PolicyKit authorization-check primitive imported, which is the shape behind CVE-2021-4034 (PwnKit, a KEV entry) and CVE-2021-3560, plus the systemd, NetworkManager, udisks2, and bluez method-handler tail. MITRE Top-25 #11.

Rulesmissing-auth.cgi-without-auth-verifier, missing-auth.dbus.no-polkit.

CWE-863 — Incorrect Authorization

A C-API XPC server (xpc_main, xpc_connection_create_mach_service) with no peer-validation primitive in its imports. The C-XPC sibling of the CWE-345 detector: where 345 covers the Objective-C NSXPCConnection and NSXPCListener surface, this covers the lower-level libxpc C API. CVE-2024-44131 (FileProvider TCC bypass) and the bundle-ID-spoofing privilege-escalation tail are the reference cases. MITRE Top-25 #24.

Rulesxpc.c-api.no-audit-token-validation.

CWE-916 — Password Hash With Insufficient Computational Effort

Resolves the iteration-count argument at PBKDF2 derivation sites by strict register trace, across Apple's CCKeyDerivationPBKDF and OpenSSL's PKCS5_PBKDF2_HMAC and _SHA1. Three tiers:

IterationsSeverityBasis
under 1,000Criticaltrivially brute-forceable — hours per password on one GPU
under 10,000Highbelow the NIST SP 800-132 floor
under 100,000Mediumbelow the OWASP 2023 baseline of 600,000 for SHA-256 — roughly months per password on the same hardware

Rulesweak-pbkdf.{cccrypt,openssl}.iter-{critical,high,medium}.

CWE-918 — Server-Side Request Forgery

A binary-wide pre-gate (any URL-fetch primitive imported) plus a string scan for hardcoded cloud instance-metadata endpoints: 169.254.169.254 (AWS IMDSv1 and Azure), metadata.google.internal, 100.100.100.200 (Alibaba), 169.254.170.2 (AWS Fargate task metadata). These literals do not appear in normal application code — they signal either IMDSv1 deprecation lag (the Capital One / CVE-2019-15376 class) or SSRF-exploitation tooling. One finding per string, Critical on the credentials path and High otherwise. The lattice's Ssrf sink covers the flow-proven Win32 case. MITRE Top-25 #19.

Rulesssrf.cloud-metadata.<provider>, with a -creds suffix on credential paths.

CWE-1240 — Cryptographic Primitive with a Risky Implementation

Algorithm-correct, mode-incorrect crypto. Fires when a CCCrypt call resolves its algorithm argument as AES and its options argument with the ECB bit (0x2) set. ECB leaks plaintext block patterns straight through the ciphertext — the Tux-image attack — and the fix is CBC or GCM. High confidence when both arguments resolve at the call site, High severity.

Rulesweak-crypto.aes-ecb-mode.

CWE-1284 — Improper Validation of Specified Quantity in Input

The lattice's tightest claim. When an external-input source is proven to flow specifically into a Quantity sink — the size argument of memcpy, memmove, bcopy, strncpy, strncat, strlcpy, strlcat, read, or write — with no dominating sanitiser, the detector emits at CWE-1284 rather than the looser CWE-120 a co-occurrence rule would tag. All three sanitiser shapes apply, and a candidate is suppressed when every member of its source set is dominated by a safe edge. The recv-then-snprintf-then-system and recv-then-memcpy chains behind the 2023 Zyxel pre-auth RCE family (CVE-2023-28771, CVE-2023-33009, CVE-2023-33010) are tracked end to end through this sink and the shell sink together.

Rulestaint.flow.quantity-arg.<callee>.

CWE-1395 — Dependency on Vulnerable Third-Party Component

The software-composition edge of the findings surface. The fingerprint layer recovers a library name and version from the binary itself — sonames, version banners — and this detector matches each pair against 30 curated vulnerable or end-of-life version floors across 21 components common in firmware: seven OpenSSL era rules from pre-1.0.0 end-of-life through Heartbleed to the 1.1.1 EOL, plus libcrypto, busybox, dnsmasq, dropbear, expat, glibc, musl, uClibc, bionic, gnutls, hostapd, libblkid, libssh, libssh2, libxml2, the Linux kernel, PHP, samba, wpa_supplicant, and the two xz backdoor releases. Each match names the canonical CVEs. Deliberately conservative: every rule pins the vulnerable range's upper bound, and a version that does not parse does not fire. Distinct from the firmware-tree SBOM surface (CVEs & SBOM) — this fires on the analysed binary's own linked components, on the same findings stream as every other rule.

Rulesvulnerable-component.<name>.<era>.

Quality gates

The fixture corpus. Recall and precision are measured on one corpus: 63 compiled fixtures — 33 positive and 30 negative — shipping as contrastive twins, a vulnerable variant and a -safe twin, each pinning the rule family it expects. A miss on a positive reds the gate; a fire on a -safe twin reds it the same way. A separate 362-case, language-tagged source corpus drives the source-SAST gates over the same shared rule catalog.

Scoring runs by CWE id rather than by rule id, because a binary finding carries an address, not a source line, and because a curated family accept-set (121 covers 787 covers 120; 416 covers 415; 78 and 94 are interchangeable; 129 and 787 are the read and write faces of one shape) stops a correct sibling-CWE finding from scoring as a miss. A clean fixture counts as a false positive only when a High-confidence finding fires; Medium or Low on a clean fixture is tracked debt, not a red.

Run over the current tree, all 63 fixtures score and eighteen distinct CWEs have at least one fixture that actually fires:

cwe-bench — 63 scored, 0 skipped (of 63 cases)
     CWE   pos  neg    recall   fp_rate
      22     1    1      100%        0%
      78     6    3      100%        0%
     ...
     787     9    9       89%       22%
     918     1    1      100%        0%

gate: 0 regression(s), 3 tracked
  · known gap   CWE-787  cwe-787-global-oob-macho
  · known FP    CWE-787  cwe-787-copy-safe
  · known FP    CWE-787  cwe-787-strcpy-safe

Seventeen of the eighteen sit at 100% recall and a 0% false-positive rate on their own fixtures. The eighteenth is CWE-787 — the class of the heap out-of-bounds write above, the class with by far the most fixtures, and the only one carrying real error in both directions: one positive it misses, two -safe twins it fires on. Every fixture in the corpus is scored, so those three are visible rather than absent; the gate passes because each is tagged, and a tag means the failure is accepted, not fixed.

That is eighteen CWEs with a firing fixture out of the seventy-two the detectors can emit. The other fifty-four have no contrastive twin in this corpus. Some are pinned another way — the per-binary caps hold several of them against real reference binaries — but a cap proves a count did not move, not that the detector catches a bug and stays quiet on the safe version of it.

The coverage script. A coverage script keeps that arithmetic visible by rendering three deliberately unreconciled columns. Each column has a measured blind spot:

ColumnSourcePrints todayBlind spot
claimeddetector source scraped with a literal cwe_id: <N> regex68 CWEs across 91 modulesmisses four of the 72: CWE-319 reaches the binary engine only through a shared TOML catalog row the scrape never reads; CWE-285 has no detector; CWE-129 and CWE-825 are assigned through a computed variable — the array-index detector picks 129 or 787 by read-versus-write, the mmap rule picks 825 or 693 by NULL-page-versus-W^X — which a literal regex cannot see
targetthe curated set of CWEs judged meaningfully detectable on a compiled binary57 CWEs20 claimed ids sit outside it, so "extra" is the normal state rather than an error
provenwhether a fixture actually fires0 CWEsit does not read the cwe-bench result, so it under-reports all 18 CWEs that have a firing fixture

CWE-319 is therefore printed as an undetected gap — a target with no detector — while the catalog routes SSL_CTX_set_verify to the binary engine and a finding for it renders with no CWE name, because 319 is also one of the twelve ids missing from the name registry. One instrument miscounts, another renders blank, and the detector works.

Tagged failures. A positive fixture tagged as an accepted gap that stays inert is reported as tracked debt instead of failing the run; a negative tagged as an accepted false positive that fires is treated the same way. Neither tag asserts anything about the other direction: a tagged gap that starts firing scores as an ordinary true positive and passes silently, so a loosening that fires the right CWE for the wrong reason is not caught here.

Nothing in the binary harness can express the check that would catch it. A case carries only its name, CWE, polarity, architecture, expected detector, and the two tags — no field for a rule id that must not fire. The only zero-findings assertion anywhere is the source-side probe harness's ok: marker, which pins zero findings within a source line span across the source corpus; it has no binary equivalent, because a binary finding carries an address, not a line.

Per-binary regression caps. Every release runs the detector matrix across a fixed set of real binaries — 16 fixture paths for the stack-overflow proof alone, spanning Mach-O arm64 and x86_64, ELF arm64 and x86_64, and PE x86_64 builds of the same four programs, of which 15 are present today — and holds each rule family under a per-binary ceiling calibrated as the measured baseline plus headroom. Whole-binary totals on the four Mach-O arm64 fixtures, measured on the current tree:

airportd  102 findings   taint family   5  (cap 7)
curl      153            taint family  62  (cap 64)
jq        342            taint family 108  (cap 119)
ripgrep    25            taint family   0  (pinned 0)

Per-rule caps sit alongside the aggregate so a loosening cannot hide behind an offsetting drop elsewhere — path traversal alone carries airportd 2, curl 22, jq 2, ripgrep 0. Each cap constant carries the date and the reason for its last move, so a re-pin is an argument rather than a number bump.

The ripgrep zeros are not precision. Ripgrep is a Rust binary and the lattice skips it wholesale; the cap exists to catch the skip being dropped, which would produce 2,326 findings.

jq's 342 are dominated by one Low-confidence rule (115 manual-buffer-copy hits) and one heuristic family (16 double-frees); airportd's 102 are dominated by 39 os_log disclosure hits. A raw count is not a quality measurement, which is why the caps are per rule family and why reachable_from_main exists.

False positives and false negatives are both expected. Static analysis over stripped binaries is over-approximation by construction: calls reachable only through indirect dispatch, dynamic class lookup, or unresolved jump tables are misses, and patterns matching a dangerous shape while guarded by a runtime predicate the analyser cannot see are spurious fires. The gates keep the rate measurable; the per-CWE entries above name the specific limitation per detector.

Comparison with other tools

Static binary CWE detection is a small field.

ToolApproachCoverage and limits
cwe_checker (Fraunhofer FKIE)open-source, Ghidra-backed abstract interpretation over Pcode18 CWE checks with a documented false-positive and false-negative posture per check; architecture coverage inherited from Ghidra's universal lift
BinAbsInspector (Tencent KeenLab)Ghidra-backed abstract-interpretation frameworkstrong on x86_64 firmware, thin on macOS-specific surfaces
Mayhem, ForAllSecurefuzzing-oriented, classifying crash types back to CWEs after the factdynamic exploration rather than static proof — complementary rather than competing
BinSkim (Microsoft)code-signing and hardening audit, done wellPE-only and posture-focused, with no semantic analysis of binary content
GrammaTech CodeSentry, Black Duck Binary Analysis, Veracodemostly SBOM plus CVE-by-version-string with a thin static layernot direct CWE detection

cwe_checker is the closest peer, and the overlap is audited module by module — the dangerous-calls, chroot-jail, umask, sizeof-pointer, excessive-alloc, call-order, unchecked-return, use-after-free classification, buffer-overflow object model, interprocedural taint sink, and taint sink table each cite the cwe_checker module they mirror, and the CWE-789 thresholds are matched to its defaults so cross-tool comparison is direct. Its architectural lesson is stated but unevenly applied: never reconstruct a call argument or re-derive an analysis fact inside a detector, because a detector that re-decodes MOVZ or ADRP out of the text section to find a constant is silently arm64-only. Six modules honour it and read the engine-produced argument; the rest still re-decode, and are arm64-only exactly as the rule predicts.

Three mechanisms here have no counterpart in that list.

What is not claimed:

Limits of a finding

A finding lists what was detected, where, with what confidence, and at what severity. It does not assert that the binary is exploitable, vulnerable in production, or in scope for any compliance regime. Those are judgments that depend on the deployment environment, on reachability from untrusted sources, on the secrecy of any embedded credential, and on the threat model — none of which is in the file.

Product surfaces are where judgment gets applied: grouping by binary, CWE, and severity; baselining against a known-good build so a CI run reports only what a change introduced; ranking by reachability from XPC entry points and exported symbols; tracking resolution status across releases; alerting when a known-exploited CVE shape matches a finding in a corpus.

The fence between facts and judgment is deliberate. Anything that depends on judgment lives in the surface. Anything that depends on truth lives in the engine.

SARIF export

Findings export as SARIF 2.1.0 — one run, one tool driver, rules as the union of every rule that fired, CWE expressed on the rule with a MITRE relationship link, and ATT&CK as a taxonomy component shared with the capability and secret findings. The SARIF path runs no analysis of its own; it is a projection of the already-computed findings envelope, which is how these records land in GitHub code scanning, IDE viewers, and anything else that speaks the interchange format.

Findings are one of four parallel security surfaces over the same input. Indicators tag what the binary does (capability plus MITRE ATT&CK), Malware classifies whether it is malicious (a four-tier verdict over composed signals), and CVEs & SBOM flag known-vulnerable component versions (version-based, not code-shape). All four walk the same lifted analysis and emit records of compatible shape. Security is the orientation map; Engine covers the analysis that produces the facts these rules read.