From e2134d55d0ee8e446c566337168758714df67c70 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Mon, 6 Nov 2023 09:27:45 +0000 Subject: [PATCH] Correctly handle license objects in SBOM generation As a means to resolve #6966, we can tweak the way we handle licenses, where receiving a license object, instead of license string, results in a malformed SPDX JSON SBOM. While working on this, it was noted that CycloneDX also needed to be amended, as it was omitting any license objects. Closes #6966. --- lib/utils/sbom-cyclonedx.js | 11 +++- lib/utils/sbom-spdx.js | 9 ++- .../test/lib/utils/sbom-cyclonedx.js.test.cjs | 55 +++++++++++++++++++ .../test/lib/utils/sbom-spdx.js.test.cjs | 45 +++++++++++++++ test/lib/utils/sbom-cyclonedx.js | 20 ++++++- test/lib/utils/sbom-spdx.js | 14 +++++ 6 files changed, 149 insertions(+), 5 deletions(-) diff --git a/lib/utils/sbom-cyclonedx.js b/lib/utils/sbom-cyclonedx.js index 3088068ad3b5f..0a340895bb3f4 100644 --- a/lib/utils/sbom-cyclonedx.js +++ b/lib/utils/sbom-cyclonedx.js @@ -86,7 +86,14 @@ const toCyclonedxItem = (node, { packageType }) => { let parsedLicense try { - parsedLicense = parseLicense(node.package?.license) + let license = node.package?.license + if (license) { + if (typeof license === 'object') { + license = license.type + } + } + + parsedLicense = parseLicense(license) } catch (err) { parsedLicense = null } @@ -152,7 +159,7 @@ const toCyclonedxItem = (node, { packageType }) => { // If license is a single SPDX license, use the license field if (parsedLicense?.license) { component.licenses = [{ license: { id: parsedLicense.license } }] - // If license is a conjunction, use the expression field + // If license is a conjunction, use the expression field } else if (parsedLicense?.conjunction) { component.licenses = [{ expression: node.package.license }] } diff --git a/lib/utils/sbom-spdx.js b/lib/utils/sbom-spdx.js index 890ee3310fa78..8c91147cb4102 100644 --- a/lib/utils/sbom-spdx.js +++ b/lib/utils/sbom-spdx.js @@ -93,6 +93,13 @@ const toSpdxItem = (node, { packageType }) => { location = node.linksIn.values().next().value.location } + let license = node.package?.license + if (license) { + if (typeof license === 'object') { + license = license.type + } + } + const pkg = { name: node.packageName, SPDXID: toSpdxID(node), @@ -103,7 +110,7 @@ const toSpdxItem = (node, { packageType }) => { downloadLocation: (node.isLink ? undefined : node.resolved) || NO_ASSERTION, filesAnalyzed: false, homepage: node.package?.homepage || NO_ASSERTION, - licenseDeclared: node.package?.license || NO_ASSERTION, + licenseDeclared: license || NO_ASSERTION, externalRefs: [ { referenceCategory: REF_CAT_PACKAGE_MANAGER, diff --git a/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs b/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs index 878dfd4be4705..7a8d79017f36a 100644 --- a/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs +++ b/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs @@ -912,6 +912,61 @@ exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - with license express } ` +exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - with license object > 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": [ + { + "name": "cdx:npm:package:path", + "value": "" + } + ], + "externalReferences": [], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + } + }, + "components": [], + "dependencies": [ + { + "ref": "root@1.0.0", + "dependsOn": [] + } + ] +} +` + exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - with repository url > must match snapshot 1`] = ` { "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", diff --git a/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs b/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs index ff69728168283..aeda27793a04f 100644 --- a/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs +++ b/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs @@ -550,6 +550,51 @@ exports[`test/lib/utils/sbom-spdx.js TAP single node - with license expression > } ` +exports[`test/lib/utils/sbom-spdx.js TAP single node - with license object > 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": "MIT", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/root@1.0.0" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Package-root-1.0.0", + "relationshipType": "DESCRIBES" + } + ] +} +` + exports[`test/lib/utils/sbom-spdx.js TAP single node - with single license > must match snapshot 1`] = ` { "spdxVersion": "SPDX-2.3", diff --git a/test/lib/utils/sbom-cyclonedx.js b/test/lib/utils/sbom-cyclonedx.js index 540feb9eb0ee3..a5c1e4ae816ae 100644 --- a/test/lib/utils/sbom-cyclonedx.js +++ b/test/lib/utils/sbom-cyclonedx.js @@ -190,6 +190,20 @@ t.test('single node - with license expression', t => { t.end() }) +t.test('single node - with license object', t => { + const pkg = { + ...rootPkg, + license: { + type: 'MIT', + url: 'http://github.com/kriskowal/q/raw/master/LICENSE', + }, + } + const node = { ...root, package: pkg } + const res = cyclonedxOutput({ npm, nodes: [node] }) + t.matchSnapshot(JSON.stringify(res)) + t.end() +}) + t.test('single node - from git url', t => { const node = { ...root, type: 'git', resolved: 'https://github.com/foo/bar#1234' } const res = cyclonedxOutput({ npm, nodes: [node] }) @@ -205,13 +219,15 @@ t.test('single node - no package info', t => { }) t.test('node - with deps', t => { - const node = { ...root, + const node = { + ...root, edgesOut: [ { to: dep1 }, { to: dep2 }, { to: undefined }, { to: { pkgid: 'foo' } }, - ] } + ] + } const res = cyclonedxOutput({ npm, nodes: [node, dep1, dep2, dep2Link] }) t.matchSnapshot(JSON.stringify(res)) t.end() diff --git a/test/lib/utils/sbom-spdx.js b/test/lib/utils/sbom-spdx.js index 1545596ea8812..d69e85667dc85 100644 --- a/test/lib/utils/sbom-spdx.js +++ b/test/lib/utils/sbom-spdx.js @@ -117,6 +117,20 @@ t.test('single node - with single license', t => { t.end() }) +t.test('single node - with license object', t => { + const pkg = { + ...rootPkg, + license: { + type: 'MIT', + url: 'http://github.com/kriskowal/q/raw/master/LICENSE', + }, + } + const node = { ...root, package: pkg } + const res = spdxOutput({ npm, nodes: [node] }) + t.matchSnapshot(JSON.stringify(res)) + t.end() +}) + t.test('single node - with license expression', t => { const pkg = { ...rootPkg, license: '(MIT OR Apache-2.0)' } const node = { ...root, package: pkg }