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

A

The logging library (direct). The site passes it text typed by visitors. The advisory is in the KEV catalog.

D

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

B

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.

C

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:

SignalWhat it saysWhat 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:

EcosystemWhen two blocks want two versionsWhat it means for SCA
npmBoth versions can coexist, each in its own subfolderThe 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 versionThe version declared deep in the tree is not necessarily the one that runs
GoMinimal version selection: the lowest version that satisfies everyoneA reproducible result the tool can recompute exactly
Python (pip)One version per environment, chosen at install timeWithout 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.0rc1 comes before 1.0, and 1.0.post1 after.
  • Maven: 1.0-alpha < 1.0-beta < 1.0, with a list of reserved words.
  • Debian: 1:2.3-4, where the 1: (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.

Share this article

LinkedIn Bluesky X Email RSS

Read next: Choosing an SCA tool without being fooled