From 29735c7a04c366268c4b8aa375903f8a9f5e0a7a Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Mon, 8 Jun 2026 11:19:32 +0100 Subject: [PATCH] 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) --- .github/scripts/attest-image-sbom.mjs | 18 ++- .github/scripts/package.json | 4 +- .../workflows/sbom-generation-callable.yml | 2 +- packages/@n8n/agents/package.json | 3 +- packages/@n8n/ai-node-sdk/package.json | 3 +- packages/@n8n/ai-utilities/package.json | 3 +- .../@n8n/ai-workflow-builder.ee/package.json | 3 +- packages/@n8n/api-types/package.json | 3 +- packages/@n8n/backend-common/package.json | 3 +- packages/@n8n/backend-test-utils/package.json | 3 +- packages/@n8n/benchmark/package.json | 3 +- packages/@n8n/chat-hub/package.json | 3 +- packages/@n8n/cli/package.json | 2 +- packages/@n8n/client-oauth2/package.json | 3 +- packages/@n8n/computer-use/package.json | 3 +- packages/@n8n/config/package.json | 3 +- packages/@n8n/constants/package.json | 3 +- packages/@n8n/crdt/package.json | 3 +- packages/@n8n/create-node/package.json | 3 +- packages/@n8n/db/package.json | 3 +- packages/@n8n/decorators/package.json | 3 +- packages/@n8n/di/package.json | 3 +- packages/@n8n/engine/package.json | 3 +- packages/@n8n/errors/package.json | 3 +- packages/@n8n/eslint-config/package.json | 3 +- .../package.json | 3 +- packages/@n8n/expression-runtime/package.json | 2 +- packages/@n8n/extension-sdk/package.json | 2 +- packages/@n8n/imap/package.json | 3 +- packages/@n8n/instance-ai/package.json | 3 +- packages/@n8n/local-gateway/package.json | 3 +- packages/@n8n/mcp-apps/package.json | 3 +- .../@n8n/mcp-browser-extension/package.json | 67 +++++---- packages/@n8n/mcp-browser/package.json | 3 +- packages/@n8n/node-cli/package.json | 3 +- packages/@n8n/nodes-langchain/package.json | 3 +- packages/@n8n/permissions/package.json | 3 +- .../@n8n/scan-community-package/package.json | 2 +- packages/@n8n/stylelint-config/package.json | 3 +- packages/@n8n/syslog-client/package.json | 3 +- packages/@n8n/task-runner/package.json | 3 +- packages/@n8n/typescript-config/package.json | 2 +- packages/@n8n/utils/package.json | 2 +- packages/@n8n/vitest-config/package.json | 2 +- packages/@n8n/workflow-sdk/package.json | 3 +- packages/cli/package.json | 3 +- packages/core/package.json | 3 +- packages/extensions/insights/package.json | 3 +- packages/frontend/@n8n/chat/package.json | 3 +- .../frontend/@n8n/composables/package.json | 2 +- .../frontend/@n8n/design-system/package.json | 3 +- packages/frontend/@n8n/i18n/package.json | 2 +- .../@n8n/rest-api-client/package.json | 2 +- packages/frontend/@n8n/stores/package.json | 2 +- packages/frontend/@n8n/storybook/package.json | 3 +- packages/frontend/editor-ui/package.json | 3 +- packages/node-dev/package.json | 3 +- packages/nodes-base/package.json | 3 +- packages/testing/performance/package.json | 31 ++-- packages/testing/playwright/package.json | 3 +- packages/workflow/package.json | 3 +- scripts/build-n8n.mjs | 17 +++ scripts/licenses/__fixtures__/sample.cdx.json | 30 ++++ scripts/licenses/license-overrides.json | 15 ++ scripts/licenses/pipeline.test.mjs | 9 +- scripts/licenses/render-licenses-md.test.mjs | 1 + security/sca/README.md | 142 ++++++++++++++++++ 67 files changed, 377 insertions(+), 116 deletions(-) create mode 100644 security/sca/README.md diff --git a/.github/scripts/attest-image-sbom.mjs b/.github/scripts/attest-image-sbom.mjs index e4106c57ef3..186b741e8e8 100644 --- a/.github/scripts/attest-image-sbom.mjs +++ b/.github/scripts/attest-image-sbom.mjs @@ -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::'); diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 3f074727967..dfffaf3e912 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -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": { diff --git a/.github/workflows/sbom-generation-callable.yml b/.github/workflows/sbom-generation-callable.yml index 3104087eee5..2ac911c2c11 100644 --- a/.github/workflows/sbom-generation-callable.yml +++ b/.github/workflows/sbom-generation-callable.yml @@ -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 diff --git a/packages/@n8n/agents/package.json b/packages/@n8n/agents/package.json index 51803caa723..03a75e1cac9 100644 --- a/packages/@n8n/agents/package.json +++ b/packages/@n8n/agents/package.json @@ -99,5 +99,6 @@ "@vitest/coverage-v8": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/ai-node-sdk/package.json b/packages/@n8n/ai-node-sdk/package.json index 97383e79542..3c867321db3 100644 --- a/packages/@n8n/ai-node-sdk/package.json +++ b/packages/@n8n/ai-node-sdk/package.json @@ -38,5 +38,6 @@ "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", "vitest": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/ai-utilities/package.json b/packages/@n8n/ai-utilities/package.json index 75487d68fd1..4001fa4d57e 100644 --- a/packages/@n8n/ai-utilities/package.json +++ b/packages/@n8n/ai-utilities/package.json @@ -109,5 +109,6 @@ }, "peerDependencies": { "n8n-workflow": "*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/ai-workflow-builder.ee/package.json b/packages/@n8n/ai-workflow-builder.ee/package.json index 2eefc826a4c..88208b31048 100644 --- a/packages/@n8n/ai-workflow-builder.ee/package.json +++ b/packages/@n8n/ai-workflow-builder.ee/package.json @@ -92,5 +92,6 @@ "p-limit": "^3.1.0", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/api-types/package.json b/packages/@n8n/api-types/package.json index e8ed6bf90f3..6ad77e77c4b 100644 --- a/packages/@n8n/api-types/package.json +++ b/packages/@n8n/api-types/package.json @@ -38,5 +38,6 @@ }, "peerDependencies": { "zod": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/backend-common/package.json b/packages/@n8n/backend-common/package.json index c189c76c865..0b54b4f8184 100644 --- a/packages/@n8n/backend-common/package.json +++ b/packages/@n8n/backend-common/package.json @@ -45,5 +45,6 @@ "vitest": "catalog:", "vitest-mock-extended": "catalog:", "zod": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/backend-test-utils/package.json b/packages/@n8n/backend-test-utils/package.json index a5c8e051d28..7758deb2748 100644 --- a/packages/@n8n/backend-test-utils/package.json +++ b/packages/@n8n/backend-test-utils/package.json @@ -36,5 +36,6 @@ }, "devDependencies": { "@n8n/typescript-config": "workspace:*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index ee96c1ad11a..feeb98d0611 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -53,5 +53,6 @@ "bin": "n8n-benchmark", "commands": "./dist/commands", "topicSeparator": " " - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/chat-hub/package.json b/packages/@n8n/chat-hub/package.json index 60370c60e5c..54ef1355c3c 100644 --- a/packages/@n8n/chat-hub/package.json +++ b/packages/@n8n/chat-hub/package.json @@ -30,5 +30,6 @@ }, "dependencies": { "@n8n/api-types": "workspace:*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/cli/package.json b/packages/@n8n/cli/package.json index adc30713a14..6ce497a77f9 100644 --- a/packages/@n8n/cli/package.json +++ b/packages/@n8n/cli/package.json @@ -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" }, diff --git a/packages/@n8n/client-oauth2/package.json b/packages/@n8n/client-oauth2/package.json index 70df32a4c49..4909c025d2a 100644 --- a/packages/@n8n/client-oauth2/package.json +++ b/packages/@n8n/client-oauth2/package.json @@ -31,5 +31,6 @@ "@vitest/coverage-v8": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/computer-use/package.json b/packages/@n8n/computer-use/package.json index 777079ed5c9..82b6bde50ac 100644 --- a/packages/@n8n/computer-use/package.json +++ b/packages/@n8n/computer-use/package.json @@ -60,5 +60,6 @@ "@types/yargs-parser": "21.0.0", "@vitest/coverage-v8": "catalog:", "vitest": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index ca9f5227f90..2e2e0dda873 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -34,5 +34,6 @@ "@vitest/coverage-v8": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/constants/package.json b/packages/@n8n/constants/package.json index d6ee382a6aa..5478b7db19f 100644 --- a/packages/@n8n/constants/package.json +++ b/packages/@n8n/constants/package.json @@ -22,5 +22,6 @@ ], "devDependencies": { "@n8n/typescript-config": "workspace:*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/crdt/package.json b/packages/@n8n/crdt/package.json index 3a4c962d641..6af69360350 100644 --- a/packages/@n8n/crdt/package.json +++ b/packages/@n8n/crdt/package.json @@ -39,5 +39,6 @@ "@n8n/vitest-config": "workspace:*", "vitest": "catalog:", "vitest-websocket-mock": "^0.5.0" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/create-node/package.json b/packages/@n8n/create-node/package.json index 1ad5c1ef9f4..c5bb512666e 100644 --- a/packages/@n8n/create-node/package.json +++ b/packages/@n8n/create-node/package.json @@ -21,5 +21,6 @@ }, "devDependencies": { "@n8n/typescript-config": "workspace:*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/db/package.json b/packages/@n8n/db/package.json index 555b0c64b2f..0c30e439e53 100644 --- a/packages/@n8n/db/package.json +++ b/packages/@n8n/db/package.json @@ -54,5 +54,6 @@ "typescript": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/decorators/package.json b/packages/@n8n/decorators/package.json index fbb025e07a5..f5d35a066c5 100644 --- a/packages/@n8n/decorators/package.json +++ b/packages/@n8n/decorators/package.json @@ -37,5 +37,6 @@ "@n8n/permissions": "workspace:*", "lodash": "catalog:", "n8n-workflow": "workspace:*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/di/package.json b/packages/@n8n/di/package.json index fb65447aa80..226d31eec9e 100644 --- a/packages/@n8n/di/package.json +++ b/packages/@n8n/di/package.json @@ -31,5 +31,6 @@ "vitest": "catalog:", "vitest-mock-extended": "catalog:", "@n8n/typescript-config": "workspace:*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/engine/package.json b/packages/@n8n/engine/package.json index 1f2a47bf1f5..37473733314 100644 --- a/packages/@n8n/engine/package.json +++ b/packages/@n8n/engine/package.json @@ -36,5 +36,6 @@ "@types/supertest": "^6.0.3", "supertest": "^7.1.1", "vitest": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/errors/package.json b/packages/@n8n/errors/package.json index 63179fff041..f9715a62783 100644 --- a/packages/@n8n/errors/package.json +++ b/packages/@n8n/errors/package.json @@ -27,5 +27,6 @@ }, "dependencies": { "callsites": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/eslint-config/package.json b/packages/@n8n/eslint-config/package.json index 323a01ccb71..0b3be880cca 100644 --- a/packages/@n8n/eslint-config/package.json +++ b/packages/@n8n/eslint-config/package.json @@ -55,5 +55,6 @@ }, "peerDependencies": { "eslint": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/eslint-plugin-community-nodes/package.json b/packages/@n8n/eslint-plugin-community-nodes/package.json index d07c88e9c9c..ad9b8696ee4 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/package.json +++ b/packages/@n8n/eslint-plugin-community-nodes/package.json @@ -59,5 +59,6 @@ "☑️" ] ] - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/expression-runtime/package.json b/packages/@n8n/expression-runtime/package.json index 00ff82bbcd3..12ac0d4e187 100644 --- a/packages/@n8n/expression-runtime/package.json +++ b/packages/@n8n/expression-runtime/package.json @@ -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:*", diff --git a/packages/@n8n/extension-sdk/package.json b/packages/@n8n/extension-sdk/package.json index 50c45766e66..bd6f31e87e7 100644 --- a/packages/@n8n/extension-sdk/package.json +++ b/packages/@n8n/extension-sdk/package.json @@ -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:" } diff --git a/packages/@n8n/imap/package.json b/packages/@n8n/imap/package.json index 1b3952e7bc0..a2d67f119e9 100644 --- a/packages/@n8n/imap/package.json +++ b/packages/@n8n/imap/package.json @@ -35,5 +35,6 @@ "@types/quoted-printable": "^1.0.2", "@types/utf8": "^3.0.3", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/instance-ai/package.json b/packages/@n8n/instance-ai/package.json index 4510af94057..17889672da2 100644 --- a/packages/@n8n/instance-ai/package.json +++ b/packages/@n8n/instance-ai/package.json @@ -93,5 +93,6 @@ "tsx": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/local-gateway/package.json b/packages/@n8n/local-gateway/package.json index 289ee86e64e..942879873b3 100644 --- a/packages/@n8n/local-gateway/package.json +++ b/packages/@n8n/local-gateway/package.json @@ -38,5 +38,6 @@ "electron-builder": "^26.9.0", "rimraf": "catalog:", "vitest": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/mcp-apps/package.json b/packages/@n8n/mcp-apps/package.json index faeace3537b..60a03b59fa2 100644 --- a/packages/@n8n/mcp-apps/package.json +++ b/packages/@n8n/mcp-apps/package.json @@ -60,5 +60,6 @@ }, "peerDependencies": { "@modelcontextprotocol/sdk": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/mcp-browser-extension/package.json b/packages/@n8n/mcp-browser-extension/package.json index f215770cece..76da5f349ec 100644 --- a/packages/@n8n/mcp-browser-extension/package.json +++ b/packages/@n8n/mcp-browser-extension/package.json @@ -1,35 +1,36 @@ { - "name": "@n8n/mcp-browser-extension", - "version": "0.0.3", - "private": true, - "description": "Chrome extension that lets n8n AI control browser tabs via CDP", - "scripts": { - "clean": "rimraf dist .turbo", - "build": "vite build --config vite.sw.config.mts && vite build --config vite.ui.config.mts", - "build:unchecked": "pnpm run build", - "bundle": "pnpm build && node scripts/bundle.mjs", - "typecheck": "tsc --noEmit", - "lint": "eslint . --quiet", - "lint:fix": "eslint . --fix", - "test": "vitest run", - "test:unit": "vitest run", - "test:dev": "vitest", - "format": "biome format --write src", - "format:check": "biome ci src" - }, - "dependencies": { - "@n8n/design-system": "workspace:*", - "vue": "catalog:frontend" - }, - "devDependencies": { - "@n8n/typescript-config": "workspace:*", - "@n8n/vitest-config": "workspace:*", - "@types/chrome": "0.0.300", - "@vitejs/plugin-vue": "catalog:frontend", - "@vue/test-utils": "catalog:frontend", - "unplugin-icons": "catalog:frontend", - "vite": "catalog:", - "vite-svg-loader": "catalog:frontend", - "vitest": "catalog:" - } + "name": "@n8n/mcp-browser-extension", + "version": "0.0.3", + "private": true, + "description": "Chrome extension that lets n8n AI control browser tabs via CDP", + "scripts": { + "clean": "rimraf dist .turbo", + "build": "vite build --config vite.sw.config.mts && vite build --config vite.ui.config.mts", + "build:unchecked": "pnpm run build", + "bundle": "pnpm build && node scripts/bundle.mjs", + "typecheck": "tsc --noEmit", + "lint": "eslint . --quiet", + "lint:fix": "eslint . --fix", + "test": "vitest run", + "test:unit": "vitest run", + "test:dev": "vitest", + "format": "biome format --write src", + "format:check": "biome ci src" + }, + "dependencies": { + "@n8n/design-system": "workspace:*", + "vue": "catalog:frontend" + }, + "devDependencies": { + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", + "@types/chrome": "0.0.300", + "@vitejs/plugin-vue": "catalog:frontend", + "@vue/test-utils": "catalog:frontend", + "unplugin-icons": "catalog:frontend", + "vite": "catalog:", + "vite-svg-loader": "catalog:frontend", + "vitest": "catalog:" + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/mcp-browser/package.json b/packages/@n8n/mcp-browser/package.json index ac1857d049e..7eb1938f519 100644 --- a/packages/@n8n/mcp-browser/package.json +++ b/packages/@n8n/mcp-browser/package.json @@ -46,5 +46,6 @@ "@types/yargs-parser": "21.0.0", "@vitest/coverage-v8": "catalog:", "vitest": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/node-cli/package.json b/packages/@n8n/node-cli/package.json index 88e85482ba3..d935d951b1d 100644 --- a/packages/@n8n/node-cli/package.json +++ b/packages/@n8n/node-cli/package.json @@ -75,5 +75,6 @@ }, "peerDependencies": { "eslint": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 18b16d1ae3e..617056c13d1 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -311,5 +311,6 @@ "weaviate-client": "3.9.0", "zod": "catalog:", "zod-to-json-schema": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/permissions/package.json b/packages/@n8n/permissions/package.json index cb22149c724..a57234b17ee 100644 --- a/packages/@n8n/permissions/package.json +++ b/packages/@n8n/permissions/package.json @@ -31,5 +31,6 @@ "@vitest/coverage-v8": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/scan-community-package/package.json b/packages/@n8n/scan-community-package/package.json index f5e2656b41b..4c2224d1854 100644 --- a/packages/@n8n/scan-community-package/package.json +++ b/packages/@n8n/scan-community-package/package.json @@ -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", diff --git a/packages/@n8n/stylelint-config/package.json b/packages/@n8n/stylelint-config/package.json index 2818dd7248a..b55a43f68ac 100644 --- a/packages/@n8n/stylelint-config/package.json +++ b/packages/@n8n/stylelint-config/package.json @@ -44,5 +44,6 @@ }, "peerDependencies": { "stylelint": ">= 16" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/syslog-client/package.json b/packages/@n8n/syslog-client/package.json index c17fb3af933..29fbde6c91b 100644 --- a/packages/@n8n/syslog-client/package.json +++ b/packages/@n8n/syslog-client/package.json @@ -32,5 +32,6 @@ "vitest": "catalog:", "vitest-mock-extended": "catalog:", "get-port": "^7.1.0" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index 09d1c89a36a..d3e23a6f9a7 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -57,5 +57,6 @@ "@vitest/coverage-v8": "catalog:", "vitest": "catalog:", "vitest-mock-extended": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/typescript-config/package.json b/packages/@n8n/typescript-config/package.json index 23fd5d704ee..4bfa6c68ba2 100644 --- a/packages/@n8n/typescript-config/package.json +++ b/packages/@n8n/typescript-config/package.json @@ -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" } diff --git a/packages/@n8n/utils/package.json b/packages/@n8n/utils/package.json index 732ea50e33b..96f0ba101bb 100644 --- a/packages/@n8n/utils/package.json +++ b/packages/@n8n/utils/package.json @@ -55,5 +55,5 @@ "vite": "catalog:", "vitest": "catalog:" }, - "license": "See LICENSE.md file in the root of the repository" + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/@n8n/vitest-config/package.json b/packages/@n8n/vitest-config/package.json index 1b1bfc84a4a..1b0f17e12d5 100644 --- a/packages/@n8n/vitest-config/package.json +++ b/packages/@n8n/vitest-config/package.json @@ -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" } diff --git a/packages/@n8n/workflow-sdk/package.json b/packages/@n8n/workflow-sdk/package.json index c887584ed4e..1f34a7a4811 100644 --- a/packages/@n8n/workflow-sdk/package.json +++ b/packages/@n8n/workflow-sdk/package.json @@ -101,5 +101,6 @@ "n8n-workflow": "workspace:*", "uuid": "catalog:", "zod": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/cli/package.json b/packages/cli/package.json index 7f65a1a861d..3014ef8eb3b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -235,5 +235,6 @@ "yargs-parser": "21.1.1", "zod": "catalog:", "zod-to-json-schema": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/core/package.json b/packages/core/package.json index eb396713e07..c130027751b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,5 +91,6 @@ "uuid": "catalog:", "winston": "3.14.2", "xml2js": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/extensions/insights/package.json b/packages/extensions/insights/package.json index 9ffc1eeed67..9c069f4dfa6 100644 --- a/packages/extensions/insights/package.json +++ b/packages/extensions/insights/package.json @@ -60,5 +60,6 @@ "vue": "catalog:frontend", "vue-router": "catalog:frontend", "vue-tsc": "catalog:frontend" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/frontend/@n8n/chat/package.json b/packages/frontend/@n8n/chat/package.json index e7a7c0b8c73..46748017f29 100644 --- a/packages/frontend/@n8n/chat/package.json +++ b/packages/frontend/@n8n/chat/package.json @@ -66,5 +66,6 @@ "files": [ "README.md", "dist" - ] + ], + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/frontend/@n8n/composables/package.json b/packages/frontend/@n8n/composables/package.json index 0a984224174..84d0719e226 100644 --- a/packages/frontend/@n8n/composables/package.json +++ b/packages/frontend/@n8n/composables/package.json @@ -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" } diff --git a/packages/frontend/@n8n/design-system/package.json b/packages/frontend/@n8n/design-system/package.json index 19f7e173af9..d443d68fc92 100644 --- a/packages/frontend/@n8n/design-system/package.json +++ b/packages/frontend/@n8n/design-system/package.json @@ -86,5 +86,6 @@ }, "peerDependencies": { "@vueuse/core": "*" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/frontend/@n8n/i18n/package.json b/packages/frontend/@n8n/i18n/package.json index a40a74f3b53..5250f8a9668 100644 --- a/packages/frontend/@n8n/i18n/package.json +++ b/packages/frontend/@n8n/i18n/package.json @@ -60,5 +60,5 @@ "peerDependencies": { "vue": "catalog:frontend" }, - "license": "See LICENSE.md file in the root of the repository" + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/frontend/@n8n/rest-api-client/package.json b/packages/frontend/@n8n/rest-api-client/package.json index 60c163a1b35..24041e004f2 100644 --- a/packages/frontend/@n8n/rest-api-client/package.json +++ b/packages/frontend/@n8n/rest-api-client/package.json @@ -58,5 +58,5 @@ "vite": "catalog:", "vitest": "catalog:" }, - "license": "See LICENSE.md file in the root of the repository" + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/frontend/@n8n/stores/package.json b/packages/frontend/@n8n/stores/package.json index a349baabd76..6b9b21c4886 100644 --- a/packages/frontend/@n8n/stores/package.json +++ b/packages/frontend/@n8n/stores/package.json @@ -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" } diff --git a/packages/frontend/@n8n/storybook/package.json b/packages/frontend/@n8n/storybook/package.json index 3b5d078b303..1ac6303de09 100644 --- a/packages/frontend/@n8n/storybook/package.json +++ b/packages/frontend/@n8n/storybook/package.json @@ -51,5 +51,6 @@ "extends": [ "plugin:storybook/recommended" ] - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index f6748f38802..b8a71896a90 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -154,5 +154,6 @@ "vitest-mock-extended": "catalog:", "vue-tsc": "catalog:frontend", "z-vue-scan": "^0.0.35" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index a751dde6a2f..decfaf6675f 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -53,5 +53,6 @@ "n8n-workflow": "workspace:*", "replace-in-file": "^6.0.0", "tmp-promise": "^3.0.3" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b277ce40b7b..1a8d79fa60e 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -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" } diff --git a/packages/testing/performance/package.json b/packages/testing/performance/package.json index 0fbf5df9189..3364d0875e2 100644 --- a/packages/testing/performance/package.json +++ b/packages/testing/performance/package.json @@ -1,19 +1,20 @@ { - "name": "@n8n/performance", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "format": "biome format --write .", - "format:check": "biome ci .", - "bench": "vitest bench --run", - "bench:baseline": "node scripts/save-baseline.mjs", - "bench:compare": "vitest bench --run --outputJson ./profiles/benchmark-results.json && node scripts/check-regression.mjs" - }, - "devDependencies": { - "@codspeed/vitest-plugin": "^5.2.0", + "name": "@n8n/performance", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "format": "biome format --write .", + "format:check": "biome ci .", + "bench": "vitest bench --run", + "bench:baseline": "node scripts/save-baseline.mjs", + "bench:compare": "vitest bench --run --outputJson ./profiles/benchmark-results.json && node scripts/check-regression.mjs" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.2.0", "@n8n/expression-runtime": "workspace:*", "vitest": "catalog:", - "n8n-workflow": "workspace:*" - } + "n8n-workflow": "workspace:*" + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/testing/playwright/package.json b/packages/testing/playwright/package.json index 7589fa3a16e..cb09245280c 100644 --- a/packages/testing/playwright/package.json +++ b/packages/testing/playwright/package.json @@ -83,5 +83,6 @@ "tsx": "catalog:", "zod": "catalog:", "vitest": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/packages/workflow/package.json b/packages/workflow/package.json index f8a55274035..7e2e685997b 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -81,5 +81,6 @@ }, "peerDependencies": { "zod": "catalog:" - } + }, + "license": "LicenseRef-n8n-sustainable-use" } diff --git a/scripts/build-n8n.mjs b/scripts/build-n8n.mjs index 1e5a86afb9d..f42bc87f574 100755 --- a/scripts/build-n8n.mjs +++ b/scripts/build-n8n.mjs @@ -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( diff --git a/scripts/licenses/__fixtures__/sample.cdx.json b/scripts/licenses/__fixtures__/sample.cdx.json index 8c763083139..155d3215a97 100644 --- a/scripts/licenses/__fixtures__/sample.cdx.json +++ b/scripts/licenses/__fixtures__/sample.cdx.json @@ -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", diff --git a/scripts/licenses/license-overrides.json b/scripts/licenses/license-overrides.json index ba65887ffb4..2f77bcf6a33 100644 --- a/scripts/licenses/license-overrides.json +++ b/scripts/licenses/license-overrides.json @@ -1,6 +1,11 @@ { "_comment": "Hand-resolved licenses for packages cdxgen + FETCH_LICENSE cannot resolve. 'overrides' are PURL-pinned (pkg:npm/@, 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": { diff --git a/scripts/licenses/pipeline.test.mjs b/scripts/licenses/pipeline.test.mjs index ccc305f1be7..f986b2c48e1 100644 --- a/scripts/licenses/pipeline.test.mjs +++ b/scripts/licenses/pipeline.test.mjs @@ -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 }); diff --git a/scripts/licenses/render-licenses-md.test.mjs b/scripts/licenses/render-licenses-md.test.mjs index 0706d0863df..8d49a41a4d1 100644 --- a/scripts/licenses/render-licenses-md.test.mjs +++ b/scripts/licenses/render-licenses-md.test.mjs @@ -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) => { diff --git a/security/sca/README.md b/security/sca/README.md new file mode 100644 index 00000000000..f09c146e99f --- /dev/null +++ b/security/sca/README.md @@ -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: \ + --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: \ + --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 `` 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@ \ + --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.