Summary
CycloneDX components carry a scope field — required (runtime), optional (dev/test/build), or excluded — and generators like syft, cdxgen, and npm sbom populate it so consumers can tell production dependencies apart from dev/test ones. The parser silently drops it: scope is not on the Component type (src/types.ts:11-26) and parseCycloneDX never reads c.scope when mapping components (src/parser.ts:29-36). A grep -ri scope src/ returns nothing.
Because the signal is thrown away, every diff and every --fail-on gate treats a dev/test-only dependency exactly like a production one. For a package keyworded supply-chain-security whose headline use case is a CI/CD gate (README "CI/CD gate", src/cli.ts:105-112), that has two concrete costs:
- Gate false positives. A new CVE that only affects a
devDependency (a test runner, a bundler plugin) trips --fail-on high and breaks the build for a vulnerability that never ships to production. Today there is no way to scope the gate to runtime risk.
- Report noise. Routine dev-dependency churn (linters, type stubs, test libs bumping) floods
Added / Removed / Upgraded, burying the production changes a reviewer actually needs to see in an audit.
This is the same "declared/parsed-but-unused signal" class already recognized for license (#9), hashes (#22), and document identity (#52) — data the SBOM provides that never reaches the diff. It is distinct from all of those (they surface license/integrity/provenance; this is about dependency classification) and from the in-flight keying/upgrade PRs (which change how components are matched, not which components are in scope). No open issue or PR references component.scope, dev-dependency filtering, or a --scope/--runtime-only concept.
Reproduction (current main)
new.cdx.json introduces a critical CVE that only affects an optional-scope (dev) component:
npx @hailbytes/sbom-diff old.cdx.json new.cdx.json --fail-on high
# → Gate failed (exit 3), even though the only affected component is a dev-only test runner
# that never reaches a production artifact.
There is no flag that lets the user say "gate on production dependencies only."
Proposed change
Capture the field and let callers scope by it. Backward-compatible (default preserves today's behavior).
- Type (
src/types.ts): add to Component
/** CycloneDX component scope: 'required' (runtime), 'optional' (dev/test/build), 'excluded'. */
scope?: 'required' | 'optional' | 'excluded';
- Parser (
src/parser.ts, parseCycloneDX): read c.scope when it is one of the three valid values; leave undefined otherwise. SPDX has no direct scope equivalent, so parseSPDX leaves it undefined (worth a one-line doc note).
- CLI (
src/cli.ts): add --scope <all|runtime> (default all).
all → current behavior (diff every component).
runtime → before diffing, keep only components whose scope is required or undefined (treat unknown/unset scope as runtime, so SBOMs without scope data are never silently emptied). This filters the input SBOMs, so it applies uniformly to added / removed / upgraded and to the CVE gate.
- Docs: one row in the
README options table + a note that --scope runtime is the recommended setting for --fail-on gates.
- Tests: parser captures
scope; --scope runtime excludes an optional component and its CVE from the report and the gate; unset-scope components survive the filter.
Open design questions for the maintainer
- Flag surface:
--scope all|runtime vs. a simpler boolean --runtime-only. A value flag leaves room for a future --scope dev or explicit excluded handling.
excluded components: CycloneDX excluded means "known present but explicitly out of the assertion." Reasonable to drop these under runtime (and arguably under all too, but that would be a behavior change — probably out of scope here).
- Should
--scope runtime also filter the report body, or only the gate? Filtering the whole diff (as proposed) keeps report and gate consistent; gate-only would be a smaller change but leaves the noise in the report.
Happy to open a focused PR once the flag name and the excluded/undefined defaults are settled — flagging the API decision first since it touches types.ts / parser.ts / cli.ts, which several in-flight PRs also modify.
Summary
CycloneDX components carry a
scopefield —required(runtime),optional(dev/test/build), orexcluded— and generators likesyft,cdxgen, andnpm sbompopulate it so consumers can tell production dependencies apart from dev/test ones. The parser silently drops it:scopeis not on theComponenttype (src/types.ts:11-26) andparseCycloneDXnever readsc.scopewhen mapping components (src/parser.ts:29-36). Agrep -ri scope src/returns nothing.Because the signal is thrown away, every diff and every
--fail-ongate treats a dev/test-only dependency exactly like a production one. For a package keywordedsupply-chain-securitywhose headline use case is a CI/CD gate (README"CI/CD gate",src/cli.ts:105-112), that has two concrete costs:devDependency(a test runner, a bundler plugin) trips--fail-on highand breaks the build for a vulnerability that never ships to production. Today there is no way to scope the gate to runtime risk.Added/Removed/Upgraded, burying the production changes a reviewer actually needs to see in an audit.This is the same "declared/parsed-but-unused signal" class already recognized for license (#9), hashes (#22), and document identity (#52) — data the SBOM provides that never reaches the diff. It is distinct from all of those (they surface license/integrity/provenance; this is about dependency classification) and from the in-flight keying/upgrade PRs (which change how components are matched, not which components are in scope). No open issue or PR references
component.scope, dev-dependency filtering, or a--scope/--runtime-onlyconcept.Reproduction (current
main)new.cdx.jsonintroduces a critical CVE that only affects anoptional-scope (dev) component:There is no flag that lets the user say "gate on production dependencies only."
Proposed change
Capture the field and let callers scope by it. Backward-compatible (default preserves today's behavior).
src/types.ts): add toComponentsrc/parser.ts,parseCycloneDX): readc.scopewhen it is one of the three valid values; leaveundefinedotherwise. SPDX has no direct scope equivalent, soparseSPDXleaves itundefined(worth a one-line doc note).src/cli.ts): add--scope <all|runtime>(defaultall).all→ current behavior (diff every component).runtime→ before diffing, keep only components whosescopeisrequiredorundefined(treat unknown/unset scope as runtime, so SBOMs without scope data are never silently emptied). This filters the input SBOMs, so it applies uniformly toadded/removed/upgradedand to the CVE gate.READMEoptions table + a note that--scope runtimeis the recommended setting for--fail-ongates.scope;--scope runtimeexcludes anoptionalcomponent and its CVE from the report and the gate; unset-scope components survive the filter.Open design questions for the maintainer
--scope all|runtimevs. a simpler boolean--runtime-only. A value flag leaves room for a future--scope devor explicitexcludedhandling.excludedcomponents: CycloneDXexcludedmeans "known present but explicitly out of the assertion." Reasonable to drop these underruntime(and arguably underalltoo, but that would be a behavior change — probably out of scope here).--scope runtimealso filter the report body, or only the gate? Filtering the whole diff (as proposed) keeps report and gate consistent; gate-only would be a smaller change but leaves the noise in the report.Happy to open a focused PR once the flag name and the
excluded/undefineddefaults are settled — flagging the API decision first since it touchestypes.ts/parser.ts/cli.ts, which several in-flight PRs also modify.