Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions lib/utils/sbom-cyclonedx.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,20 @@ const toCyclonedxItem = (node, { packageType }) => {
}

const toCyclonedxDependency = (node, nodes) => {
return {
ref: toCyclonedxID(node),
dependsOn: [...node.edgesOut.values()]
// A node can have multiple outgoing edges resolving to the same
// `name@version` (e.g. via npm aliases like `foo: npm:bar@1` alongside a
// direct `bar: ^1` dep), which would produce duplicate entries in
// `dependsOn`. CycloneDX 1.5 requires unique items, so dedupe by ref.
const dependsOn = [...new Set(
[...node.edgesOut.values()]
// Filter out edges that are linking to nodes not in the list
.filter(edge => nodes.find(n => n === edge.to))
.map(edge => toCyclonedxID(edge.to))
.filter(id => id),
.filter(id => id)
)]
return {
ref: toCyclonedxID(node),
dependsOn,
}
}

Expand Down
12 changes: 12 additions & 0 deletions lib/utils/sbom-spdx.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,23 @@ const spdxOutput = ({ npm, nodes, packageType }) => {
}
seen.add(node)

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

relationships.push(...rels)
}
Expand Down
60 changes: 60 additions & 0 deletions tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,66 @@ exports[`test/lib/utils/sbom-cyclonedx.js TAP node - with duplicate deps > must
}
`

exports[`test/lib/utils/sbom-cyclonedx.js TAP node - with duplicate edges to same dep > must match snapshot 1`] = `
{
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000",
"version": 1,
"metadata": {
"timestamp": "2020-01-01T00:00:00.000Z",
"lifecycles": [
{
"phase": "build"
}
],
"tools": [
{
"vendor": "npm",
"name": "cli",
"version": "10.0.0 "
}
],
"component": {
"bom-ref": "root@1.0.0",
"type": "library",
"name": "root",
"version": "1.0.0",
"scope": "required",
"author": "Author",
"purl": "pkg:npm/root@1.0.0",
"properties": [],
"externalReferences": []
}
},
"components": [
{
"bom-ref": "dep1@0.0.1",
"type": "library",
"name": "dep1",
"version": "0.0.1",
"scope": "required",
"purl": "pkg:npm/dep1@0.0.1",
"properties": [],
"externalReferences": []
}
],
"dependencies": [
{
"ref": "root@1.0.0",
"dependsOn": [
"dep1@0.0.1"
]
},
{
"ref": "dep1@0.0.1",
"dependsOn": []
}
]
}
`

exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - application package type > must match snapshot 1`] = `
{
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
Expand Down
67 changes: 67 additions & 0 deletions tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,73 @@ exports[`test/lib/utils/sbom-spdx.js TAP node - with duplicate deps > must match
}
`

exports[`test/lib/utils/sbom-spdx.js TAP node - with duplicate edges to same dep > must match snapshot 1`] = `
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "root@1.0.0",
"documentNamespace": "docns",
"creationInfo": {
"created": "2020-01-01T00:00:00.000Z",
"creators": [
"Tool: npm/cli-10.0.0 "
]
},
"documentDescribes": [
"SPDXRef-Package-root-1.0.0"
],
"packages": [
{
"name": "root",
"SPDXID": "SPDXRef-Package-root-1.0.0",
"versionInfo": "1.0.0",
"packageFileName": "",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"homepage": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/root@1.0.0"
}
]
},
{
"name": "dep1",
"SPDXID": "SPDXRef-Package-dep1-0.0.1",
"versionInfo": "0.0.1",
"packageFileName": "node_modules/dep1",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"homepage": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/dep1@0.0.1"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-Package-root-1.0.0",
"relationshipType": "DESCRIBES"
},
{
"spdxElementId": "SPDXRef-Package-dep1-0.0.1",
"relatedSpdxElement": "SPDXRef-Package-root-1.0.0",
"relationshipType": "DEPENDENCY_OF"
}
]
}
`

exports[`test/lib/utils/sbom-spdx.js TAP single node - application package type > must match snapshot 1`] = `
{
"spdxVersion": "SPDX-2.3",
Expand Down
19 changes: 19 additions & 0 deletions test/lib/utils/sbom-cyclonedx.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,25 @@ t.test('node - with duplicate deps', t => {
t.end()
})

t.test('node - with duplicate edges to same dep', t => {
// A node can have multiple outgoing edges resolving to the same
// `name@version` (e.g. a direct `dep1: ^1` plus an alias
// `dep1-aliased: npm:dep1@^1`). The resulting `dependsOn` array must
// still contain each ref at most once, since CycloneDX 1.5 requires
// unique items.
const node = {
...root,
edgesOut: [
{ to: dep1 },
{ to: dep1 },
],
}
const res = cyclonedxOutput({ npm, nodes: [node, dep1] })
t.same(res.dependencies[0].dependsOn, ['dep1@0.0.1'])
t.matchSnapshot(JSON.stringify(res))
t.end()
})

// Check that all of the generated test snapshots validate against the CycloneDX schema
t.test('schema validation', t => {
// Load schemas
Expand Down
21 changes: 21 additions & 0 deletions test/lib/utils/sbom-spdx.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,27 @@ t.test('node - with duplicate deps', t => {
t.end()
})

t.test('node - with duplicate edges to same dep', t => {
// A node can have multiple outgoing edges resolving to the same
// `name@version` of the same edge type (e.g. a direct `dep1: ^1` plus an
// alias `dep1-aliased: npm:dep1@^1`). The resulting relationships must
// still be unique per (source, target, type) triple.
const node = { ...root,
edgesOut: [
{ to: dep1 },
{ to: dep1 },
] }
const res = spdxOutput({ npm, nodes: [node, dep1] })
const depRels = res.relationships.filter(
r => r.spdxElementId === 'SPDXRef-Package-dep1-0.0.1'
&& r.relatedSpdxElement === 'SPDXRef-Package-root-1.0.0'
&& r.relationshipType === 'DEPENDENCY_OF'
)
t.equal(depRels.length, 1)
t.matchSnapshot(JSON.stringify(res))
t.end()
})

// Check that all of the generated test snapshots validate against the SPDX schema
t.test('schema validation', t => {
const ajv = new Ajv()
Expand Down
Loading