Dependency research · 11
What your SBOM cannot say
A vulnerability in your test runner and one in your web server look the same to a security platform unless the SBOM says which is which. Whether it does is decided by the tool that wrote the file, before your platform ever sees it.
1 Oct 2026 · a demonstration, not a measurement, and not an accuracy claim · re-runnable in about five minutes
An empty field is a claim
CycloneDX gives every component a scope field: required,
optional or excluded. The last one exists for exactly this case,
packages for tests and builds that never reach the shipped artifact. The schema also sets a
default, and states it plainly:
“If scope is not specified, ‘required’ scope SHOULD be assumed by the consumer of the BOM.”
So leaving the field out is not silence. It is an assertion that everything in the file ships, your test framework included. In our measurement of findings that never ship, a large share of reported vulnerabilities sat on packages that only run in tests and builds. If the SBOM cannot say which those are, every one of them reads as a production finding.
The test
One small npm project: express as a runtime dependency, mocha as a
dev-only one. npm already knows which packages are which, because its lockfile marks every
dev-only package. So npm is the referee, not us. We ran four generators against it and asked
one question of each file: does it say which packages are dev-only?
| Generator | Says which packages are dev-only? |
|---|---|
| syft 1.51.1 | No. Leaves dev dependencies out and writes no scope. |
| trivy 0.74.0 | No. Leaves dev dependencies out and writes no scope. |
trivy 0.74.0, --include-dev-deps | No. Adds them, but with no scope, so the file says they ship. |
| cdxgen 12.8.4 | Yes, marked optional. |
| depproof 0.1.24 | Yes, marked excluded, with one error described below. |
Leaving dev dependencies out is a reasonable default, not a mistake. The trivy row with dev dependencies switched on is the one worth noticing: asking for more of the tree made the file say something untrue.
Snyk, which we did not run, says the same of its own CycloneDX output in its CLI reference: “development-only dependencies are not labeled differently from non-development dependencies.” Its SPDX output does carry the distinction.
What Dependency-Track keeps
We loaded each file into Dependency-Track 5.1.0. It stored the scope field exactly as each generator wrote it, and nothing where the generator wrote nothing. (The older 4.14.3 line refuses CycloneDX 1.7 files outright, so on that version only our 1.6 file loaded at all.) Its issue #449, open since 2019, asks to track scope; the storing half of that is already done. The distinction is not lost in the platform. It is lost at the generator, and it comes back by changing only the generator.
The question that survives nowhere
Under every SBOM sits a second question: was this list ever complete? CycloneDX has a
field for that too, compositions. Ours fills it. None of the other generators we
ran uses it, though cdxgen 13.1 can now
grade how complete its resolution was in other
fields. Either way, Dependency-Track drops it, on 4.14.3 and 5.1.0 alike. Which is why a
short list of findings can never be read as proof of a short list of problems.
Check your own SBOM
Point this at any CycloneDX file from your pipeline:
jq '[.components[] | .scope // "absent"] | group_by(.) | map({(.[0]): length}) | add' sbom.json
If it prints only absent, the file is telling your platform that everything in it
ships. To re-run our test, create a package.json with express as a
dependency and mocha as a dev dependency, run
npm install --package-lock-only, then point each generator at the folder.
What goes against us
- We get one package wrong. When the same package is installed on both sides, once under a dev dependency and once under a runtime one, we mark it test-only. It ships, so it should be
required. That is the worse direction to be wrong in, and cdxgen got it right. Fixed in Scanner 1.1, released 4 October 2026; the tables above are from the earlier version. - We write CycloneDX 1.6; the others write 1.7. Ordinary debt.
- Our dependency graph leaves leaf packages without an entry of their own, so a reader cannot tell “this is a leaf” from “this was never analysed”. The same absent-field mistake this page is about, one field over.
- No exploitation signal on its own. Our KEV and EPSS context comes with the hub; grype attaches EPSS to every finding with no server at all.
What this does not prove
It is one small npm project, not a survey of SBOMs, and not a ranking of tools. Other ecosystems were not tested. These tools ship constantly, so re-run it before quoting.
What we do with the field
depproof writes scope on every package, so a platform downstream can separate the vulnerabilities that ship from the ones that only run in your tests, and rank them that way rather than hide either. It also states whether it saw the whole dependency graph, so a short list is never mistaken for a complete one.
If you get something different on your own pipeline, we would rather hear it. hello@depproof.com.
Related reading: findings that never ship · why zero findings is not zero risk · nobody left to fix it · what software supply chain security covers
Questions people ask
Does an SBOM say which dependencies are dev-only?
Only if the tool that wrote it fills in the scope field, and most of the generators we tested do not. CycloneDX has a field for exactly this, component scope, with three values: required, optional and excluded. An SBOM without it cannot tell a test runner from a web server.
What does an empty scope mean in a CycloneDX SBOM?
It means required. The CycloneDX schema sets the default for component scope to required and says that if scope is not specified, required scope should be assumed by the consumer of the BOM. So an SBOM that leaves scope out is not saying it does not know. It is saying that everything in it ships.
Does Trivy mark dev dependencies in its SBOM?
No. By default Trivy leaves dev dependencies out of the SBOM. With the include-dev-deps option it adds them, but without a scope on any package, so the file then asserts that the dev dependencies ship too.
Does Dependency-Track show whether a component is a dev dependency?
Yes, if the SBOM it receives says so. Dependency-Track 5.1.0 stored the scope field exactly as each generator wrote it, and stored nothing when the generator wrote nothing. The distinction is kept or lost by whichever tool wrote the file. The older 4.14.3 line refuses CycloneDX 1.7 files outright, which is what most generators now write.
How do I check the scope field in my own SBOM?
Run one jq command over the components array that prints each scope, or absent where there is none. If everything comes back absent, your SBOM is telling your platform that everything ships, including test tooling.
Cite this
An SBOM that leaves out component scope asserts that everything in it ships.
depproof, “What your SBOM cannot say, and what happens when Dependency-Track reads it,” Dependency research 11, 1 October 2026. Based on the CycloneDX schema’s default for component scope and one npm test project; versions and limits stated above. May be reproduced with attribution and a link back.