diff --git a/.gitignore b/.gitignore index 3c460e0c1..13332ad0d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ tmp/ # BOM generated by CDXGEN during local development bom.json +bomresults # Logs logs diff --git a/binary.js b/binary.js index 63c23be22..3d5680634 100644 --- a/binary.js +++ b/binary.js @@ -12,7 +12,13 @@ import { import { basename, dirname, join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { PackageURL } from "packageurl-js"; -import { DEBUG_MODE, TIMEOUT_MS, findLicenseId } from "./utils.js"; +import { + DEBUG_MODE, + TIMEOUT_MS, + findLicenseId, + adjustLicenseInformation, + isSpdxLicenseExpression +} from "./utils.js"; import { URL, fileURLToPath } from "node:url"; @@ -572,33 +578,28 @@ export function getOSPackages(src) { comp.licenses.length ) { const newLicenses = []; - for (const alic of comp.licenses) { - if (alic.license.name) { - // Licenses array can either be made of expressions or id/name but not both - if ( - comp.licenses.length == 1 && - (alic.license.name.toUpperCase().includes(" AND ") || - alic.license.name.toUpperCase().includes(" OR ")) - ) { - newLicenses.push({ expression: alic.license.name }); + for (const aLic of comp.licenses) { + if (aLic.license.name) { + if (isSpdxLicenseExpression(aLic.license.name)) { + newLicenses.push({ expression: aLic.license.name }); } else { - const possibleId = findLicenseId(alic.license.name); - if (possibleId !== alic.license.name) { + const possibleId = findLicenseId(aLic.license.name); + if (possibleId !== aLic.license.name) { newLicenses.push({ license: { id: possibleId } }); } else { newLicenses.push({ - license: { name: alic.license.name } + license: { name: aLic.license.name } }); } } } else if ( - Object.keys(alic).length && - Object.keys(alic.license).length + Object.keys(aLic).length && + Object.keys(aLic.license).length ) { - newLicenses.push(alic); + newLicenses.push(aLic); } } - comp.licenses = newLicenses; + comp.licenses = adjustLicenseInformation(newLicenses); } // Fix hashes if ( diff --git a/evinser.js b/evinser.js index a920a8b32..a1036529a 100644 --- a/evinser.js +++ b/evinser.js @@ -5,7 +5,8 @@ import { executeAtom, getAllFiles, getGradleCommand, - getMavenCommand + getMavenCommand, + getTimestamp } from "./utils.js"; import { findCryptoAlgos } from "./cbomutils.js"; import { tmpdir } from "node:os"; @@ -1085,7 +1086,7 @@ export const createEvinseFile = (sliceArtefacts, options) => { bomJson.annotations.push({ subjects: [bomJson.serialNumber], annotator: { component: bomJson.metadata.tools.components[0] }, - timestamp: new Date().toISOString(), + timestamp: getTimestamp(), text: fs.readFileSync(usagesSlicesFile, "utf8") }); } @@ -1093,7 +1094,7 @@ export const createEvinseFile = (sliceArtefacts, options) => { bomJson.annotations.push({ subjects: [bomJson.serialNumber], annotator: { component: bomJson.metadata.tools.components[0] }, - timestamp: new Date().toISOString(), + timestamp: getTimestamp(), text: fs.readFileSync(dataFlowSlicesFile, "utf8") }); } @@ -1101,7 +1102,7 @@ export const createEvinseFile = (sliceArtefacts, options) => { bomJson.annotations.push({ subjects: [bomJson.serialNumber], annotator: { component: bomJson.metadata.tools.components[0] }, - timestamp: new Date().toISOString(), + timestamp: getTimestamp(), text: fs.readFileSync(reachablesSlicesFile, "utf8") }); } @@ -1109,7 +1110,7 @@ export const createEvinseFile = (sliceArtefacts, options) => { // Increment the version bomJson.version = (bomJson.version || 1) + 1; // Set the current timestamp to indicate this is newer - bomJson.metadata.timestamp = new Date().toISOString(); + bomJson.metadata.timestamp = getTimestamp(); delete bomJson.signature; fs.writeFileSync(evinseOutFile, JSON.stringify(bomJson, null, null)); if (occEvidencePresent || csEvidencePresent || servicesPresent) { diff --git a/index.js b/index.js index 650e965af..94cdf94ef 100644 --- a/index.js +++ b/index.js @@ -114,7 +114,8 @@ import { parseSwiftJsonTree, parseSwiftResolved, parseYarnLock, - readZipEntry + readZipEntry, + getTimestamp } from "./utils.js"; import { collectEnvInfo, @@ -479,7 +480,7 @@ function addMetadata(parentComponent = {}, options = {}) { const lifecycles = options.specVersion >= 1.5 ? addLifecyclesSection(options) : undefined; const metadata = { - timestamp: new Date().toISOString(), + timestamp: getTimestamp(), tools, authors, supplier: undefined diff --git a/utils.js b/utils.js index 24f57aeae..eb84f40d7 100644 --- a/utils.js +++ b/utils.js @@ -274,6 +274,71 @@ function toBase64(hexString) { return Buffer.from(hexString, "hex").toString("base64"); } +/** + * Return the current timestamp in YYYY-MM-DDTHH:MM:SSZ format. + * + * @returns {string} ISO formatted timestamp, without milliseconds. + */ +export function getTimestamp() { + return new Date().toISOString().split(".")[0] + "Z"; +} + +/** + * Method to determine if a license is a valid SPDX license expression + * + * @param {string} license License string + * @returns {boolean} true if the license is a valid SPDX license expression + * @see https://spdx.dev/learn/handling-license-info/ + **/ +export function isSpdxLicenseExpression(license) { + if (!license) { + return false; + } + + if (/[(\s]+/g.test(license)) { + return true; + } + + if (license.endsWith("+")) { + return true; // GPL-2.0+ means GPL-2.0 or any later version, at the licensee’s option. + } + + return false; +} + +/** + * Convert the array of licenses to a CycloneDX 1.5 compliant license array. + * This should return an array containing: + * - one or more SPDX license if no expression is present + * - the first license expression if at least one is present + * + * @param {Array} licenses Array of licenses + * @returns {Array} CycloneDX 1.5 compliant license array + */ +export function adjustLicenseInformation(licenses) { + if (!licenses || !Array.isArray(licenses)) { + return []; + } + + const expressions = licenses.filter((f) => { + return f.expression; + }); + if (expressions.length >= 1) { + if (expressions.length > 1) { + console.warn("multiple license expressions found", expressions); + } + return [{ expression: expressions[0].expression }]; + } else { + return licenses.map((l) => { + if (typeof l.license === "object") { + return l; + } else { + return { license: l }; + } + }); + } +} + /** * Performs a lookup + validation of the license specified in the * package. If the license is a valid SPDX license ID, set the 'id' @@ -286,8 +351,8 @@ export function getLicenses(pkg) { if (!Array.isArray(license)) { license = [license]; } - return license - .map((l) => { + return adjustLicenseInformation( + license.map((l) => { let licenseContent = {}; if (typeof l === "string" || l instanceof String) { if ( @@ -309,6 +374,8 @@ export function getLicenses(pkg) { licenseContent.name = "CUSTOM"; } licenseContent.url = l; + } else if (isSpdxLicenseExpression(l)) { + licenseContent.expression = l; } else { licenseContent.name = l; } @@ -322,7 +389,7 @@ export function getLicenses(pkg) { } return licenseContent; }) - .map((l) => ({ license: l })); + ); } else { const knownLicense = getKnownLicense(undefined, pkg); if (knownLicense) { diff --git a/utils.test.js b/utils.test.js index 866398cd1..20ff80641 100644 --- a/utils.test.js +++ b/utils.test.js @@ -2058,6 +2058,46 @@ test("get licenses", () => { } } ]); + + licenses = getLicenses({ + license: "GPL-2.0+" + }); + expect(licenses).toEqual([ + { + license: { + id: "GPL-2.0+", + url: "https://opensource.org/licenses/GPL-2.0+" + } + } + ]); + + licenses = getLicenses({ + license: "(MIT or Apache-2.0)" + }); + expect(licenses).toEqual([ + { + expression: "(MIT or Apache-2.0)" + } + ]); + + // In case this is not a known license in the current build but it is a valid SPDX license expression + licenses = getLicenses({ + license: "NOT-GPL-2.1+" + }); + expect(licenses).toEqual([ + { + expression: "NOT-GPL-2.1+" + } + ]); + + licenses = getLicenses({ + license: "GPL-3.0-only WITH Classpath-exception-2.0" + }); + expect(licenses).toEqual([ + { + expression: "GPL-3.0-only WITH Classpath-exception-2.0" + } + ]); }); test("parsePkgJson", async () => { diff --git a/validator.js b/validator.js index eba593687..a162a1f8b 100644 --- a/validator.js +++ b/validator.js @@ -50,6 +50,9 @@ export const validateBom = (bomJson) => { ); const isValid = validate(bomJson); if (!isValid) { + console.log( + `Schema validation failed for ${bomJson.metadata.component.name}` + ); console.log(validate.errors); return false; }