You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a CycloneDX vulnerability lists multiple affected components, @hailbytes/sbom-diff records and reports only the first one. extractCycloneDXAffects() reads affects[0].ref and discards the rest (src/parser.ts:122-126), and CVEEntry.affects is typed as a single string (src/types.ts:33). Every output format then renders that single ref, so the report understates the blast radius of a vulnerability.
For a package keyworded vulnerability-management / supply-chain-security, this is a real signal loss: the canonical example is Log4Shell (CVE-2021-44228), which in a typical CycloneDX SBOM affects both log4j-coreandlog4j-api (and often shaded/relocated copies). The tool will tell the analyst it affects only log4j-core, silently hiding the other impacted packages.
This is distinct from every open PR/issue. The in-flight CVE work touches other parts of the pipeline: CVSS extraction (#18), highest-severity selection (already merged), risk ordering (#25), the --fail-on gate (#5), and Markdown escaping (#23). None of them change how many affected refs are captured — they all operate on the single-string affects this issue is about.
Evidence (current main)
src/parser.ts:122-126:
functionextractCycloneDXAffects(v: Record<string,unknown>): string{constaffects=v.affects;if(!Array.isArray(affects)||affects.length===0)return'unknown';constref=affects[0]asRecord<string,unknown>;// <-- only [0]returntypeofref.ref==='string' ? ref.ref : 'unknown';}
src/types.ts:33:
affects: string;// <-- one ref, not a list
The truncated value is then surfaced verbatim in all three renderers — text (src/reporter.ts:49, :56) and markdown (src/reporter.ts:106, :113) — so the loss is uniform across text / json / markdown.
Reproduction
A single CycloneDX vulnerability affecting two components:
import{parse}from'@hailbytes/sbom-diff';constsbom=parse(/* the JSON above */);console.log(sbom.vulnerabilities[0].affects);// -> "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"// log4j-api is gone; the report shows the CVE hitting only one package
Diffed against a clean baseline, the markdown "New CVEs" table reads:
(If a hard type change is undesirable, an additive affectsAll?: string[] alongside the existing first-ref affects is a non-breaking alternative — but a single string[] is cleaner.)
2. Parser (src/parser.ts)
functionextractCycloneDXAffects(v: Record<string,unknown>): string[]{constaffects=v.affects;if(!Array.isArray(affects))return[];returnaffects.map(a=>(aasRecord<string,unknown>).ref).filter((r): r is string=>typeofr==='string');}
Empty/absent affects → [] (the renderer can show a placeholder), rather than the current 'unknown' sentinel.
3. Reporter (src/reporter.ts)
Join the refs for display, e.g. v.affects.join(', ') (or one row per ref) in the text (:49, :56) and markdown (:106, :113) sections. json is automatic.
4. Tests (src/__tests__/)
A vuln with two affects refs ⇒ affects.length === 2, both preserved (parser test — extend the existing CVE-2021-44228 fixture in parser.test.ts).
A vuln with no affects ⇒ affects === [] (or documented placeholder), no crash.
Reporter renders both refs in text and markdown (extend reporter.test.ts, whose fixtures currently use a single-string affects).
Note: reporter.test.ts:9-10 and diff.test.ts:73,82 construct CVEEntry objects with a string affects, so those fixtures need updating in lockstep with the type change. Flagging so this is sequenced cleanly relative to the in-flight reporter PRs (#23/#24/#25) to avoid churn.
Secondary, related undercount (optional, same area)
diff() keys vulnerabilities by id alone (src/diff.ts:42-46). If a CycloneDX document ever represents one CVE as two vulnerability objects with the same id (e.g. one per component), the Map<string, CVEEntry> collapses them to one before diffing. Capturing all refs per object (above) handles the common case; this id-only keying is a smaller, adjacent edge that could be folded into the same change or tracked separately.
Why this is high-leverage
Restores the tool's headline signal. "Detect newly introduced CVEs" is only useful if it tells you which packages are hit — truncating to one ref understates exposure precisely when a CVE is most dangerous (one vuln, many components).
Zero new dependencies / no new parsing infrastructure — the data is already in the input affects array; the parser just drops it on the floor today.
Orthogonal to all open PRs/issues — none capture more than the first affected ref, so this won't conflict with anything in flight beyond the in-lockstep test-fixture update noted above.
Happy to open a focused PR (parser + types + reporter + tests) once the direction (single string[] vs additive field) is confirmed and the in-flight reporter PRs land, to keep reporter.ts conflicts minimal.
Summary
When a CycloneDX vulnerability lists multiple affected components,
@hailbytes/sbom-diffrecords and reports only the first one.extractCycloneDXAffects()readsaffects[0].refand discards the rest (src/parser.ts:122-126), andCVEEntry.affectsis typed as a singlestring(src/types.ts:33). Every output format then renders that single ref, so the report understates the blast radius of a vulnerability.For a package keyworded
vulnerability-management/supply-chain-security, this is a real signal loss: the canonical example is Log4Shell (CVE-2021-44228), which in a typical CycloneDX SBOM affects bothlog4j-coreandlog4j-api(and often shaded/relocated copies). The tool will tell the analyst it affects onlylog4j-core, silently hiding the other impacted packages.This is distinct from every open PR/issue. The in-flight CVE work touches other parts of the pipeline: CVSS extraction (#18), highest-severity selection (already merged), risk ordering (#25), the
--fail-ongate (#5), and Markdown escaping (#23). None of them change how many affected refs are captured — they all operate on the single-stringaffectsthis issue is about.Evidence (current
main)src/parser.ts:122-126:src/types.ts:33:The truncated value is then surfaced verbatim in all three renderers — text (
src/reporter.ts:49,:56) and markdown (src/reporter.ts:106,:113) — so the loss is uniform acrosstext/json/markdown.Reproduction
A single CycloneDX vulnerability affecting two components:
{ "bomFormat": "CycloneDX", "specVersion": "1.5", "components": [], "vulnerabilities": [ { "id": "CVE-2021-44228", "affects": [ { "ref": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1" }, { "ref": "pkg:maven/org.apache.logging.log4j/log4j-api@2.14.1" } ], "ratings": [{ "severity": "critical" }] } ] }Diffed against a clean baseline, the markdown "New CVEs" table reads:
— with no indication that
log4j-apiis also affected.Proposed change
Capture all affected refs and render them. Suggested shape (open to alternatives):
1. Types (
src/types.ts)Make
affectsa list while keeping it ergonomic:(If a hard type change is undesirable, an additive
affectsAll?: string[]alongside the existing first-refaffectsis a non-breaking alternative — but a singlestring[]is cleaner.)2. Parser (
src/parser.ts)Empty/absent
affects→[](the renderer can show a placeholder), rather than the current'unknown'sentinel.3. Reporter (
src/reporter.ts)Join the refs for display, e.g.
v.affects.join(', ')(or one row per ref) in the text (:49,:56) and markdown (:106,:113) sections.jsonis automatic.4. Tests (
src/__tests__/)affectsrefs ⇒affects.length === 2, both preserved (parser test — extend the existingCVE-2021-44228fixture inparser.test.ts).affects⇒affects === [](or documented placeholder), no crash.reporter.test.ts, whose fixtures currently use a single-stringaffects).Secondary, related undercount (optional, same area)
diff()keys vulnerabilities byidalone (src/diff.ts:42-46). If a CycloneDX document ever represents one CVE as two vulnerability objects with the sameid(e.g. one per component), theMap<string, CVEEntry>collapses them to one before diffing. Capturing all refs per object (above) handles the common case; thisid-only keying is a smaller, adjacent edge that could be folded into the same change or tracked separately.Why this is high-leverage
affectsarray; the parser just drops it on the floor today.--fail-ondecision and per-CVE sorting trustworthy.Happy to open a focused PR (parser + types + reporter + tests) once the direction (single
string[]vs additive field) is confirmed and the in-flight reporter PRs land, to keepreporter.tsconflicts minimal.