Visitar URL original
fix(sbom): dedupe per-node dependsOn / relationships (#9318) · npm/cli@6c17544 · GitHub
Skip to content

Commit 6c17544

Browse files
fix(sbom): dedupe per-node dependsOn / relationships (#9318)
Backport of #9311 to `release/v11`. Co-authored-by: Mikael Kristiansson <1733784+mikaelkristiansson@users.noreply.github.com>
1 parent c3ea2cf commit 6c17544

6 files changed

Lines changed: 190 additions & 4 deletions

File tree

‎lib/utils/sbom-cyclonedx.js‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,20 @@ const toCyclonedxItem = (node, { packageType }) => {
170170
}
171171

172172
const toCyclonedxDependency = (node, nodes) => {
173-
return {
174-
ref: toCyclonedxID(node),
175-
dependsOn: [...node.edgesOut.values()]
173+
// A node can have multiple outgoing edges resolving to the same
174+
// `name@version` (e.g. via npm aliases like `foo: npm:bar@1` alongside a
175+
// direct `bar: ^1` dep), which would produce duplicate entries in
176+
// `dependsOn`. CycloneDX 1.5 requires unique items, so dedupe by ref.
177+
const dependsOn = [...new Set(
178+
[...node.edgesOut.values()]
176179
// Filter out edges that are linking to nodes not in the list
177180
.filter(edge => nodes.find(n => n === edge.to))
178181
.map(edge => toCyclonedxID(edge.to))
179-
.filter(id => id),
182+
.filter(id => id)
183+
)]
184+
return {
185+
ref: toCyclonedxID(node),
186+
dependsOn,
180187
}
181188
}
182189

‎lib/utils/sbom-spdx.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,23 @@ const spdxOutput = ({ npm, nodes, packageType }) => {
4848
}
4949
seen.add(node)
5050

51+
// A node can have multiple outgoing edges resolving to the same
52+
// `name@version` of the same edge type (e.g. via npm aliases), which
53+
// would produce identical relationship triples. Dedupe per source node.
54+
const seenRels = new Set()
5155
const rels = [...node.edgesOut.values()]
5256
// Filter out edges that are linking to nodes not in the list
5357
.filter(edge => nodes.find(n => n === edge.to))
5458
.map(edge => toSpdxRelationship(node, edge))
5559
.filter(rel => rel)
60+
.filter(rel => {
61+
const key = `${rel.spdxElementId}|${rel.relatedSpdxElement}|${rel.relationshipType}`
62+
if (seenRels.has(key)) {
63+
return false
64+
}
65+
seenRels.add(key)
66+
return true
67+
})
5668

5769
relationships.push(...rels)
5870
}

‎tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,66 @@ exports[`test/lib/utils/sbom-cyclonedx.js TAP node - with duplicate deps > must
142142
}
143143
`
144144

145+
exports[`test/lib/utils/sbom-cyclonedx.js TAP node - with duplicate edges to same dep > must match snapshot 1`] = `
146+
{
147+
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
148+
"bomFormat": "CycloneDX",
149+
"specVersion": "1.5",
150+
"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000",
151+
"version": 1,
152+
"metadata": {
153+
"timestamp": "2020-01-01T00:00:00.000Z",
154+
"lifecycles": [
155+
{
156+
"phase": "build"
157+
}
158+
],
159+
"tools": [
160+
{
161+
"vendor": "npm",
162+
"name": "cli",
163+
"version": "10.0.0 "
164+
}
165+
],
166+
"component": {
167+
"bom-ref": "root@1.0.0",
168+
"type": "library",
169+
"name": "root",
170+
"version": "1.0.0",
171+
"scope": "required",
172+
"author": "Author",
173+
"purl": "pkg:npm/root@1.0.0",
174+
"properties": [],
175+
"externalReferences": []
176+
}
177+
},
178+
"components": [
179+
{
180+
"bom-ref": "dep1@0.0.1",
181+
"type": "library",
182+
"name": "dep1",
183+
"version": "0.0.1",
184+
"scope": "required",
185+
"purl": "pkg:npm/dep1@0.0.1",
186+
"properties": [],
187+
"externalReferences": []
188+
}
189+
],
190+
"dependencies": [
191+
{
192+
"ref": "root@1.0.0",
193+
"dependsOn": [
194+
"dep1@0.0.1"
195+
]
196+
},
197+
{
198+
"ref": "dep1@0.0.1",
199+
"dependsOn": []
200+
}
201+
]
202+
}
203+
`
204+
145205
exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - application package type > must match snapshot 1`] = `
146206
{
147207
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",

‎tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,73 @@ exports[`test/lib/utils/sbom-spdx.js TAP node - with duplicate deps > must match
271271
}
272272
`
273273

274+
exports[`test/lib/utils/sbom-spdx.js TAP node - with duplicate edges to same dep > must match snapshot 1`] = `
275+
{
276+
"spdxVersion": "SPDX-2.3",
277+
"dataLicense": "CC0-1.0",
278+
"SPDXID": "SPDXRef-DOCUMENT",
279+
"name": "root@1.0.0",
280+
"documentNamespace": "docns",
281+
"creationInfo": {
282+
"created": "2020-01-01T00:00:00.000Z",
283+
"creators": [
284+
"Tool: npm/cli-10.0.0 "
285+
]
286+
},
287+
"documentDescribes": [
288+
"SPDXRef-Package-root-1.0.0"
289+
],
290+
"packages": [
291+
{
292+
"name": "root",
293+
"SPDXID": "SPDXRef-Package-root-1.0.0",
294+
"versionInfo": "1.0.0",
295+
"packageFileName": "",
296+
"downloadLocation": "NOASSERTION",
297+
"filesAnalyzed": false,
298+
"homepage": "NOASSERTION",
299+
"licenseDeclared": "NOASSERTION",
300+
"externalRefs": [
301+
{
302+
"referenceCategory": "PACKAGE-MANAGER",
303+
"referenceType": "purl",
304+
"referenceLocator": "pkg:npm/root@1.0.0"
305+
}
306+
]
307+
},
308+
{
309+
"name": "dep1",
310+
"SPDXID": "SPDXRef-Package-dep1-0.0.1",
311+
"versionInfo": "0.0.1",
312+
"packageFileName": "node_modules/dep1",
313+
"downloadLocation": "NOASSERTION",
314+
"filesAnalyzed": false,
315+
"homepage": "NOASSERTION",
316+
"licenseDeclared": "NOASSERTION",
317+
"externalRefs": [
318+
{
319+
"referenceCategory": "PACKAGE-MANAGER",
320+
"referenceType": "purl",
321+
"referenceLocator": "pkg:npm/dep1@0.0.1"
322+
}
323+
]
324+
}
325+
],
326+
"relationships": [
327+
{
328+
"spdxElementId": "SPDXRef-DOCUMENT",
329+
"relatedSpdxElement": "SPDXRef-Package-root-1.0.0",
330+
"relationshipType": "DESCRIBES"
331+
},
332+
{
333+
"spdxElementId": "SPDXRef-Package-dep1-0.0.1",
334+
"relatedSpdxElement": "SPDXRef-Package-root-1.0.0",
335+
"relationshipType": "DEPENDENCY_OF"
336+
}
337+
]
338+
}
339+
`
340+
274341
exports[`test/lib/utils/sbom-spdx.js TAP single node - application package type > must match snapshot 1`] = `
275342
{
276343
"spdxVersion": "SPDX-2.3",

‎test/lib/utils/sbom-cyclonedx.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,25 @@ t.test('node - with duplicate deps', t => {
291291
t.end()
292292
})
293293

294+
t.test('node - with duplicate edges to same dep', t => {
295+
// A node can have multiple outgoing edges resolving to the same
296+
// `name@version` (e.g. a direct `dep1: ^1` plus an alias
297+
// `dep1-aliased: npm:dep1@^1`). The resulting `dependsOn` array must
298+
// still contain each ref at most once, since CycloneDX 1.5 requires
299+
// unique items.
300+
const node = {
301+
...root,
302+
edgesOut: [
303+
{ to: dep1 },
304+
{ to: dep1 },
305+
],
306+
}
307+
const res = cyclonedxOutput({ npm, nodes: [node, dep1] })
308+
t.same(res.dependencies[0].dependsOn, ['dep1@0.0.1'])
309+
t.matchSnapshot(JSON.stringify(res))
310+
t.end()
311+
})
312+
294313
// Check that all of the generated test snapshots validate against the CycloneDX schema
295314
t.test('schema validation', t => {
296315
// Load schemas

‎test/lib/utils/sbom-spdx.js‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,27 @@ t.test('node - with duplicate deps', t => {
256256
t.end()
257257
})
258258

259+
t.test('node - with duplicate edges to same dep', t => {
260+
// A node can have multiple outgoing edges resolving to the same
261+
// `name@version` of the same edge type (e.g. a direct `dep1: ^1` plus an
262+
// alias `dep1-aliased: npm:dep1@^1`). The resulting relationships must
263+
// still be unique per (source, target, type) triple.
264+
const node = { ...root,
265+
edgesOut: [
266+
{ to: dep1 },
267+
{ to: dep1 },
268+
] }
269+
const res = spdxOutput({ npm, nodes: [node, dep1] })
270+
const depRels = res.relationships.filter(
271+
r => r.spdxElementId === 'SPDXRef-Package-dep1-0.0.1'
272+
&& r.relatedSpdxElement === 'SPDXRef-Package-root-1.0.0'
273+
&& r.relationshipType === 'DEPENDENCY_OF'
274+
)
275+
t.equal(depRels.length, 1)
276+
t.matchSnapshot(JSON.stringify(res))
277+
t.end()
278+
})
279+
259280
// Check that all of the generated test snapshots validate against the SPDX schema
260281
t.test('schema validation', t => {
261282
const ajv = new Ajv()

0 commit comments

Comments
 (0)