SCA: from inventory to priority
Run two SCA tools on the same project: they will rarely give the same list.
To see why, we follow a fictional application through five levels of analysis, then open the hood: the dependency tree, version comparison, reachability, VEX. The article builds on SCA explained simply, but stands on its own.
The example application
Here is a fictional application, "OrderEasy", an online ordering site written in Java. Its developers declared 6 direct dependencies; once the full tree is rebuilt, it contains 38, 32 of them indirect.
Four matches are reported, including the mistaken namesake C. Everything here is invented, advisories included, to illustrate the reasoning. A good tool must understand that these four cases do not call for the same reaction:
2 real exposuresThe component is there and its faulty part is used
The logging library (direct). The site passes it text typed by visitors. The advisory is in the KEV catalog.
An XMLXML: a text file format structured with tags file-reading library (indirect). The site uses it, but only at night, for files from known partners, on a server not accessible from the Internet.
2 trapsTwo cases to verify before dismissal
An image-processing library (indirect). The faulty functionfunction: a piece of code that does one precise task, called by the rest of the program exists, but no call was found within the analyzed scope.
A date library (indirect). A tool that searches by product name confuses it with another product of the same name, which does have an advisory.
Before the level-by-level detail, here is the full tree. Click through the levels and watch the alerts sort themselves out:
This animation needs JavaScript. In short: 38 components, 4 initial alerts.
Level 1: the inventory alone
38 components listed0 answers on risk
The first level rebuilds the list, with exact versions, direct and indirect: it is the software bill of materials, or SBOMSBOM: Software Bill of Materials: the developer's shopping list, with every component and its version. It is usually written in a standard format, for example CycloneDX (the ECMA-424 standard) or SPDX (the ISO/IEC 5962 standard).
It is indispensable, and it is not yet risk analysis. When a flaw like Log4Shell lands, you can at least answer "are we affected?" without digging through each project by hand.
Level 2: matching advisories by name
4 alerts1 wrong identification
The second level compares each component with advisory databases. On our example, components A, B, C and D come up, but C is a mistake.
The difficulty lies in how a component is identified. Some tools rebuild a generic product name from what they find in the files, then look it up in the US government's vulnerability database (the NVDNVD: National Vulnerability Database: the US database describing CVEs). OWASPOWASP: a foundation that publishes free application security guides and tools Dependency-Check, for instance, describes this approach: it collects evidence to determine a product identifier (a CPECPE: a product identifier used by the NVD to say which software a flaw affects), then looks up the associated CVECVE: Common Vulnerabilities and Exposures: the unique number of a known flaw, such as CVE-2021-44228s. When two products have similar names, confusion is possible, and matching a package to a CPE stays approximate.
Key point:a wrong identification is not a detail.
It costs an investigation, and it teaches the team to stop trusting the tool.
Level 3: matching by version and ecosystem
3 alertsC dismissed
Other sources work differently: they publish advisories directly per package managerpackage manager: the tool that installs a project's blocks, such as npm or Maven (Maven for Java, npm for JavaScript, PyPI for Python) and per version range. The open OSVOSV: Open Source Vulnerabilities: a public database of security advisories, sorted by ecosystem database, for example, aggregates advisories from GitHub, PyPA or RustSec across more than 50 ecosystems, with a common format that ties each advisory to precise versions.
The difference lies in the identifier. The CPE describes a product ("log4j, by apache"), chosen by NVD analysts. The purl (package URL) describes a package, as the package manager knows it: ecosystem, exact name, version.
CPE cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*
purl pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
On our example the match is exact here: C disappears, because once identified by its ecosystem and exact package name, the date library is not the product the advisory is about. The versions of A, B and D still fall within the affected ranges.
No database is complete, though, and two databases rarely cover the same advisories at the same moment. In early 2024, the NVD fell massively behind in enriching CVEs: thousands of flaws published without the CPEs saying which products they affect. A tool living only off the NVD went blind on part of the new flaws, without necessarily saying so.
Key point:always ask a tool where its advisories come from. A good tool cross-checks several sources and says, for each alert, which one triggered it.
Level 4: reachability
2 priority alertsB at low priority
A component provides many functions. Advisory B concerns a single one of them in the image library, and the analysis finds no call path to this function, directly or through another block, within the scope examined.
The application's code
Calls functions from its dependencies.
The faulty function
Is it on a call path that starts from the application?
To find out, the tool draws a map of calls (who calls whom) and checks whether a path leads to the vulnerable function. Google presents this idea in OSV-Scanner under the name call analysis.
It still has to know which function is vulnerable (from the advisory, an enriched database or the patch) and support the language. The most mature example is Go: its vulnerability database lists the affected functions, and the official govulncheck tool only reports a flaw if your code can call one of them. Elsewhere it is more fragile: in Java, reflection and dependency injection hide part of the calls; in JavaScript, the dynamic nature of the language does the rest.
It also carries a risk: indirect calls, through reflection or configuration, are hard to follow, and a tool that concludes "not reachablereachable: said of a flaw whose faulty part the application can actually reach" too quickly may dismiss a real flaw. Lowering the priority therefore requires checking analysis coverage and the deployed configuration. If the function or language is unsupported, the result remains unknown: finding no path is not enough to dismiss the alert.
Level 5: context and exploitation signals
1 urgent, 1 plannedA first
A and D remain, both truly present and called. The last level ranks them with outside signals and the application's context:
| Signal | What it says | What it does not say |
|---|---|---|
| Base severity score (CVSS) | The theoretical severity of the flaw | Whether it is exploited, or whether it affects you: the base score (CVSS-B) ignores your environment, which CVSS 4.0 (2023) treats separately |
| KEV (CISA catalog) | Real attacks have been observed | Whether your configuration is targeted, or every exploitation in the world |
| EPSS (FIRST) | A probability of exploitation seen in the wild within 30 days, updated daily (0.02 = 2%), not to be confused with the percentile | A fact: it is a statistical estimate |
| Exposure | Is the component reachable from the Internet? | How severe the flaw itself is |
In our example, A has it all: a direct component, a function called with a visitor's input, a server open to the Internet, an advisory in the KEV. D is real but locked behind an internal network, with known partner files. It still has to be fixed, with a written deadline (for example the next planned update), after checking that this network and these files really are under control.
Key takeaway:none of these signals replaces your context. A KEV-listed flaw in a function you never call can weigh less than an average flaw wired to your login form.
Under the hood: rebuilding the tree
Everything starts with the inventory, and the inventory is sneakier than it looks. A project states its wishes in a manifest (package.json, pom.xml, pyproject.toml), often as ranges: ^4.17.0 means "any 4.x from 4.17.0 up".
The versions actually selected are written in the lockfilelockfile: a file that pins the exact version of every installed block (package-lock.json, poetry.lock…), and what ends up in the shipped application can differ again (development dependencies left out, for example). With the manifest alone, without resolution or a real inventory, the tool guesses, and it often guesses wrong.
Then each ecosystem resolves conflicts its own way, and that changes what ends up in the application:
| Ecosystem | When two blocks want two versions | What it means for SCA |
|---|---|---|
| npm | Both versions can coexist, each in its own subfolder | The same block can show up three times, in three versions: you have to see all of them |
| Maven | “Nearest wins”: the declaration closest to the project prevails, unless a dependencyManagement section forces a version | The version declared deep in the tree is not necessarily the one that runs |
| Go | Minimal version selection: the lowest version that satisfies everyone | A reproducible result the tool can recompute exactly |
| Python (pip) | One version per environment, chosen at install time | Without a lockfile, two installs of the same project can differ |
And then there are stowaways. A block can bundle a copy of another inside its own package, sometimes even renamed (Java folks talk about a fat JAR and shading). It can then vanish from the published dependency graph.
During Log4Shell, many teams discovered Log4j hidden this way: tools that inspected the contents of archives found those copies, tools that only read manifests missed them.
Finally, a container image also holds the system packages (Debian, Alpine, Red Hat…), read from the system package manager's databases. And there, a new trap: distributions often fix a flaw without changing the upstream version number (a backport).
A tool that does not read distribution-specific advisories will cry wolf on a package that is already fixed.
Key takeaway:two tools that do not rebuild the same tree cannot produce the same report. Before comparing alerts, compare inventories.
Comparing two versions is not comparing two strings
Trick question: is 1.10 greater than 1.9? For a human, yes.
For a computer comparing text character by character, no: after "1.", it compares "1" and "9", and concludes that 1.10 is smaller than 1.9.
Congratulations, your home-made tool has just declared a fixed version vulnerable.
And each ecosystem has its own rules:
- Python (PEP 440):
1.0rc1comes before1.0, and1.0.post1after. - Maven:
1.0-alpha<1.0-beta<1.0, with a list of reserved words. - Debian:
1:2.3-4, where the1:(the “epoch”) overrides everything else. - npm: semantic versioning, with pre-releases such as
2.0.0-beta.3.
That is why the OSV format describes an affected range with events (“introduced in”, “fixed in”) and states which comparison rule applies: semantic versioning, the ecosystem's rules, or Git history.
VEX: writing decisions in a format machines can read
When you set B aside or postpone D, the reason has to outlive your memory, and ideally travel with your software. That is the job of VEXVEX: a document stating, for a known flaw, whether a product is really affected.
For each "product, flaw" pair, it gives a status: affected, not affected, fixed or under investigation. And a "not affected" must be justified, for example:
{
"vulnerability": { "name": "CVE-XXXX-YYYY" },
"products": [ { "@id": "pkg:maven/com.example/easyorder@3.2.0" } ],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
This fragment, in OpenVEX format (CSAF and CycloneDX have one too), says in essence: "the faulty block is there, but the vulnerable code is never executed". For B, this status requires a verified justification for the product and its version. Postponing D leaves it "affected", with tracked deadlines and mitigations.
This fragment is not a complete document: exchanging it also requires the metadata defined by OpenVEX, including the author, timestamp and document version.
And what does the law say?
For products covered by the CRA on the EU market, manufacturers must document components, including through an SBOM, from 11 December 2027; reporting of actively exploited vulnerabilities and severe incidents applies from 11 September 2026, with early warning within 24 hours and notification within 72 hours of awareness.
Wrapping up
Between a list of CVEs and a list of risks lie an exact inventory, matching by ecosystem and version rather than by name, verified reachability and the exposure context. In OrderEasy, four alerts become one emergency, one planned fix, one low priority and one misidentification set aside. Before comparing two tools, compare their inventories, then the quality of their triage, and keep every decision in writing.
Words from this article · 13 words
- Bill of materials (SBOM)
- The list of a piece of software's components, with their versions.
- CycloneDX, SPDX
- Two standard bill-of-materials formats.
- CVE
- A unique public identifier given to a known flaw.
- CPE
- A generic product identifier, used to link a component to CVEs.
- NVD
- The US government's vulnerability database, run by NIST.
- OSV
- An open advisory database, organized by ecosystem and versions.
- Lockfile
- The file that pins the exact versions of all dependencies.
- Reachability
- Whether a call path can reach a component's faulty part within the analyzed scope.
- KEV
- CISA's catalog of flaws with confirmed exploitation.
- EPSS
- An estimate from 0 to 1 of the probability of exploitation within 30 days.
- VEX
- Vulnerability Exploitability eXchange: a document that says whether a product is affected by a known flaw.
- purl
- Package URL: a package identifier stating the ecosystem, the exact name and the version.
- Backport
- A fix applied by a distribution without changing the upstream version number.
To choose a tool (market tools, questions to ask, tests to run): Choosing an SCA tool without being fooled. To revisit the basics: SCA explained simply. And for the code the team writes itself: SAST in depth.
Read next: Choosing an SCA tool without being fooled