From 2b6c29e00ca52ef22f310a594d6b22907bc10f5b Mon Sep 17 00:00:00 2001 From: ubeddulla Date: Tue, 30 Jun 2026 16:11:42 +0530 Subject: [PATCH] fix(sbom): escape dots in spdx ids to avoid component collisions --- lib/utils/sbom-spdx.js | 7 +++++++ test/lib/utils/sbom-spdx.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/utils/sbom-spdx.js b/lib/utils/sbom-spdx.js index 88797fd03effb..8b4693fcbbd0d 100644 --- a/lib/utils/sbom-spdx.js +++ b/lib/utils/sbom-spdx.js @@ -188,6 +188,13 @@ const toSpdxID = (node) => { // Strip leading @ for scoped packages name = name.replace(/^@/, '') + // Escape literal dots before mapping the scope separator to a dot, so a + // scoped name and an unscoped name that only differ by `/` vs `.` (e.g. + // `@a/b` and `a.b`) don't collapse to the same SPDXID. A collision there + // would drop one of the two distinct packages from the document, since the + // identifier is used to dedupe components. + name = name.replace(/\./g, '..') + // Replace slashes with dots name = name.replace(/\//g, '.') diff --git a/test/lib/utils/sbom-spdx.js b/test/lib/utils/sbom-spdx.js index 1e21c945ca75d..4d9a458c78596 100644 --- a/test/lib/utils/sbom-spdx.js +++ b/test/lib/utils/sbom-spdx.js @@ -241,6 +241,35 @@ t.test('single node - linked', t => { t.end() }) +t.test('scoped and dotted unscoped deps get distinct SPDXIDs', t => { + // `@a/b` and `a.b` are different packages but both used to map to + // SPDXRef-Package-a.b-1.0.0, so the second collided with the first and was + // dropped from the document during component dedup. + const scoped = { + packageName: '@a/b', + version: '1.0.0', + pkgid: '@a/b@1.0.0', + package: {}, + location: 'node_modules/@a/b', + edgesOut: [], + } + const dotted = { + packageName: 'a.b', + version: '1.0.0', + pkgid: 'a.b@1.0.0', + package: {}, + location: 'node_modules/a.b', + edgesOut: [], + } + const node = { ...root, edgesOut: [{ to: scoped }, { to: dotted }] } + const res = spdxOutput({ npm, nodes: [node, scoped, dotted] }) + const ids = res.packages.map(p => p.SPDXID) + t.equal(res.packages.length, 3, 'both deps and the root are present') + t.equal(new Set(ids).size, ids.length, 'every package has a unique SPDXID') + t.equal(ids.filter(id => id === 'SPDXRef-Package-a.b-1.0.0').length, 1) + t.end() +}) + t.test('node - with deps', t => { const node = { ...root, edgesOut: [