chore: Standardize license metadata across all first-party packages and tighten SBOM pipeline (no-changelog) (#31880)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Declan Carroll
2026-06-08 11:19:32 +01:00
committed by GitHub
parent 0b3890605b
commit 29735c7a04
67 changed files with 377 additions and 116 deletions
+12 -6
View File
@@ -19,7 +19,10 @@ const REPO_ROOT = path.resolve(scriptDir, '..', '..');
const CDXGEN = path.join(scriptDir, 'node_modules', '.bin', 'cdxgen');
const ENRICH = path.join(REPO_ROOT, 'scripts', 'licenses', 'enrich-sbom.mjs');
const CHECK = path.join(REPO_ROOT, 'scripts', 'licenses', 'check-sbom-licenses.mjs');
const ALLOW_REF = '--allow-ref=LicenseRef-n8n-sustainable-use';
const ALLOW_REFS = [
'--allow-ref=LicenseRef-n8n-sustainable-use',
'--allow-ref=LicenseRef-n8n-enterprise',
];
export function parseTargets(env) {
return [
@@ -43,12 +46,15 @@ function attest({ label, image, digest }) {
// Pull the (host-arch) image and scan its filesystem: OS packages + npm.
run('docker', ['pull', ref]);
// FETCH_LICENSE=true would make cdxgen call the npm registry for every package
// to resolve missing license data. In practice it resolves nothing — packages
// without a license field in their tarball also have no license in the registry —
// and adds hundreds of sequential HTTP requests. License gaps are covered by
// enrich-sbom.mjs (license-overrides.json + first-party detection) below.
run(
CDXGEN,
['-t', 'docker', '--no-install-deps', '--profile', 'license-compliance', '-o', out, ref],
{
FETCH_LICENSE: 'true',
},
['-t', 'docker', '--no-install-deps', '--profile', 'license-compliance', '--spec-version', '1.6', '-o', out, ref],
{ CDXGEN_NO_BANNER: '1' },
);
// Resolve first-party + override licenses (lenient: this image holds only a
@@ -58,7 +64,7 @@ function attest({ label, image, digest }) {
// Release-blocking gate, scoped to npm — OS packages carry upstream-distro
// license strings we don't control, so they're inventoried but not gated.
run(process.execPath, [CHECK, out, ALLOW_REF, '--enforce-prefix=pkg:npm/']);
run(process.execPath, [CHECK, out, ...ALLOW_REFS, '--enforce-prefix=pkg:npm/']);
run('cosign', ['attest', '--yes', '--type', 'cyclonedx', '--predicate', out, ref]);
console.log('::endgroup::');
+2 -2
View File
@@ -2,10 +2,10 @@
"name": "workflow-scripts",
"scripts": {
"test": "node --test --experimental-test-module-mocks ./*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs ./stale/*.test.mjs ../../scripts/licenses/*.test.mjs",
"generate-sbom": "FETCH_LICENSE=true cdxgen -t pnpm --no-install-deps --profile license-compliance -o ../../sbom-source.cdx.json ../../compiled/",
"generate-sbom": "FETCH_LICENSE=true cdxgen -t pnpm --no-install-deps --profile license-compliance --spec-version 1.6 -o ../../sbom-source.cdx.json ../../compiled/",
"enrich-sbom": "node ../../scripts/licenses/enrich-sbom.mjs ../../sbom-source.cdx.json",
"render-licenses-md": "node ../../scripts/licenses/render-licenses-md.mjs ../../sbom-source.cdx.json ../../packages/cli/THIRD_PARTY_LICENSES.md ../../compiled/node_modules",
"check-licenses": "node ../../scripts/licenses/check-sbom-licenses.mjs ../../sbom-source.cdx.json --allow-ref=LicenseRef-n8n-sustainable-use",
"check-licenses": "node ../../scripts/licenses/check-sbom-licenses.mjs ../../sbom-source.cdx.json --allow-ref=LicenseRef-n8n-sustainable-use --allow-ref=LicenseRef-n8n-enterprise",
"generate-licenses": "pnpm generate-sbom && pnpm enrich-sbom && pnpm render-licenses-md && pnpm check-licenses"
},
"dependencies": {
@@ -64,7 +64,7 @@ jobs:
run: |
node scripts/licenses/check-sbom-licenses.mjs \
sbom-source.cdx.json \
--allow-ref=LicenseRef-n8n-sustainable-use
--allow-ref=LicenseRef-n8n-sustainable-use --allow-ref=LicenseRef-n8n-enterprise
- name: Attest SBOM for release
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
+2 -1
View File
@@ -99,5 +99,6 @@
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -38,5 +38,6 @@
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -109,5 +109,6 @@
},
"peerDependencies": {
"n8n-workflow": "*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -92,5 +92,6 @@
"p-limit": "^3.1.0",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -38,5 +38,6 @@
},
"peerDependencies": {
"zod": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -45,5 +45,6 @@
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"zod": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -36,5 +36,6 @@
},
"devDependencies": {
"@n8n/typescript-config": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -53,5 +53,6 @@
"bin": "n8n-benchmark",
"commands": "./dist/commands",
"topicSeparator": " "
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -30,5 +30,6 @@
},
"dependencies": {
"@n8n/api-types": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@n8n/cli",
"version": "0.6.0",
"description": "[beta] Client CLI for n8n — manage workflows, executions, and credentials from the terminal",
"license": "SEE LICENSE IN LICENSE.md",
"license": "LicenseRef-n8n-sustainable-use",
"bin": {
"n8n-cli": "bin/n8n-cli.mjs"
},
+2 -1
View File
@@ -31,5 +31,6 @@
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"@types/yargs-parser": "21.0.0",
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -34,5 +34,6 @@
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -22,5 +22,6 @@
],
"devDependencies": {
"@n8n/typescript-config": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -39,5 +39,6 @@
"@n8n/vitest-config": "workspace:*",
"vitest": "catalog:",
"vitest-websocket-mock": "^0.5.0"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -21,5 +21,6 @@
},
"devDependencies": {
"@n8n/typescript-config": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -54,5 +54,6 @@
"typescript": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -37,5 +37,6 @@
"@n8n/permissions": "workspace:*",
"lodash": "catalog:",
"n8n-workflow": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -31,5 +31,6 @@
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"@n8n/typescript-config": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -36,5 +36,6 @@
"@types/supertest": "^6.0.3",
"supertest": "^7.1.1",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -27,5 +27,6 @@
},
"dependencies": {
"callsites": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -55,5 +55,6 @@
},
"peerDependencies": {
"eslint": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -59,5 +59,6 @@
"☑️"
]
]
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -31,7 +31,7 @@
"web-worker",
"security"
],
"license": "SEE LICENSE IN LICENSE.md",
"license": "LicenseRef-n8n-sustainable-use",
"dependencies": {
"@n8n/errors": "workspace:*",
"@n8n/tournament": "workspace:*",
+1 -1
View File
@@ -58,7 +58,7 @@
"zod-to-json-schema": "catalog:",
"tsx": "catalog:"
},
"license": "https://docs.n8n.io/sustainable-use-license/",
"license": "LicenseRef-n8n-sustainable-use",
"dependencies": {
"zod": "catalog:"
}
+2 -1
View File
@@ -35,5 +35,6 @@
"@types/quoted-printable": "^1.0.2",
"@types/utf8": "^3.0.3",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -93,5 +93,6 @@
"tsx": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -38,5 +38,6 @@
"electron-builder": "^26.9.0",
"rimraf": "catalog:",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -60,5 +60,6 @@
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -31,5 +31,6 @@
"vite": "catalog:",
"vite-svg-loader": "catalog:frontend",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -46,5 +46,6 @@
"@types/yargs-parser": "21.0.0",
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -75,5 +75,6 @@
},
"peerDependencies": {
"eslint": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -311,5 +311,6 @@
"weaviate-client": "3.9.0",
"zod": "catalog:",
"zod-to-json-schema": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -31,5 +31,6 @@
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -2,7 +2,7 @@
"name": "@n8n/scan-community-package",
"version": "0.21.0",
"description": "Static code analyser for n8n community packages",
"license": "none",
"license": "LicenseRef-n8n-sustainable-use",
"bin": "scanner/cli.mjs",
"scripts": {
"test": "vitest run",
+2 -1
View File
@@ -44,5 +44,6 @@
},
"peerDependencies": {
"stylelint": ">= 16"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -32,5 +32,6 @@
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"get-port": "^7.1.0"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -57,5 +57,6 @@
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+1 -1
View File
@@ -15,5 +15,5 @@
"./tsconfig.frontend.json": "./tsconfig.frontend.json",
"./*": "./*"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
+1 -1
View File
@@ -55,5 +55,5 @@
"vite": "catalog:",
"vitest": "catalog:"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
+1 -1
View File
@@ -48,5 +48,5 @@
"format:check": "biome ci .",
"watch": "tsc -p tsconfig.build.json --watch"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -101,5 +101,6 @@
"n8n-workflow": "workspace:*",
"uuid": "catalog:",
"zod": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -235,5 +235,6 @@
"yargs-parser": "21.1.1",
"zod": "catalog:",
"zod-to-json-schema": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -91,5 +91,6 @@
"uuid": "catalog:",
"winston": "3.14.2",
"xml2js": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"vue": "catalog:frontend",
"vue-router": "catalog:frontend",
"vue-tsc": "catalog:frontend"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -66,5 +66,6 @@
"files": [
"README.md",
"dist"
]
],
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -49,5 +49,5 @@
"@vueuse/core": "catalog:frontend",
"vue": "catalog:frontend"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -86,5 +86,6 @@
},
"peerDependencies": {
"@vueuse/core": "*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+1 -1
View File
@@ -60,5 +60,5 @@
"peerDependencies": {
"vue": "catalog:frontend"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -58,5 +58,5 @@
"vite": "catalog:",
"vitest": "catalog:"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
+1 -1
View File
@@ -62,5 +62,5 @@
"pinia": "catalog:frontend",
"vue": "catalog:frontend"
},
"license": "See LICENSE.md file in the root of the repository"
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -51,5 +51,6 @@
"extends": [
"plugin:storybook/recommended"
]
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -154,5 +154,6 @@
"vitest-mock-extended": "catalog:",
"vue-tsc": "catalog:frontend",
"z-vue-scan": "^0.0.35"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -53,5 +53,6 @@
"n8n-workflow": "workspace:*",
"replace-in-file": "^6.0.0",
"tmp-promise": "^3.0.3"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -982,5 +982,6 @@
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz",
"xml2js": "catalog:",
"xmlhttprequest-ssl": "3.1.0"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -15,5 +15,6 @@
"@n8n/expression-runtime": "workspace:*",
"vitest": "catalog:",
"n8n-workflow": "workspace:*"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -83,5 +83,6 @@
"tsx": "catalog:",
"zod": "catalog:",
"vitest": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+2 -1
View File
@@ -81,5 +81,6 @@
},
"peerDependencies": {
"zod": "catalog:"
}
},
"license": "LicenseRef-n8n-sustainable-use"
}
+17
View File
@@ -191,6 +191,23 @@ if (excludeTestController) {
}
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --legacy deploy --no-optional ./compiled`;
// Strip test/example/benchmark dirs shipped inside production deps that lack a
// `files` field in their package.json. These are valid runtime deps but their
// authors published full source trees; syft inventories the subdirs as phantom
// packages with no license, which fails enterprise SBOM license gates.
echo(chalk.yellow('INFO: Stripping test/example/benchmark dirs from production closure...'));
const phantomDirs = [
'resolve/*/test',
'import-in-the-middle/*/test',
'github-from-package/*/example',
'tedious/*/benchmarks',
];
for (const pattern of phantomDirs) {
await $`find ${config.compiledAppDir}/node_modules/.pnpm -type d -path "*/${pattern}" -exec rm -rf {} + 2>/dev/null || true`;
}
echo(chalk.green('✅ Phantom dirs stripped'));
await fs.ensureDir(config.compiledTaskRunnerDir);
echo(
@@ -201,6 +201,36 @@
]
}
},
{
"type": "library",
"group": "",
"name": "wa-sqlite",
"version": "1.0.9",
"scope": "optional",
"purl": "pkg:npm/wa-sqlite@1.0.9",
"properties": [
{
"name": "SrcFile",
"value": "/home/runner/work/n8n/n8n/compiled/node_modules/wa-sqlite/package.json"
}
],
"evidence": {
"identity": [
{
"field": "purl",
"confidence": 0.7,
"methods": [
{
"technique": "manifest-analysis",
"confidence": 0.7,
"value": "/home/runner/work/n8n/n8n/compiled/node_modules/wa-sqlite/package.json"
}
],
"concludedValue": "/home/runner/work/n8n/n8n/compiled/node_modules/wa-sqlite/package.json"
}
]
}
},
{
"type": "library",
"group": "@n8n",
+15
View File
@@ -1,6 +1,11 @@
{
"_comment": "Hand-resolved licenses for packages cdxgen + FETCH_LICENSE cannot resolve. 'overrides' are PURL-pinned (pkg:npm/<name>@<version>, exact match) and drive the release-closure SBOM — a pin that stops matching fails loudly so the license is re-verified on the bump. 'byName' is version-agnostic (keyed by package name) for licenses stable across versions; it resolves the same package at whatever version a container image installed (e.g. ssh2 ships both 1.15.0 and 1.16.0 in the image, both MIT). 'elections' record which license n8n elects for a validly dual-licensed (OR) dependency so a copyleft policy gate reads the elected term. 'source' records where each was verified. Optional 'skipDiskText: true' opts out of on-disk LICENSE text lookup when the file disagrees with the overridden id.",
"overrides": {
"pkg:npm/wa-sqlite@1.0.9": {
"license": "MIT",
"source": "https://github.com/rhashimoto/wa-sqlite — LICENSE file in published tarball confirms MIT. Package is installed via GitHub tarball URL so npm registry metadata is absent; no license field in package.json.",
"skipDiskText": true
},
"pkg:npm/nub@0.0.0": {
"license": "MIT",
"source": "https://www.npmjs.com/package/nub — package.json declares non-SPDX 'MIT/X11'; X11 is the historical alias for the MIT license. Normalised to the canonical SPDX id."
@@ -58,6 +63,16 @@
"ssh2": {
"license": "MIT",
"source": "compiled/node_modules/ssh2/LICENSE — MIT; package.json uses a legacy licenses[] array so cdxgen leaves it unresolved. Version-agnostic: a container image can install more than one ssh2 (e.g. 1.15.0 and 1.16.0 side by side), and the license is MIT across versions."
},
"@n8n_io/license-sdk": {
"license": "LicenseRef-n8n-enterprise",
"source": "n8n-io/license-management — ships LICENSE_EE.md (n8n Enterprise License). EE-only runtime component; not under the Sustainable Use License. Version-agnostic: license is stable across SDK versions. FIRST_PARTY_PATTERNS would otherwise incorrectly stamp it as LicenseRef-n8n-sustainable-use.",
"skipDiskText": true
},
"@n8n_io/ai-assistant-sdk": {
"license": "LicenseRef-n8n-enterprise",
"source": "n8n-io/ai-assistant-service — ships LICENSE_EE.md (n8n Enterprise License). EE-only runtime component; not under the Sustainable Use License. Version-agnostic: license is stable across SDK versions.",
"skipDiskText": true
}
},
"elections": {
+6 -3
View File
@@ -19,7 +19,10 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE = path.join(scriptDir, '__fixtures__', 'sample.cdx.json');
const ENRICH = path.join(scriptDir, 'enrich-sbom.mjs');
const CHECK = path.join(scriptDir, 'check-sbom-licenses.mjs');
const ALLOW_REF = '--allow-ref=LicenseRef-n8n-sustainable-use';
const ALLOW_REFS = [
'--allow-ref=LicenseRef-n8n-sustainable-use',
'--allow-ref=LicenseRef-n8n-enterprise',
];
const run = (script, args) => spawnSync(process.execPath, [script, ...args], { encoding: 'utf-8' });
@@ -36,7 +39,7 @@ describe('license-generation chain (real CLIs end-to-end)', () => {
});
it('rejects the raw (un-enriched) SBOM at the gate (exit 1)', () => {
const r = run(CHECK, [raw, ALLOW_REF]);
const r = run(CHECK, [raw, ...ALLOW_REFS]);
assert.equal(r.status, 1, r.stderr);
assert.match(r.stderr, /binascii/); // empty-license component is named
});
@@ -66,7 +69,7 @@ describe('license-generation chain (real CLIs end-to-end)', () => {
});
it('passes the gate on the enriched SBOM (exit 0, dual-license warning only)', () => {
const r = run(CHECK, [enriched, ALLOW_REF]);
const r = run(CHECK, [enriched, ...ALLOW_REFS]);
assert.equal(r.status, 0, r.stderr);
assert.match(r.stderr, /jszip/); // surfaced as a dual-license warning, not a failure
});
@@ -374,6 +374,7 @@ describe('renderSbom — edge cases', () => {
'pkg:npm/nub@0.0.0',
'pkg:npm/xml-escape@1.1.0',
'pkg:npm/duck@0.1.12',
'pkg:npm/wa-sqlite@1.0.9',
];
const sbom = {
components: purls.map((purl) => {
+142
View File
@@ -0,0 +1,142 @@
# Software Composition Analysis (SCA)
## Posture
Every component in the enriched release SBOM carries a valid SPDX license
identifier. The two dual-licensed packages in the tree (`jszip`, `mailsplit`)
offer MIT as an alternative to their copyleft option; n8n elects MIT for both,
recorded as `cdx:license:elected` in the SBOM. No copyleft license is in force.
---
## License picture
| Scope | License | Notes |
|---|---|---|
| `@n8n/*`, `n8n`, `n8n-core`, `n8n-nodes-base`, `n8n-workflow`, `n8n-editor-ui` | `LicenseRef-n8n-sustainable-use` | Full text at https://docs.n8n.io/sustainable-use-license/ |
| Community tooling, codemirror extensions | `MIT` / `Apache-2.0` / `ISC` | Intentionally OSI-licensed |
| `@n8n_io/license-sdk`, `@n8n_io/ai-assistant-sdk` | `LicenseRef-n8n-enterprise` | EE-only runtime components; require enterprise contract |
| All third-party npm dependencies | Permissive OSI | No copyleft; dual-licensed packages elect MIT |
A human-readable rendering is at `/rest/third-party-licenses` on any running
n8n instance and as `THIRD_PARTY_LICENSES.md` attached to each GitHub release.
---
## SBOM pipelines
### Release SBOM (authoritative)
Produced by `sbom-generation-callable.yml` on every release. This is the
artifact to use for compliance review.
```
pnpm build:deploy (N8N_GENERATE_LICENSES=true)
└─ cdxgen → sbom-source.cdx.json
└─ enrich-sbom.mjs → resolves first-party + override licenses
└─ check-sbom-licenses.mjs → SPDX gate (release-blocking)
└─ actions/attest → signed attestation against package.json
└─ gh release upload
```
### Docker image SBOM
Produced by the `sbom-attestation` job in `docker-build-push.yml` on
`stable`/`rc`/`nightly` builds.
```
docker push
└─ cdxgen -t docker → OS + npm scan of the pushed image
└─ enrich-sbom.mjs → resolves licenses
└─ check-sbom-licenses.mjs → SPDX gate (npm only)
└─ cosign attest → attested to image digest
```
---
## Verifying the SBOM
The enriched, attested SBOM is attached to every published Docker image via
cosign. Pull it once and run all checks against the file — this gives the
enriched picture with 0 unlicensed and 0 license failures.
```bash
# Pull the attested SBOM from any published image
cosign download attestation ghcr.io/n8n-io/n8n:<version> \
--predicate-type https://cyclonedx.org/bom \
| jq -r '.payload' | base64 -d | jq '.predicate' > sbom.cdx.json
# Verify it was produced by n8n's CI (not tampered with)
cosign verify-attestation ghcr.io/n8n-io/n8n:<version> \
--type cyclonedx \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/n8n-io/n8n/.github/workflows/"
# Check for unlicensed packages — expect 0
grant check --unlicensed sbom.cdx.json
# Full license list
grant list sbom.cdx.json
# n8n's SPDX gate — expect 0 failures
node scripts/licenses/check-sbom-licenses.mjs sbom.cdx.json \
--allow-ref=LicenseRef-n8n-sustainable-use \
--allow-ref=LicenseRef-n8n-enterprise \
--enforce-prefix=pkg:npm/
# Vulnerability scan
grype sbom:sbom.cdx.json
# Full audit — vulnerabilities + licenses
trivy sbom sbom.cdx.json
```
Replace `<version>` with `nightly`, `latest`, or a specific version tag
(e.g. `n8n@2.25.0`). The same image is available on both
`ghcr.io/n8n-io/n8n` and `docker.io/n8nio/n8n`.
---
## Copyleft explainer
The Docker image SBOM will show GPL/LGPL entries in `grant list`. These come
entirely from Alpine OS system packages (`busybox`, `git`, `libgcc`,
`libstdc++`, etc.). GPL in an OS binary has no effect on n8n's licensing
obligations or your use of n8n; they are inventoried in the SBOM for
completeness but are not gated by the license pipeline.
The npm layer contains no copyleft in force. The two dual-licensed packages
(`jszip`: MIT OR GPL-3.0-or-later, `mailsplit`: MIT OR EUPL-1.1+) elect MIT;
this election is recorded as `cdx:license:elected` in the SBOM.
---
## Release SBOM
For source-level compliance review, download from the GitHub release page:
```bash
gh release download n8n@<version> \
--repo n8n-io/n8n \
--pattern sbom-source.cdx.json
gh attestation verify sbom-source.cdx.json \
--repo n8n-io/n8n \
--owner n8n-io
```
---
## Tooling
| Tool | Role |
|---|---|
| cdxgen | SBOM generation (CycloneDX 1.6) |
| enrich-sbom.mjs | License enrichment (`scripts/licenses/`) |
| check-sbom-licenses.mjs | SPDX compliance gate (`scripts/licenses/`) |
| grant | License listing and unlicensed check |
| grype | Vulnerability scanning against SBOM |
| trivy | Full audit — vulnerabilities + licenses |
| cosign / actions/attest | SBOM attestation |
See `security/vex.openvex.json` for the VEX document attested alongside the image.