mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
ci: Remove unnecessary license override from SBOM (#38893)
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user