Sign in

Security

lsns, the util-linux tool that lists namespaces — a 67,752-byte stripped arm64 ELF — produces 42 CWE findings: one critical, six high, spread across ten CWE classes. Downloader.exe, a 7,680-byte PE catalogued on MalwareBazaar as StormKitty since 9 January 2023, produces five: no Control Flow Guard, no stack cookie, no Authenticode signature, no CET shadow stack, no load-time integrity check. Every one of the trojan's five findings is a complaint about its build flags. None of them is about what it does.

That is not a scanner failing. It is two different questions asked of two different files, and only one of them was ever "is this thing hostile?" Four such questions get asked of a compiled binary, and their answers move independently of each other.

QuestionAnswered inA hit isA hit is not
What is wrong with this code?Findings — MITRE CWEa call site where a defect class is structurally present, with rule id, severity, confidenceproof of exploitability; that needs a deployment and a threat model the engine does not have
What does this binary do?Capabilities and Indicators — capability catalog, capa categories, MITRE ATT&CKa named behavior with its evidence and technique idsa judgment — a stealer's recon sweep and lsns earn the same discovery.files_walk_stat row
Is this binary malicious?Malwareone label plus the point ledger that produced it, every row citing a rule and a call siteattribution — a family row reports strings shared with samples labelled X, never that this is X
Does it bundle something already known-vulnerable?CVEs & SBOM — MITRE CVEa component, a version, and the published affected ranges that version falls insidea statement about the binary's own code

Verdict labels

The verdict carries five labels: Benign, Suspicious, LikelyMalicious, Malicious, and NotEnoughEvidence. The fifth is an abstention with two independent causes, because neither a crashed analysis nor a barely-analyzed one may serialize as the same clean answer a binary earns by being analyzed with nothing found.

NotEnoughEvidence fires whenThe condition
The findings-and-malware pass never completedthe assessment did not run to completion — an engine panic leaves the threat block at its default
Coverage was too thin to stand behind any verdictunsupported architecture; or the file reads as packed, or as tiny-imports-plus-high-entropy code; or no functions were discovered at all; or under 8 functions were discovered and under 5% of them carry a name

On a packed sample the coverage gate is the dominant path, and it never downgrades a real detection: a Malicious tier, a LikelyMalicious tier reached by a proven or definitive ledger row, and an independent anomaly flag are each exempt. A missing coverage block reads as sufficient — abstention needs an affirmative thin-coverage signal, never an absent field.

Source code

Source files share the binary side's CWE taxonomy, finding schema and rule catalog (Source SAST): system(user_input) fires the same CWE-78 topic whether the C file or the compiled ELF was uploaded. The shared catalog is nonetheless a source-side asset — the binary engine's real detector surface is hand-written Rust, not the catalog.

CountWhat it counts
10source languages dispatched by file extension in production
9further walkers that exist, reachable only from the evaluation harness
1,190catalog rows carrying a detector list
109of those 1,190 naming the binary engine, 9% — every other row is source-only
106catalog rows naming more than one engine
96of those 106: libc primitives split between C source and compiled binaries — literally the same row firing on both
10the remaining multi-engine rows: 9 shared Java/Kotlin, 1 spanning binary, C and Python

Catalog depth is uneven by language, and the thin end is where a source scan will miss things:

EngineCatalog rows
source-python321
source-php223
source-javascript201
source-c123
binary109
source-ruby107
source-java100
source-go74
source-swift22
source-kotlin9
source-csharp8

Worked example: lsns

lsns fires 42 findings across ten CWE classes, one indicator, no capabilities, and reads Benign:

sha256   001037f69f7c75a77e5e13b7f95e679627f9fc9477b789ffe0ffb4b39dfa7a6e
         ELF arm64, 67,752 bytes, stripped, glibc

findings 42   CWE-134 ×17  CWE-252 ×7  CWE-787 ×4  CWE-209 ×3  CWE-782 ×3
              CWE-125 ×2   CWE-415 ×2  CWE-693 ×2  CWE-367 ×1   CWE-15 ×1
              1 critical, 6 high, 29 medium, 1 low, 5 info
              reachable from entry point: 0   unreachable: 38   unknown: 4

indicators 1  discovery.files_walk_stat — T1083 File and Directory Discovery
              evidence: imported `fstatat`

capabilities 0

verdict       Benign

The one critical finding is a compound: seven independent detectors converging on the same function at 0x2a00, spanning five CWE classes — CWE-15, CWE-125, CWE-134, CWE-252 and CWE-787. A compound emits one tier above the strongest finding it composes, capped at Critical, so a Critical compound needs a High constituent — lsns has six. Two cases suppress that escalation and emit at the strongest constituent's own severity: a caller with 45 or more outgoing calls, where convergence reflects caller complexity rather than corroboration, and a stack of entirely low-confidence constituents.

