Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow CycloneDX 1.5 spec for SPDX license expressions #975

Merged
merged 9 commits into from
Apr 13, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ tmp/

# BOM generated by CDXGEN during local development
bom.json
bomresults

# Logs
logs
Expand Down
35 changes: 18 additions & 17 deletions binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 (
Expand Down
11 changes: 6 additions & 5 deletions evinser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
executeAtom,
getAllFiles,
getGradleCommand,
getMavenCommand
getMavenCommand,
getTimestamp
} from "./utils.js";
import { findCryptoAlgos } from "./cbomutils.js";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -1085,31 +1086,31 @@ 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")
});
}
if (dataFlowSlicesFile && fs.existsSync(dataFlowSlicesFile)) {
bomJson.annotations.push({
subjects: [bomJson.serialNumber],
annotator: { component: bomJson.metadata.tools.components[0] },
timestamp: new Date().toISOString(),
timestamp: getTimestamp(),
text: fs.readFileSync(dataFlowSlicesFile, "utf8")
});
}
if (reachablesSlicesFile && fs.existsSync(reachablesSlicesFile)) {
bomJson.annotations.push({
subjects: [bomJson.serialNumber],
annotator: { component: bomJson.metadata.tools.components[0] },
timestamp: new Date().toISOString(),
timestamp: getTimestamp(),
text: fs.readFileSync(reachablesSlicesFile, "utf8")
});
}
}
// 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) {
Expand Down
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ import {
parseSwiftJsonTree,
parseSwiftResolved,
parseYarnLock,
readZipEntry
readZipEntry,
getTimestamp
} from "./utils.js";
import {
collectEnvInfo,
Expand Down Expand Up @@ -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
Expand Down
73 changes: 70 additions & 3 deletions utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

return new Date().toISOString().split(".")[0] + "Z";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I think jsonschema date-time is wrong to leave out millseconds and move away from ISO8601 format.

}

/**
* 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'
Expand All @@ -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 (
Expand All @@ -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;
}
Expand All @@ -322,7 +389,7 @@ export function getLicenses(pkg) {
}
return licenseContent;
})
.map((l) => ({ license: l }));
);
} else {
const knownLicense = getKnownLicense(undefined, pkg);
if (knownLicense) {
Expand Down
40 changes: 40 additions & 0 deletions utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 3 additions & 0 deletions validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading