ci: Remove unnecessary license override from SBOM (#38893)

This commit is contained in:
Matsu
2026-09-17 06:51:30 +00:00
committed by GitHub
parent 386fedf257
commit 5227face30
6 changed files with 76 additions and 26 deletions
@@ -0,0 +1,25 @@
{
"_comment": "Frozen fixture config, paired with sample.cdx.json. It holds only what the behaviour suites (pipeline.test.mjs, properties.test.mjs) exercise and must NOT be kept in sync with the shipped scripts/licenses/license-overrides.json — that coupling is what made a dependency removal break these tests. The shipped file is validated by render-licenses-md.test.mjs instead.",
"overrides": {
"pkg:npm/[email protected]": {
"license": "MIT",
"source": "Fixture only. A PURL-pinned override for a component the fixture SBOM declares with no license at all."
}
},
"byName": {
"wa-sqlite": {
"license": "MIT",
"source": "Fixture only. The second license-less component in sample.cdx.json, resolved version-agnostically."
},
"ssh2": {
"license": "MIT",
"source": "Fixture only. The byName version-drift case in properties.test.mjs (SBOM version differs from any pin)."
}
},
"elections": {
"pkg:npm/[email protected]": {
"elected": "MIT",
"source": "Fixture only. The dual-license (MIT OR GPL-3.0-or-later) election case."
}
}
}
+13 -5
View File
@@ -26,8 +26,10 @@
* test/fixture/exports-subpath package.json) — only relevant for image scans.
*
* Usage: node enrich-sbom.mjs <sbom-path> [output-path] [--license-file=<path>]
* [--lenient-config] [--drop-phantom-npm]
* [--overrides=<path>] [--lenient-config]
* [--drop-phantom-npm]
* output-path defaults to <sbom-path> (in-place).
* --overrides defaults to license-overrides.json next to this script.
*/
import { readFile, writeFile, readdir } from 'node:fs/promises';
@@ -48,9 +50,9 @@ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.turbo', 'coverage']
export const FIRST_PARTY_LICENSE_REF = 'LicenseRef-n8n-sustainable-use';
export const ELECTED_PROPERTY = 'cdx:license:elected';
export async function loadLicenseConfig() {
export async function loadLicenseConfig(overridesPath = OVERRIDES_PATH) {
try {
const raw = await readFile(OVERRIDES_PATH, 'utf-8');
const raw = await readFile(overridesPath, 'utf-8');
const parsed = JSON.parse(raw);
return {
overrides: parsed.overrides ?? {},
@@ -290,6 +292,9 @@ async function main() {
const args = process.argv.slice(2);
const positional = args.filter((a) => !a.startsWith('--'));
const licenseFileArg = args.find((a) => a.startsWith('--license-file='));
// Lets a test drive the chain against a frozen fixture config instead of the
// shipped one, so a dependency change can't break a behaviour test.
const overridesArg = args.find((a) => a.startsWith('--overrides='));
// A per-image scan contains only the npm subset present in that image, so most
// overrides/elections won't match — that's expected, not a stale pin. Lenient
// mode warns instead of failing. The full release-closure run stays strict.
@@ -302,7 +307,7 @@ async function main() {
const sbomPath = positional[0];
if (!sbomPath) {
console.error(
'Usage: enrich-sbom.mjs <sbom-path> [output-path] [--license-file=<path>] [--lenient-config] [--drop-phantom-npm]',
'Usage: enrich-sbom.mjs <sbom-path> [output-path] [--license-file=<path>] [--overrides=<path>] [--lenient-config] [--drop-phantom-npm]',
);
process.exit(1);
}
@@ -310,9 +315,12 @@ async function main() {
const licenseFile = licenseFileArg
? licenseFileArg.slice('--license-file='.length)
: DEFAULT_LICENSE_FILE;
const overridesPath = overridesArg
? overridesArg.slice('--overrides='.length)
: OVERRIDES_PATH;
const sbom = JSON.parse(await readFile(sbomPath, 'utf-8'));
const { overrides, byName, elections } = await loadLicenseConfig();
const { overrides, byName, elections } = await loadLicenseConfig(overridesPath);
const validIds = await loadSpdxIds();
const firstPartyOsi = await buildFirstPartyOsiMap(DEFAULT_PACKAGES_DIR, validIds);
-4
View File
@@ -17,10 +17,6 @@
"license": "Python-2.0",
"source": "https://github.com/ewoudenberg/difflib.js — package.json declares legacy licenses[] array with PSF type, http://docs.python.org/license.html"
},
"pkg:npm/[email protected]": {
"license": "MIT",
"source": "compiled/node_modules/binascii/LICENSE — verbatim MIT, no license field in package.json"
},
"pkg:npm/[email protected]": {
"license": "MIT",
"source": "compiled/node_modules/busboy/LICENSE — package.json uses legacy licenses[] array"
+24 -4
View File
@@ -1,7 +1,7 @@
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtemp, rm, readFile, copyFile } from 'node:fs/promises';
import { mkdtemp, rm, readFile, copyFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
@@ -14,9 +14,14 @@ import { fileURLToPath } from 'node:url';
// change that makes a downstream stage mis-parse. (render-licenses-md is a
// full-closure renderer covered end-to-end by its own suite; it intentionally
// rejects a subset fixture via its unused-override check, so it's excluded here.)
//
// Both fixtures are frozen: the SBOM and the override config it needs are a
// matched pair, so the chain runs strict and no dependency change can break it.
// The *shipped* license-overrides.json is validated by render-licenses-md.test.mjs.
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE = path.join(scriptDir, '__fixtures__', 'sample.cdx.json');
const FIXTURE_OVERRIDES = path.join(scriptDir, '__fixtures__', 'license-overrides.json');
const ENRICH = path.join(scriptDir, 'enrich-sbom.mjs');
const CHECK = path.join(scriptDir, 'check-sbom-licenses.mjs');
const ALLOW_REFS = [
@@ -45,9 +50,9 @@ describe('license-generation chain (real CLIs end-to-end)', () => {
});
it('enriches the SBOM (exit 0) and writes valid JSON resolving every path', async () => {
// --lenient-config: the fixture is a representative subset, so overrides for
// packages not in it are expected-absent, not stale pins (same as an image scan).
const r = run(ENRICH, [raw, enriched, '--lenient-config']);
// No --lenient-config: every entry in the fixture config matches a fixture
// component, so the strict stale-config gate must stay quiet.
const r = run(ENRICH, [raw, enriched, `--overrides=${FIXTURE_OVERRIDES}`]);
assert.equal(r.status, 0, r.stderr);
const sbom = JSON.parse(await readFile(enriched, 'utf-8'));
@@ -73,4 +78,19 @@ describe('license-generation chain (real CLIs end-to-end)', () => {
assert.equal(r.status, 0, r.stderr);
assert.match(r.stderr, /jszip/); // surfaced as a dual-license warning, not a failure
});
it('fails on a stale override pin (exit 3) unless --lenient-config', async () => {
const stale = path.join(dir, 'stale-overrides.json');
await writeFile(
stale,
JSON.stringify({ overrides: { 'pkg:npm/[email protected]': { license: 'MIT' } } }),
);
const args = [raw, path.join(dir, 'stale.cdx.json'), `--overrides=${stale}`];
const strict = run(ENRICH, args);
assert.equal(strict.status, 3, strict.stderr);
assert.match(strict.stderr, /not-in-the-sbom/);
assert.equal(run(ENRICH, [...args, '--lenient-config']).status, 0);
});
});
+5 -2
View File
@@ -27,6 +27,9 @@ import { isFirstParty } from './render-licenses-md.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const PACKAGES_DIR = path.resolve(HERE, '..', '..', 'packages');
// Frozen fixture config, not the shipped one: these are rule assertions over the
// component classes below, so they must not move when a dependency does.
const FIXTURE_OVERRIDES = path.join(HERE, '__fixtures__', 'license-overrides.json');
const ALLOW = new Set([FIRST_PARTY_LICENSE_REF]);
const NPM = ['pkg:npm/'];
@@ -45,7 +48,7 @@ const cfg = () => ({
before(async () => {
spdxIds = await loadSpdxIds();
({ overrides, byName, elections } = await loadLicenseConfig());
({ overrides, byName, elections } = await loadLicenseConfig(FIXTURE_OVERRIDES));
firstPartyOsi = await buildFirstPartyOsiMap(PACKAGES_DIR, spdxIds);
});
@@ -125,7 +128,7 @@ const RESOLVABLE = [
licenses: [{ expression: 'SEE LICENSE IN LICENSE.md' }],
}, // first-party ref
{ group: '@n8n', name: 'tournament', version: '9.9.9', purl: 'pkg:npm/%40n8n/[email protected]' }, // first-party OSI (Apache-2.0)
{ name: 'binascii', version: '0.0.2', purl: 'pkg:npm/[email protected]' }, // purl-pinned override
{ name: 'binascii', version: '0.0.2', purl: 'pkg:npm/[email protected]' }, // purl-pinned override (fixture config)
{
name: 'ssh2',
version: '9.9.9',
+9 -11
View File
@@ -360,16 +360,15 @@ describe('renderSbom — edge cases', () => {
});
it('all documented overrides resolve to zero unresolved (end-to-end)', async () => {
const purls = [
'pkg:npm/%40ewoudenberg/[email protected]',
'pkg:npm/[email protected]',
'pkg:npm/[email protected]',
'pkg:npm/[email protected]',
'pkg:npm/[email protected]',
'pkg:npm/[email protected]',
'pkg:npm/[email protected]',
'pkg:npm/[email protected]',
];
// Derived from the shipped config, never hand-listed: a copy here would
// break on every override the repo adds or drops. This is the one suite
// that reads the real file — it asserts each documented override yields a
// resolvable license, and (via unusedOverrides) that the matcher still
// recognises every key shape, including percent-encoded scopes.
const overridesModule = await loadOverrides();
const purls = Object.keys(overridesModule);
assert.ok(purls.length > 0, 'expected at least one documented override');
const sbom = {
components: purls.map((purl) => {
const m = purl.match(/^pkg:npm\/(?:%40([^/]+)\/)?([^@]+)@(.+)$/);
@@ -383,7 +382,6 @@ describe('renderSbom — edge cases', () => {
}),
};
const overridesModule = await loadOverrides();
const { summary, unresolved, unusedOverrides } = await renderSbom(sbom, overridesModule);
assert.equal(unresolved.length, 0, `unresolved: ${unresolved.join(', ')}`);
assert.equal(summary.unresolved, 0);