The single indicator, discovery.files_walk_stat, is a structural shape and not a judgment. It is opendir plus readdir plus a per-entry stat companion — the read-directory-then-stat-each-entry loop. That is exactly the ATT&CK technique a stealer earns for its recon sweep, and lsns earns it for the same structural reason: it walks /proc and stats what it finds. The indicator surface reports the shape and stops. Nothing in the verdict moved.

The triage filter fails on this file: reachable_from_main is the field that turns 42 findings into a short list, and here the entry-point closure was computed, placed 38 of the 42 outside it, and left 4 unmeasured — so it orders nothing. The binary is stripped, every caller is an address (sub_2a00, sub_4f90), and its table-output path registers callbacks with libsmartcols.

Indirect calls are not a blanket blind spot — the closure traverses most of them and names what it drops:

Indirect edgeIn the closure
16 recovered kinds: jump tables, C++ and Objective-C devirtualization, blocks, function-pointer constants and function-pointer tables, GOT and import binds, P/Invoke thunks, hash-resolved API calls, heap function-pointer fields, XPC handlers, targets recovered by emulation or by value-range analysistraversed like any direct edge
An indirect call that resolved to no targetdropped
A callback registered through an API outside the curated 14: pthread_create, atexit, at_quick_exit, qsort, qsort_r, bsearch, the four dispatch_*_f variants, CreateThread, the three signal-family entry pointsdropped — the callee never becomes a node

scols_column_set_wrapfunc is not among the 14, so the call to it resolves while the function lsns hands it never becomes a node. sigaction is excluded for a different reason: its handler lives in a struct field rather than a register argument.

A global reliability gate decides whether the field is stamped at all. Entry roots come from seven tiers — the format's own entry point, main, daemon run-loop callers, __libc_start_main callees, .init_array and TLS constructors, exports, and C++ exception landing pads — and a closure covering under 10% of the call graph's caller nodes leaves reachable_from_main unset on every finding rather than stamped from a closure that reached almost nothing. So unreachable means the closure was judged reliable and this caller sat outside it; unset means the question was not answered (Findings).

Worked example: the StormKitty downloader

The StormKitty downloader fires five findings — all of them build posture, none a code defect — plus nine capabilities that between them quote a live payload URL and name reflective stage-two execution, and it still reads Benign:

sha256   000a12ffb6c5dfcc0fada157d40500965861ca5e63d4f44fa4a679f7024c6f69
         PE x86, 7,680 bytes, .NET, unsigned

findings 5    posture.pe.no-cfg  posture.pe.no-gs-cookie  posture.pe.unsigned
              posture.pe.no-cet-shadow-stack  posture.pe.no-force-integrity
              CWE-693 ×3, CWE-345 ×2 — all build-posture, no code defect

indicators 0

capabilities 9
              cdn-hosted-payload.fetch-remote-payload
                T1105 Ingress Tool Transfer, T1102 Web Service
                evidence: the string
                  https://cdn.discordapp.com/attachments/1053623990066876427/
                    1054133763724021820/Xumrybezs.dat
                co-located with the download primitive HttpWebRequest
              nursery-load-code.load-net-assembly                T1620
              nursery-load-code.invoke-net-assembly-method       T1620
              nursery-data-manipulation.compress-data-using-
                gzip-in-net                                  T1560.002
              communication.send-http-request
              communication.create-http-request
              communication.receive-http-response
              communication.send-data
              host-interaction.suspend-thread

verdict       Benign

Read the findings alone and this file is a slightly under-hardened build. Read the capabilities and it is a stager: it fetches stage two from an allowlist-friendly host, runs it through a .NET GZip stream, loads it as a .NET assembly and invokes a method on it. The two T1620 rows are the stronger evidence — reflective execution of code that arrived over the wire — and carry no more verdict weight than the URL does. Reading one surface and calling it a security review is how a 7.5 KB downloader passes.

Surface availability

Three of the four surfaces are open to any caller who can see the binary; the findings surface is not, and no plan buys it.

SurfaceGateBelow the gate
Capabilitiesvisibility only, no feature flagnothing to fall below — a caller who cannot see the binary gets a not-found
Malware and the threat ledgerFree floorsame
CVEs & SBOMFree floorFree reads per-severity counts and an upgrade hint; per-CVE detail is Pro
Findings and embedded secretsoperator-issued grant onlyabsent, including from the SARIF export — Pro does not clear the floor, and neither does anything else sold self-serve

A second gate crosses the whole binary envelope rather than one surface: a Free or anonymous caller reading a binary whose architecture is outside x86, x86-64, arm and arm64 gets counts only, with no analysis in the envelope. Both worked examples sit inside that set; MIPS, PowerPC and RISC-V firmware do not. An absent arch string or the literal unknown fails open; every other unrecognized value fails closed.

