ci: Scan release images with syft instead of cdxgen (no-changelog)
Stacked on #37356 (docker bake). That PR merges first; GitHub will retarget this to master.
Summary
Replaces cdxgen with syft for the release image SBOM attestation. Verified end to end on a real CI run.
| Before | After | |
|---|---|---|
SBOM Attestation job | 22.0 min | 5.7 min |
| Scan time, 4 images | 1124s | 135s (8.3×) |
| npm registry requests | ~3,700 | 0 |
~16 min off the release critical path. Combined with the parent PR (17m09s ->
4m48s on the build job), the two together take the release path from ~39 min to
~10.5 min. Baseline: run 33151088330 (n8n@2.37.4). After: run 33200937665.
attest-image-sbom.mjs passed --profile license-compliance to cdxgen. That profile sets FETCH_LICENSE=true and nothing else, so it made one sequential npm registry call per component — the exact thing the comment above it claimed was disabled. syft reads licenses from the LICENSE files already on disk, so it needs no network.
The fetch could not simply be dropped: without it, 13 npm packages lose their license and the release-blocking gate fails.
Accuracy improves
All four images pass the gate with 0 licenses lost vs the cdxgen baseline, and syft catalogues more:
n8n 1886 npm enforced ✅ (cdxgen: 1843)
n8n-pc 2030 npm enforced ✅ (cdxgen: 1855)
runners 483 npm enforced ✅ (cdxgen: 459)
runners-distroless 435 npm enforced ✅ (cdxgen: 432)
- Finds
@n8n/task-runner— a first-party package we ship that was missing from its own attested SBOM. - Emits an
operating-systemcomponent, which cdxgen did not. Scanners need it to select the distro vulnerability feed, sotrivy sbomagainst every SBOM we have published has silently skipped Alpine CVEs. Confirmed on the artifact this run published: it now reportsalpine 3.24andNode.js. - Drops 2191
pkg:genericentries that were config files (/etc/fstab,/etc/hostname) catalogued as components, and 21 Python packages declared inpyproject.tomlbut never installed.
Reviewer note — apk count drops 87 → 69, and that is correct. The apk database holds exactly 69 entries. cdxgen's extra 18 were installed subpackages collapsed onto their origin names (libssl3/libcrypto3 reported as openssl). syft names what is installed and keeps the origin in the purl (?upstream=openssl), so CVE mapping still resolves. Checked that this does not break the purl join with the VEX attestation on the same digest.
Also here
- 3 license overrides, each verified against the upstream LICENSE file rather than registry metadata:
qrcode-terminal(Apache-2.0),dreamopt(MIT),@getzep/zep-cloud(Apache-2.0). Two have no license field in the registry at all. - Cross-scanner phantom filter. syft names the source path
syft:location:0:path(notSrcFile) and marks phantomsversion: "UNKNOWN". Without both fixes the filter matches nothing and 23 phantoms reach the gate.enrich-sbom.mjsnow logs which purls it drops. assertSbomIsUsable()— the gate only inspects components it can see, so it passes on an SBOM that catalogued nothing. Now requires npm components and anoperating-systemcomponent before cosign runs.- Obsolete VEX statements removed. All three graphicsmagick statements pinned
1.3.45-r0; the image ships1.3.47-r0and NVD givesversionEndExcluding 1.3.46for all three CVEs, so they suppressed nothing.CVE-2025-27796was recorded asaffected, telling consumers the released image carried a WPG heap-overflow patched two releases earlier. - Docs.
security/sca/README.mdclaimed "0 unlicensed", which was already untrue. Rewritten to lead with the SPDX gate and document the OS-coverage trap above.
Verification
- Real CI run green;
cosign attestsucceeded against all four digests (4 Rekor entries) - Pulled the published attestation back with the documented consumer command:
specVersion 1.6,operating-system: alpine 3.24, gate passes 1886/1886 - 509 tests,
actionlintandbiomeclean - Reviewed by
pr-council(correctness, security, tests, integration); findings fixed ine6975c64af
Notes
sbom: trueon the buildx builds is removed by the parent PR (#37356). It was
restored in 320d454e62 for the OCI manifest index format; that format now comes
fromoci-mediatypeson the exporter instead, without the syft scan this job
replaces anyway.- The release SBOM job is untouched; a lockfile scan has no package files to read licenses from, so it still needs cdxgen with
FETCH_LICENSE=true. - Docker pulls (154s) are now the largest cost in the job, not scanning. Next lever for DEVP-906.
- The verification run built amd64 only, and its Trivy/VEX jobs were skipped (they carry the same
stable|rc|nightlygate). Neither is on the SBOM path. - SBOM shape changes — consumers diffing attestations across releases will see a one-time discontinuity.
https://linear.app/n8n/issue/DEVP-907
🤖 Generated with Claude Code