diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index 11cf1737e09..7f60b8035ec 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -599,10 +599,10 @@ npm audit signatures n8n@VERSION VEX documents which CVEs actually affect n8n vs false positives from scanners. -- **File:** `vex.openvex.json` (repo root) +- **File:** `security/vex.openvex.json` - **Format:** OpenVEX (broad scanner compatibility - Trivy, Docker Scout, etc.) - **Attached to:** GitHub Release, Docker image attestations -- **Used by:** Trivy scans (via `.github/trivy.yaml`) +- **Used by:** Trivy scans (via `security/trivy.yaml`) **VEX Status Types:** | Status | Meaning | @@ -620,7 +620,7 @@ cosign verify-attestation --type openvex \ ghcr.io/n8n-io/n8n:VERSION ``` -**Adding a CVE statement to vex.openvex.json:** +**Adding a CVE statement to security/vex.openvex.json:** ```json { "statements": [ diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 8542a1bb955..c4bcdd95b4f 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -330,7 +330,7 @@ jobs: secrets: registry-password: ${{ secrets.GITHUB_TOKEN }} - # VEX Attestation - Documents which CVEs affect us (vex.openvex.json) + # VEX Attestation - Documents which CVEs affect us (security/vex.openvex.json) vex-attestation: name: VEX Attestation needs: [determine-build-context, build-and-push-docker, create_multi_arch_manifest, provenance-n8n, provenance-runners, provenance-runners-distroless] @@ -349,7 +349,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Cosign - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 + uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 - name: Login to GHCR uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -363,7 +363,7 @@ jobs: run: | cosign attest --yes \ --type openvex \ - --predicate vex.openvex.json \ + --predicate security/vex.openvex.json \ ${{ needs.create_multi_arch_manifest.outputs.n8n_image }}@${{ needs.create_multi_arch_manifest.outputs.n8n_digest }} - name: Attest VEX to runners image @@ -371,7 +371,7 @@ jobs: run: | cosign attest --yes \ --type openvex \ - --predicate vex.openvex.json \ + --predicate security/vex.openvex.json \ ${{ needs.create_multi_arch_manifest.outputs.runners_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_digest }} - name: Attest VEX to runners-distroless image @@ -379,7 +379,7 @@ jobs: run: | cosign attest --yes \ --type openvex \ - --predicate vex.openvex.json \ + --predicate security/vex.openvex.json \ ${{ needs.create_multi_arch_manifest.outputs.runners_distroless_image }}@${{ needs.create_multi_arch_manifest.outputs.runners_distroless_digest }} security-scan: diff --git a/.github/workflows/sbom-generation-callable.yml b/.github/workflows/sbom-generation-callable.yml index 410d339297f..47d623eead6 100644 --- a/.github/workflows/sbom-generation-callable.yml +++ b/.github/workflows/sbom-generation-callable.yml @@ -68,11 +68,11 @@ jobs: # Upload SBOM and VEX files to the existing release gh release upload "${{ inputs.release_tag_ref }}" \ sbom-source.cdx.json \ - vex.openvex.json \ + security/vex.openvex.json \ --clobber COMPONENT_COUNT=$(jq '.components | length' sbom-source.cdx.json 2>/dev/null || echo "unknown") - VEX_STATEMENTS=$(jq '.statements | length' vex.openvex.json 2>/dev/null || echo "0") + VEX_STATEMENTS=$(jq '.statements | length' security/vex.openvex.json 2>/dev/null || echo "0") echo "SBOM and VEX attached to release" echo " - SBOM: $COMPONENT_COUNT components" echo " - VEX: $VEX_STATEMENTS CVE statements" diff --git a/.github/workflows/security-trivy-scan-callable.yml b/.github/workflows/security-trivy-scan-callable.yml index bf2be1fb2dd..72f86acf0d0 100644 --- a/.github/workflows/security-trivy-scan-callable.yml +++ b/.github/workflows/security-trivy-scan-callable.yml @@ -34,8 +34,9 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: sparse-checkout: | - vex.openvex.json - .github/trivy.yaml + security/vex.openvex.json + security/trivy.yaml + security/trivy-ignore-policy.rego sparse-checkout-cone-mode: false - name: Pull Docker image with retry @@ -46,7 +47,7 @@ jobs: done - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0 + uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # v0.34.1 id: trivy_scan with: image-ref: ${{ inputs.image_ref }} @@ -55,7 +56,7 @@ jobs: severity: 'CRITICAL,HIGH,MEDIUM,LOW' ignore-unfixed: false exit-code: '0' - trivy-config: '.github/trivy.yaml' + trivy-config: 'security/trivy.yaml' - name: Calculate vulnerability counts id: process_results diff --git a/scripts/scan-n8n-image.mjs b/scripts/scan-n8n-image.mjs index 481a4df0780..fa581b5a470 100755 --- a/scripts/scan-n8n-image.mjs +++ b/scripts/scan-n8n-image.mjs @@ -14,6 +14,15 @@ const scriptDir = path.dirname(new URL(import.meta.url).pathname); const isInScriptsDir = path.basename(scriptDir) === 'scripts'; const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir; +const assertPathWithinRoot = (envVar, defaultRelPath) => { + const resolved = path.resolve(process.env[envVar] || path.join(rootDir, defaultRelPath)); + if (!resolved.startsWith(rootDir + path.sep) && resolved !== rootDir) { + echo(chalk.red(`Error: ${envVar} must resolve within the repository root`)); + process.exit(1); + } + return resolved; +}; + // #region ===== Configuration ===== const config = { imageBaseName: process.env.IMAGE_BASE_NAME || 'n8nio/n8n', @@ -27,7 +36,8 @@ const config = { scanners: process.env.TRIVY_SCANNERS || 'vuln', quiet: process.env.TRIVY_QUIET === 'true', rootDir: rootDir, - vexFile: process.env.TRIVY_VEX || path.join(rootDir, 'vex.openvex.json'), + vexFile: assertPathWithinRoot('TRIVY_VEX', 'security/vex.openvex.json'), + ignorePolicyFile: assertPathWithinRoot('TRIVY_IGNORE_POLICY', 'security/trivy-ignore-policy.rego'), }; config.fullImageName = `${config.imageBaseName}:${config.imageTag}`; @@ -54,6 +64,7 @@ const printSummary = (status, time, message) => { echo(chalk.gray(` • Severity Levels: ${config.severity}`)); echo(chalk.gray(` • Scanners: ${config.scanners}`)); echo(chalk.gray(` • VEX file: ${config.vexFile}`)); + echo(chalk.gray(` • Ignore policy: ${config.ignorePolicyFile}`)); if (config.ignoreUnfixed) echo(chalk.gray(` • Ignored unfixed: yes`)); echo(chalk.blue.bold('========================')); }; @@ -94,6 +105,8 @@ const printSummary = (status, time, message) => { '/var/run/docker.sock:/var/run/docker.sock', '-v', `${config.vexFile}:/vex.openvex.json:ro`, + '-v', + `${config.ignorePolicyFile}:/trivy-ignore-policy.rego:ro`, config.trivyImage, 'image', '--severity', @@ -107,6 +120,8 @@ const printSummary = (status, time, message) => { '--no-progress', '--vex', '/vex.openvex.json', + '--ignore-policy', + '/trivy-ignore-policy.rego', ]; if (config.ignoreUnfixed) trivyArgs.push('--ignore-unfixed'); diff --git a/security/trivy-ignore-policy.rego b/security/trivy-ignore-policy.rego new file mode 100644 index 00000000000..a018a2ee6ef --- /dev/null +++ b/security/trivy-ignore-policy.rego @@ -0,0 +1,14 @@ +# Trivy ignore policy for n8n security scans. +# n8n's own published CVEs/GHSAs are intentionally excluded from internal +# scan results. Vulnerabilities in the n8n package should be visible to +# anyone running an older version — they indicate an upgrade is required. +# VEX (vex.openvex.json) covers third-party dependency false positives only. +package trivy + +import future.keywords.if + +default ignore := false + +ignore if { + input.PkgName == "n8n" +} diff --git a/.github/trivy.yaml b/security/trivy.yaml similarity index 63% rename from .github/trivy.yaml rename to security/trivy.yaml index d63d68097ce..2c52fa5e9a9 100644 --- a/.github/trivy.yaml +++ b/security/trivy.yaml @@ -2,4 +2,5 @@ # See: https://trivy.dev/latest/docs/references/configuration/config-file/ vulnerability: vex: - - vex.openvex.json + - security/vex.openvex.json + ignore-policy: security/trivy-ignore-policy.rego diff --git a/vex.openvex.json b/security/vex.openvex.json similarity index 98% rename from vex.openvex.json rename to security/vex.openvex.json index b0ec93f88e4..7ef6a6791ca 100644 --- a/vex.openvex.json +++ b/security/vex.openvex.json @@ -3,8 +3,8 @@ "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://github.com/n8n-io/n8n/vex", "author": "n8n Security Team ", - "timestamp": "2026-02-13T00:00:00Z", - "version": 3, + "timestamp": "2026-03-01T00:00:00Z", + "version": 5, "statements": [ { "vulnerability": {