Reading order by task

Your own code starts at findings; a third-party binary starts at capabilities and indicators; a bundled-component image starts at CVEs; a pipeline gate rides the SARIF export. Each order carries its own caveats.

TaskOrderThe catch
Auditing your own code before shippingFindingsthe surface is grant-only — no plan tier clears it; severity carries no deployment context; reachable_from_main only filters when the closure clears the 10% reliability gate
Auditing a third-party binaryCapabilities and Indicators → Malware → Findingsfindings describe code quality, which says little about intent
Auditing firmware or a bundled-component imageCVEs & SBOM first; findings, indicators and malware still runcomponent identification is what makes a several-thousand-file image tractable, but CVE rows carry no reachability at all, and a MIPS/PowerPC/RISC-V image is Pro-view-gated
Wiring a pipelineSARIF 2.1.0 exportan ungated caller gets a valid, empty document rather than an error; there is no built-in diff against a known-good build

Auditing your own code before shipping. Findings is the surface. The catalog covers the memory-safety, crypto, injection, identity and build-posture classes that come up in review. Treat the output as facts about the code and apply your own deployment context; reachable_from_main is the highest-leverage filter when the closure is reliable, and on a stripped lsns it was computed and placed 38 of the 42 findings outside it — a short list of 0. The per-CWE pages document each rule's confidence and false-positive posture.

Auditing a third-party binary. Capabilities and Indicators are the first surfaces — what it does, before what is wrong with it. Then Malware for the label and the ledger that produced it. Findings come last here: they describe code quality, and the StormKitty case shows how little that says about intent.

Auditing firmware or a container of bundled components. CVEs & SBOM is the surface, and identification is what makes an image of several thousand files tractable. Most of it comes from the image's own package metadata: eleven package-manager and lockfile readers — apk, dpkg, opkg, rpm (including the raw rpm header), npm, PyPI, Packagist, RubyGems, Go, Cargo and Maven. Identified components then match against an operator-fetched advisory database: CVE-List and OSV affected-version ranges across thirteen ecosystems, enriched with CVSS, EPSS and CISA KEV.

The curated fallback plane, for images with no package database, is small — not broad upstream coverage:

Curated tableEntriesWhat it covers
Version banners scanned out of a binary7 rules6 upstream products — busybox, openssl, dropbear (two symbol prefixes), dnsmasq, the Linux kernel, openssh
Path patterns identifying a component by file location6 patterns2 Zyxel-specific (weblogin.cgi, zysh) and 4 generic (samba, apache-httpd, cups, ipsec/pluto)
Firmware-version CVE rules8 rulesZyxel model strings only — AAZF.4/5/6, 7.3.245.300, V4.6/4.70/4.71/4.72

Findings, indicators and malware still run on every member. Reachability on a CVE row is a declared stub, always unset, so the filter the findings surface leans on does not exist here.

Wiring a pipeline. The SARIF 2.1.0 export is the surface, so a gate rides GitHub code scanning or any SARIF-speaking consumer rather than bespoke JSON. A binary's document carries CWE findings, secrets, CVE rows, capability hits and the threat ledger; source-file findings are a separate document, and no route fuses the two. Rule ids are stable string identifiers rather than positional indexes, which is what makes suppression and baselining possible on the consumer's side — the export emits no baseline state of its own and there is no built-in diff against a known-good build. Two binaries can be compared directly, and the comparison diffs indicator hits, attributed families, secret findings and the ATT&CK-tactic-organized capability inventory; a capability present in v1.1 and absent in v1.0 is the alertable event. The verdict is not on that surface — the compare response carries no tier, so a Benign-to-Malicious move must be read off the binary detail.

Three limits decide whether a gate is telling the truth:

LimitValueConsequence
Gated families are dropped, not erroredno findings grant means no CWE or secret results, no CVE feature means no CVE results — the document stays valid and the gate passes green on it
Results per run25,000beyond that, results are kept by severity rank and the run reports an omitted count
Members per package document1,024a several-thousand-file firmware image — the case this export is most wanted for — is truncated

Capability promotion to the verdict

Of the 923 entries the capability catalog holds, a capability moves the verdict only if its id appears in a curated promotion table; every other entry is enrichment worth zero points.

Rule groupCountEffect on the verdict
Capability ids in the promotion tableexactly 35, spanning 23 familiescarries weight, at the band of the hand-written detector it replaced
Every other capability entry in the catalogthe balance of the 923none — a recorded fact worth zero points
Hand-written Rust detectors, never moved to the catalogroughly 35unaffected by promotion; they stay in Rust

35 promoted ids do not mean up to 35 ledger rows. Three guardrails stand between a fired capability and a point:

GuardrailEffect
A capability id is family.shape, and promoted ids sharing a family collapse into one row carrying the highest band that firedceiling of 23 rows; a family authored as a strong shape plus a broader weak one reproduces the retired detector's per-shape banding rather than double-counting it
A capability marked ineligible for scoring, set on verified platform-signed binarieshard-excluded, not down-weighted — on a platform-signed binary the promotion path is inert entirely
Capabilities never fire inside code matched as a known static librarya vulnerable-looking shape vendored in from a linked library scores nothing

cdn-hosted-payload.fetch-remote-payload fired correctly on the StormKitty sample, named the technique, and quoted the Discord CDN URL — and the stored verdict is still Benign, because that capability id is not one of the 35. Letting every catalog rule score instead reopens the individually-scored path the verdict was recalibrated away from, where a loose capability rule floors a benign binary that a tighter detector's corroboration would have spared.

That rule carries its own measurement, taken 3 August 2026: 9 hits across the 1,723 malware samples then in the corpus, spanning seven distinct families, and 0 across 82 benign binaries. The corpus has since grown to 1,729; the measurement has not been re-run against the six additions. A looser arm keyed on a CDN host plus a payload-shaped file extension was cut — it added one malware sample the strict arm already caught, and it was the only arm producing benign matches, on a shell script and a README fetching pinned test fixtures from raw.githubusercontent.com. Fetching from a public CDN stays dual-use in principle, which is the other reason the id was never promoted.

Promotion is frozen at those 35 ids. The stated condition for resuming is a standing recall-and-benign-false-positive gate over the full malware corpus wired into CI. That gate does not exist, so each further promotion would rest on an ad-hoc local run that leaves no regression fence behind it.

False positives on known-clean binaries

The false-positive surface is measurable without a labelled set, by crossing a CWE with a known-clean provenance: a curated distribution binary firing a memory-corruption class is a false-positive candidate by construction.

Rule classFiring, of 1,475 Linux-distro-packaged corpus binariesShare of the curated set
CWE-787 out-of-bounds write718very close to half
CWE-125 out-of-bounds read732very close to half

Ranking the rules by how heavily they fire on that known-clean set ranks the over-firing rules, and that ranking is the working triage queue.

Out of scope

Five answers are deliberately not produced, and reading the output as though they were is the standard error.

Severity is not CVSS. The severity on a finding is a per-instance hint about the runtime context around a source-level bug. It is not a risk score and does not compose into one, because the inputs CVSS needs are deployment facts the engine cannot see. CVE rows are the exception and carry upstream CVSS, EPSS and KEV verbatim, because those describe a published advisory rather than this binary.

No remediation guidance, except on build posture. 18 build-posture rules close their summary with the exact flag that fixes them — /GUARD:CF, /GS (default for MSVC), signtool.exe, -Wl,-z,relro,-z,now for full RELRO, -fPIE -fpie linked -pie, -D_FORTIFY_SOURCE=2 -O1 — and all five of StormKitty's findings carry one. Nothing else does: a code-defect finding states what is present, not how to patch it, and CVE detail is the upstream fields — name, severity, description — with no fix instructions and no "upgrade to version X".

No feed fetch during analysis. Nothing is fetched at analysis time, so the same bytes against the same snapshot give the same answer. The ATT&CK technique catalog is compiled in as three tables over one pinned MITRE CTI snapshot — 430 macOS-relevant techniques, 835 full-enterprise, and a 712-technique native-binary denominator, 1,977 rows in all — and the curated firmware CVE tables are in-tree constants; neither moves without a rebuild. The advisory database is a local snapshot the operator refreshes on a schedule that defaults to off.

No dynamic analysis. Nothing is executed on a CPU, sandboxed, hooked or instrumented. A sample's own decryptor is interpreted in a bounded IL emulator to recover the strings and configuration it hides (Emulator), and that is not a runtime trace: no syscall reaches a kernel, and no branch is taken because of a real environment. Excluding its shared-library rules, capa's pinned snapshot declares a dynamic scope of call on 271 rules and span of calls on 410; none of that class of evidence is reproduced here. A fuzzer reaches paths a static analyzer never will.

No automatic family attribution. A family fingerprint reports strings shared with samples attributed to family X. It never claims the binary is family X, and a nearest-neighbour attribution alone is capped below the top tier so it can never carry a Malicious verdict on its own.

The shared analysis record

All four surfaces read one analysis record. The same recovered call graph, the same SSA-form IL, the same constant-propagation results, the same string and import inventories, the same code-signature parse feed the CWE detectors, the capability matcher, the verdict ledger and the component extractor. Almost none of that record is stated in the file — the call graph, the function boundaries and the argument values are all recovered (Engine). The taxonomies above it differ; the input does not.