43 Commits
Author SHA1 Message Date
Renovate 6b70a25cf6 chore(deps): update dependency aquasecurity/trivy to v0.75.0
CI Tests / JS Unit Tests (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=true) (push) Skipped
renovate/artifacts Artifact file update failure
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Skipped
CI Tests / JS Unit Tests (pull_request) Successful in 5m11s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m56s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Failing after 1m26s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Failing after 1m28s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Failing after 1m28s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Failing after 1m31s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Failing after 1m54s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 1m33s
2026-10-01 22:14:56 +00:00
Philipp 2d58558b5a Merge pull request 'chore(deps): update actions/upload-artifact action to v7' (#19) from renovate/major-github-artifact-actions into main
CI Tests / JS Unit Tests (push) Successful in 8s
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Successful in 6m54s
CI Tests / without-inbuild-sbom (sbom=false) (push) Successful in 6m54s
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Successful in 6m56s
CI Tests / with-inbuild-sbom (sbom=false) (push) Successful in 9m11s
CI Tests / without-inbuild-sbom (sbom=true) (push) Successful in 9m11s
CI Tests / with-inbuild-sbom (sbom=true) (push) Successful in 9m10s
Reviewed-on: #19
2026-09-18 00:54:58 +02:00
Renovate 45fc314eab chore(deps): update actions/upload-artifact action to v7
CI Tests / JS Unit Tests (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / JS Unit Tests (pull_request) Successful in 8s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m34s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 3m33s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 5m47s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m38s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 3m46s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 1m56s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m34s
2026-09-15 01:01:20 +00:00
Philipp 826f853dff Merge pull request 'chore(deps): update actions/checkout action to v7' (#18) from renovate/actions-checkout-7.x into main
CI Tests / JS Unit Tests (push) Successful in 7s
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Successful in 3m14s
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Successful in 3m14s
CI Tests / without-inbuild-sbom (sbom=false) (push) Successful in 3m15s
CI Tests / without-inbuild-sbom (sbom=true) (push) Successful in 3m25s
CI Tests / with-inbuild-sbom (sbom=false) (push) Successful in 3m31s
CI Tests / with-inbuild-sbom (sbom=true) (push) Successful in 3m37s
Reviewed-on: #18
2026-09-15 02:17:55 +02:00
Philipp 8c0c553e4f Merge branch 'main' into renovate/actions-checkout-7.x
CI Tests / JS Unit Tests (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / JS Unit Tests (pull_request) Successful in 11s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 41s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 3m8s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 6m23s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m44s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 4m6s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 1m39s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m3s
2026-09-15 01:59:09 +02:00
Philipp 3f03635d45 Merge pull request 'Revert "Merge pull request 'refactor(action): replace bash blocks with zero-dependency Node.js modules' (#17) from feat-bash-to-js-refactor into main"' (#20) from rever-js-transformation into main
CI Tests / JS Unit Tests (push) Successful in 11s
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Successful in 2m49s
CI Tests / without-inbuild-sbom (sbom=false) (push) Successful in 3m15s
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Successful in 3m41s
CI Tests / without-inbuild-sbom (sbom=true) (push) Successful in 4m25s
CI Tests / with-inbuild-sbom (sbom=false) (push) Successful in 3m39s
CI Tests / with-inbuild-sbom (sbom=true) (push) Successful in 3m54s
Reviewed-on: #20
2026-09-15 01:58:06 +02:00
Philipp 9e607b1a24 Revert "Merge pull request 'refactor(action): replace bash blocks with zero-dependency Node.js modules' (#17) from feat-bash-to-js-refactor into main"
CI Tests / JS Unit Tests (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / JS Unit Tests (pull_request) Successful in 9s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m34s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 4m20s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 6m15s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m50s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 3m54s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 3m35s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 4m45s
This reverts commit 17e5dab1b2, reversing
changes made to 46c4fedc47.
2026-09-15 01:24:04 +02:00
Renovate 7343f659bb chore(deps): update actions/checkout action to v7
CI Tests / JS Unit Tests (pull_request) Successful in 12s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m57s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Failing after 4m27s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Failing after 3m33s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Failing after 3m23s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Failing after 5m41s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Failing after 5m8s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 3m3s
2026-09-14 22:52:06 +00:00
Philipp 17e5dab1b2 Merge pull request 'refactor(action): replace bash blocks with zero-dependency Node.js modules' (#17) from feat-bash-to-js-refactor into main
CI Tests / JS Unit Tests (push) Successful in 11s
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Failing after 1m26s
CI Tests / without-inbuild-sbom (sbom=true) (push) Failing after 3m25s
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Failing after 3m26s
CI Tests / without-inbuild-sbom (sbom=false) (push) Failing after 3m27s
CI Tests / with-inbuild-sbom (sbom=false) (push) Failing after 4m23s
CI Tests / with-inbuild-sbom (sbom=true) (push) Failing after 4m23s
Reviewed-on: #17
Reviewed-by: LLMReview <27+autoreview@noreplay.horstenkamp.eu>
2026-09-15 00:51:17 +02:00
Philipp 493d119b3c Merge branch 'main' into feat-bash-to-js-refactor
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m43s
CI Tests / JS Unit Tests (pull_request) Successful in 16s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Failing after 2m48s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Failing after 3m13s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Failing after 3m13s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Failing after 4m38s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Failing after 4m39s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 3m15s
2026-09-15 00:49:02 +02:00
Philipp 77357f0c3f refactor(action): replace bash blocks with zero-dependency Node.js modules
CI Tests / JS Unit Tests (pull_request) Successful in 46s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m54s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Failing after 3m22s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Failing after 4m21s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Failing after 5m36s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Failing after 6m47s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Failing after 2m59s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 2m38s
Extract ~450 lines of complex bash from action.yaml into modular,
unit-tested Node.js scripts in lib/. All logic is covered by 26 native
node:test unit tests (node --test, no external dependencies).

Modules added:
- lib/platforms.js   - platform parsing and OCI manifest auto-discovery
- lib/distro.js      - SPDX PURL distro detection (os_name/os_version)
- lib/verifier.js    - Trivy arch mapping and SHA256 verification
- lib/diagnostics.js - CI terminal diagnostic formatters
- lib/install-trivy.js  - Trivy install/verify entrypoint (replaces bash step)
- lib/extract-sboms.js  - SBOM extraction entrypoint (replaces bash step)
- lib/scan-sboms.js     - SBOM scan + diff entrypoint (replaces bash step)

Tests added (all pass, auto-discovered by
ode --test):
- test/platforms.test.js
- test/distro.test.js
- test/verifier.test.js
- test/diagnostics.test.js
- test/orchestration.test.js

action.yaml shrinks from 554 to 107 lines. Inputs are bridged to Node
scripts via INPUT_* env vars so modules are fully decoupled from the
Actions runner and testable in isolation.
2026-09-15 00:39:13 +02:00
Philipp 46c4fedc47 Merge pull request 'chore(deps): update actions/cache action to v6' (#16) from renovate/actions-cache-6.x into main
CI Tests / JS Unit Tests (push) Successful in 2m9s
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Successful in 1m59s
CI Tests / without-inbuild-sbom (sbom=true) (push) Successful in 3m18s
CI Tests / without-inbuild-sbom (sbom=false) (push) Successful in 3m51s
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Successful in 3m57s
CI Tests / with-inbuild-sbom (sbom=false) (push) Successful in 3m36s
CI Tests / with-inbuild-sbom (sbom=true) (push) Successful in 4m45s
Reviewed-on: #16
2026-09-15 00:38:45 +02:00
Philipp 8070a3c155 Merge branch 'main' into renovate/actions-cache-6.x
CI Tests / JS Unit Tests (pull_request) Successful in 9s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m2s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 5m16s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 7m5s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 2m55s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 3m7s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 2m24s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 4m34s
2026-09-15 00:32:49 +02:00
Philipp 0ccfd5f512 Merge pull request 'ci: add matrix test workflow for debian slim images in .gitea' (#15) from feat-test-matrix-workflow into main
CI Tests / JS Unit Tests (push) Successful in 9s
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Successful in 2m46s
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Successful in 3m3s
CI Tests / without-inbuild-sbom (sbom=true) (push) Successful in 2m57s
CI Tests / without-inbuild-sbom (sbom=false) (push) Successful in 3m3s
CI Tests / with-inbuild-sbom (sbom=false) (push) Successful in 2m53s
CI Tests / with-inbuild-sbom (sbom=true) (push) Successful in 3m15s
Reviewed-on: #15
2026-09-15 00:26:57 +02:00
Philipp c98b31c23a Merge branch 'main' into feat-test-matrix-workflow
CI Tests / JS Unit Tests (pull_request) Successful in 17s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m49s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 1m58s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m1s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 3m8s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 2m59s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 3m6s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m35s
2026-09-15 00:17:06 +02:00
Philipp def42c37a8 docs: document trade-offs and differences between in-build and direct scanning
CI Tests / JS Unit Tests (pull_request) Successful in 8s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m34s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 2m35s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 7m5s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 5m52s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 5m46s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 2m22s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 2m42s
2026-09-15 00:12:42 +02:00
Philipp 64747ec466 fix: robustly parse distro from in-build SBOM package PURLs
CI Tests / JS Unit Tests (pull_request) Successful in 8s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m35s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 3m3s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 5m32s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 5m32s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 1m49s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 7m2s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m11s
2026-09-14 23:37:07 +02:00
Philipp 3b9043a746 feat: detect distro from in-build SBOM PURLs and pass --distro to trivy
CI Tests / JS Unit Tests (pull_request) Successful in 8s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m35s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 2m57s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m59s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 5m34s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 3m52s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 1m36s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m5s
2026-09-14 23:25:09 +02:00
Philipp 790c1aa3de docs: update README with in-build fallback and unit test details
CI Tests / JS Unit Tests (pull_request) Successful in 12s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m16s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 3m45s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 7m8s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 4m24s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 4m24s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 1m56s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m40s
2026-09-14 22:34:12 +02:00
Philipp f31be5eef6 ci: move unit tests into dedicated unit-test job
CI Tests / JS Unit Tests (pull_request) Successful in 11s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m16s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 5m57s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 5m39s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 5m34s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 5m38s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 5m57s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 5m58s
2026-09-14 22:29:21 +02:00
Philipp d89fb25d7b test: add unit tests for cve-diff and fallback for in-build sbom scans
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 3m0s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 3m1s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 7m14s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 7m14s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 7m32s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m29s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m0s
2026-09-14 22:27:49 +02:00
Renovate 2ece0c93ac chore(deps): update actions/cache action to v6
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m0s
2026-09-14 17:27:56 +00:00
Philipp 6643b7449e Merge pull request 'chore(config): migrate Renovate config' (#14) from renovate/migrate-config into main
Reviewed-on: #14
2026-09-14 05:08:27 +02:00
Philipp 40209bbeed style: format test workflow to comply with pretty-format-yaml
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 5m2s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 4m8s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 4m52s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Failing after 3m57s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 4m55s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m40s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 4m58s
2026-09-14 03:06:49 +02:00
Philipp 44c4b5a304 ci: add matrix test workflow for debian slim images in .gitea
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 10m38s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 10m32s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m9s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Failing after 5m21s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 5m25s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Failing after 5m8s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 9m33s
2026-09-14 03:03:32 +02:00
Renovate 6dae2af2cd chore(config): migrate config renovate.json
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m44s
2026-09-14 00:49:15 +00:00
Philipp 015e40634a Merge pull request 'feat: dynamic multi-architecture discovery, iteration, and conditional reporting' (#11) from iterate-architectures into main
Reviewed-on: #11
2026-09-14 00:40:44 +02:00
Philipp e394966a96 feat: show Arch column only when multiple architectures are scanned
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 3m5s
2026-09-13 22:46:05 +02:00
Philipp e232a22547 feat: support dynamic multi-architecture discovery and iteration 2026-09-13 22:42:40 +02:00
Philipp abe45ffb02 Merge pull request 'feat: add dual-side diagnostic overview and fail-safe gating' (#10) from feat-diagnostic-overview-and-gating into main
Reviewed-on: #10
Reviewed-by: LLMReview <27+autoreview@noreplay.horstenkamp.eu>
2026-09-13 20:52:55 +02:00
Philipp e97c5b1fc9 docs: add comparison between built-in Syft SBOMs and direct Trivy scanning
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m36s
2026-09-13 20:40:38 +02:00
Philipp 86a00c54dc feat: add use-inbuild-sbom input defaulting to false
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 35s
2026-09-13 20:34:52 +02:00
Philipp 77f3cf68e4 feat: warn when an image has 0 reported CVEs and display warning banner
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 47s
2026-09-13 20:02:35 +02:00
Philipp 3b458f8749 feat: add dual-side diagnostic overview and gating to ensure both scans succeed
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 36s
2026-09-13 19:58:54 +02:00
Philipp d49634fba5 Merge pull request 'refactor: internalize trivy version management and remove input' (#9) from internalize-trivy-version into main
Reviewed-on: #9
2026-09-13 03:42:48 +02:00
Philipp e4a0cac9ff refactor: internalize trivy version management and remove input
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 36s
2026-09-13 03:41:34 +02:00
Philipp 48faaf07d1 Merge pull request 'docs: add .git suffix to action url in README example' (#7) from add-write-summary-option into main
Reviewed-on: #7
2026-09-13 02:07:30 +02:00
Philipp 1ff782c6c9 Merge branch 'main' into add-write-summary-option
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 40s
2026-09-13 01:24:32 +02:00
Philipp 1b4460f9bd Merge pull request 'Update README.md' (#8) from philipp-patch-1 into main
Reviewed-on: #8
2026-09-13 01:23:49 +02:00
Philipp c1dede3ce2 Merge branch 'main' into add-write-summary-option
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 33s
2026-09-13 01:23:36 +02:00
Philipp a62f996eb5 Merge branch 'main' into philipp-patch-1
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 47s
2026-09-13 01:21:44 +02:00
Philipp a513cc2c18 docs: add .git suffix to action url in README example
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 56s
2026-09-13 01:10:44 +02:00
Philipp 9638e61534 Update README.md
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 52s
2026-09-13 00:37:48 +02:00
12 changed files with 1000 additions and 145 deletions

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
name: CI Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
unit-test:
name: JS Unit Tests
runs-on: k8s
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Run JS Unit Tests
shell: bash
run: node --test
test:
name: ${{ matrix.image_set.name }} (sbom=${{ matrix.use_inbuild_sbom }})
needs: unit-test
runs-on: k8s
strategy:
fail-fast: false
matrix:
image_set:
# Test 1: Slim images with built-in SBOMs (2 architectures, 15-40 CVEs each)
- name: with-inbuild-sbom
head_image: debian:bookworm-slim
target_image: debian:bullseye-slim
architectures: amd64,arm64
expect_arch_col: true
# Test 2: Slim images without built-in SBOMs (1 architecture, 70-120 CVEs each)
- name: without-inbuild-sbom
head_image: debian:buster-slim
target_image: debian:stretch-slim
architectures: amd64
expect_arch_col: false
# Test 3: Cross-check (Head has built-in SBOM, Target does not, 15-70 CVEs each)
- name: cross-check-mixed-sbom
head_image: debian:bookworm-slim
target_image: debian:buster-slim
architectures: amd64
expect_arch_col: false
use_inbuild_sbom:
- 'true'
- 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Test CVE Differential Action
uses: ./
with:
head-image: ${{ matrix.image_set.head_image }}
target-image: ${{ matrix.image_set.target_image }}
target-branch: main
architectures: ${{ matrix.image_set.architectures }}
use-inbuild-sbom: ${{ matrix.use_inbuild_sbom }}
output-delta: cve-delta.md
output-summary: cve-summary.md
- name: Verify Report Generation & Content
shell: bash
run: |
set -euo pipefail
test -s cve-delta.md || (echo "::error::cve-delta.md is empty or missing" && exit 1)
test -s cve-summary.md || (echo "::error::cve-summary.md is empty or missing" && exit 1)
# Verify that each scan produced at least 3 CVE findings
cve_count=$(grep -c "CVE-" cve-summary.md || echo 0)
echo "Total CVE entries found in summary report: $cve_count"
if [ "$cve_count" -lt 3 ]; then
echo "::error::Expected at least 3 CVEs in cve-summary.md, found only $cve_count"
exit 1
fi
echo "=== Delta Report Preview ==="
head -n 25 cve-delta.md
# Verify architectural column rendering behavior
if [ "${{ matrix.image_set.expect_arch_col }}" = "true" ]; then
grep -q "| Arch |" cve-delta.md || (echo "::error::Expected Arch column in multi-arch report" && exit 1)
else
if grep -q "| Arch |" cve-delta.md; then
echo "::error::Did not expect Arch column in single-arch report" && exit 1
fi
fi
echo "✅ All assertions passed (including >= 3 CVEs verification)."
- name: Archive SBOMs and CVE Reports
if: always()
uses: actions/upload-artifact@v7
with:
name: sboms-and-reports-${{ matrix.image_set.name }}-inbuild-${{ matrix.use_inbuild_sbom }}
path: |
sboms/
cve-reports/
cve-delta.md
cve-summary.md
if-no-files-found: ignore
+1
View File
@@ -13,3 +13,4 @@ cve-*.json
cve-delta.md
cve-summary.md
node_modules/
!test/fixtures/**
+48 -12
View File
@@ -6,28 +6,28 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p
## ✨ Features
- **Multi-Platform Support**: Automatically detects and extracts SPDX SBOM attestations for multi arch images.
- **Fast Attestation Extraction**: Uses `docker buildx imagetools inspect` to extract SBOMs directly from registry manifest indices in seconds without downloading layer blobs.
- **Graceful Fallbacks**: If SBOM attestations or baseline images are missing, automatically falls back to generating SBOMs directly with Trivy. Using images that do contain an SBOM is highly recomended though!
- **Dynamic Multi-Platform Support**: Automatically discovers and scans all architectures present in container images, or iterates over an explicit list of target architectures.
- **Fast Attestation Extraction with Automatic Fallback**: Uses `docker buildx imagetools inspect` to extract platform-specific SBOMs directly from registry manifest indices. If attestations are missing or produce 0 CVE findings (common when third-party generators omit OS qualifiers in SPDX), it automatically falls back to generating SBOMs directly with Trivy per architecture.
- **Set-Based Differential Analysis**: Categorizes vulnerabilities into:
- 🔴 **Introduced Vulnerabilities** (newly added in this branch / commit)
- 🟢 **Fixed Vulnerabilities** (present in target baseline, resolved in this branch)
- ⚪ **Unchanged Vulnerabilities** (pre-existing in target baseline)
- **Secure by Default**: Verified Trivy binary downloads against static SHA256 checksum lockfiles and single-step unified caching (binary + vulnerability database).
- **Secure & Unit Tested**: Verified Trivy binary downloads against static SHA256 checksum lockfiles, single-step unified caching, and comprehensive native unit tests (`node:test`).
---
## 📥 Inputs
| Input | Description | Required | Default |
| :--- | :--- | :---: | :--- |
| :--- | :--- | :--- | :--- |
| `head-image` | Head container image reference to scan (e.g. `registry.example.com/repo:git-sha`) | **Yes** | — |
| `target-image` | Target baseline container image reference to compare against (e.g. `registry.example.com/repo:latest`) | **Yes** | — |
| `target-branch` | Target branch name for report headings | No | `main` |
| `trivy-version` | Trivy version to install and verify | No | `0.74.0` |
| `architectures` | Comma-separated architectures/platforms to scan (e.g. `amd64,arm64`). If omitted, scans all architectures present in the image. | No | _(all)_ |
| `output-delta` | Output path for the CVE delta Markdown report | No | `cve-delta.md` |
| `output-summary` | Output path for the full vulnerability Markdown summary | No | `cve-summary.md` |
| `write-summary` | Whether to append the generated reports to the job summary (`$GITHUB_STEP_SUMMARY`) | No | `true` |
| `use-inbuild-sbom` | Whether to inspect and use built-in image SBOM attestations. If false (recommended default), generates SBOMs directly with Trivy for maximum vulnerability fidelity. If true, extracts attestations without pulling layers (faster, but findings reflect binary-package SBOMs without rootfs source-mapping; falls back to Trivy image scan if 0 CVEs detected). | No | `false` |
---
@@ -40,6 +40,46 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p
---
## 🔍 Built-in SBOM Attestations vs. Trivy Direct Scanning (Syft vs. Trivy)
This action supports two modes for obtaining SBOMs:
### 1. Direct Image Scanning with Trivy (`use-inbuild-sbom: false` — Default & Recommended)
By default, the action uses `trivy image --format spdx-json` to generate the SBOM directly from the container images:
- **Full Filesystem Fidelity**: Inspects the entire flattened root filesystem, reading `/etc/os-release` and native package databases (e.g. `/var/lib/dpkg/status`, RPM databases, apk).
- **Accurate OS & Language Package Detection**: Catalogs both OS packages (Debian, Alpine, RHEL, etc.) and application dependencies (Python, Node.js, Go, etc.).
- **Symmetric, Apples-to-Apples Comparisons**: Both `head` and `target` images are analyzed using the identical Trivy engine and vulnerability database, preventing skewed diffs.
### 2. Built-in Attestation Extraction with Resilient Fallback (`use-inbuild-sbom: true`)
When enabled, the action extracts SBOM attestations attached to the container image using `docker buildx imagetools inspect`:
- **Speed**: Extracts the SBOM JSON metadata directly from the registry manifest index in seconds without pulling image layer blobs.
- **Underlying Generator (Syft / Docker Scout)**: Docker Buildx (`sbom: true`) uses BuildKit's embedded Syft scanner (`moby/buildkit-syft-scanner`).
- **Distro Detection**: Automatically extracts OS distribution information from package PURLs (e.g. `debian/12`) and queries Trivy with `--distro` to accurately identify vulnerabilities on the extracted SBOM.
- **Resilient Fallback**: If an extracted in-build SBOM scan produces 0 CVE findings (a common issue when third-party generators omit OS distribution qualifiers from SPDX packages) or if scanning fails, the action **automatically falls back** to running `trivy image --platform "$platform" --format spdx-json` directly on the image. This guarantees that vulnerability scans never fail silently or report false zeros.
- **Audit Copies**: The action preserves copies of both the raw extracted in-build SBOM (`inbuild-sbom-*.spdx.json`) and the final scanned SBOM (`sbom-*.spdx.json`) for auditing and debugging.
### ⚖️ Trade-offs: Why do the results differ between strategies?
The vulnerability results between Strategy 1 (`sbom: false`) and Strategy 2 (`sbom: true`) are **not identical**:
| Feature / Metric | Direct Trivy Scan (`use-inbuild-sbom: false`) | Built-in Attestation (`use-inbuild-sbom: true`) |
| :--- | :--- | :--- |
| **Generator** | Trivy directly on container rootfs | Docker Scout / BuildKit (Syft) |
| **Execution Speed** | Slower (pulls image layer blobs) | **Very Fast** (~5x faster, 0 layer downloads) |
| **Debian Slim Findings Example** | **~225 CVEs** (`debian:bookworm-slim`) | **~104 CVEs** (`debian:bookworm-slim`) |
| **Package Resolution Model** | Source & binary packages (`/var/lib/dpkg/status`) | Binary packages cataloged in SPDX PURLs |
| **Filesystem Inspection** | Full filesystem (static binaries, libraries) | Manifest-only (packages cataloged at build time) |
| **Primary Use Case** | **High-fidelity security audits & release gates** | **High-speed PR checks & developer feedback loops** |
#### Why the vulnerability counts differ:
1. **Debian Source vs. Binary Packages**: Debian security advisories (DSAs / DLAs) are tracked and published per **source package** (e.g., the source package `glibc` produces multiple binary packages like `libc6`, `libc-bin`, `locales`). Trivy's filesystem scanner reads `/var/lib/dpkg/status` to map installed binary packages directly to Debian source package advisories. Syft's default SPDX output catalogs installed binary packages as PURLs without source-package relations, so Trivy's SBOM scan queries only the exact binary package entries that match Debian database schemas without the source-mapping context.
2. **Filesystem vs. Manifest Scope**: Direct image scanning unpacks and inspects the entire container image filesystem (including file hashes, crypto libraries, and static binaries). An in-build SBOM scan is strictly limited to the package inventory written by the build-time generator.
> [!TIP]
> Setting `use-inbuild-sbom: 'false'` remains the recommended default for maximum accuracy and cross-platform consistency. If you choose to enable `use-inbuild-sbom: 'true'`, the built-in fallback ensures full coverage even when third-party attestations lack OS package metadata.
---
## 🚀 Usage Example
```yaml
@@ -47,20 +87,16 @@ jobs:
build-and-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build and Push Head Container Image
uses: docker/build-push-action@v5
with:
context: .
push: true
sbom: true
platforms: linux/amd64,linux/arm64
tags: registry.example.com/my-app:git-${{ github.sha }}
- name: Compare Vulnerabilities (Head vs Target)
uses: https://git.horstenkamp.eu/Philipp/cve-differential@main
uses: https://git.horstenkamp.eu/Philipp/cve-differential.git@main
with:
head-image: registry.example.com/my-app:git-${{ github.sha }}
target-image: registry.example.com/my-app:latest
+436 -85
View File
@@ -12,10 +12,10 @@ inputs:
description: Target branch name for delta heading (e.g. main)
required: false
default: main
trivy-version:
description: Trivy binary version to install and verify
architectures:
description: Comma-separated list of architectures or platforms to scan (e.g. 'amd64,arm64' or 'linux/amd64,linux/arm64'). If empty, scans all architectures found in the container image.
required: false
default: 0.74.0 # trivy-version
default: ''
output-delta:
description: Output markdown file for CVE delta report
required: false
@@ -28,6 +28,10 @@ inputs:
description: Whether to append the generated reports to the job summary ($GITHUB_STEP_SUMMARY)
required: false
default: 'true'
use-inbuild-sbom:
description: Whether to inspect and use built-in image SBOM attestations. If false (recommended default), generates SBOMs directly with Trivy for maximum vulnerability detection fidelity. If true, extracts attestations without pulling layers (faster, but findings may differ from direct scans due to binary vs source package tracking; falls back to Trivy image scan if 0 CVEs detected).
required: false
default: 'false'
outputs:
delta-report:
@@ -40,41 +44,47 @@ outputs:
runs:
using: composite
steps:
- name: Prepare Cache Keys
- name: Prepare Cache Keys and Environment
id: vars
shell: bash
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
env:
TRIVY_VERSION: 0.75.0 # trivy-version
run: |
echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
echo "version=${TRIVY_VERSION}" >> "$GITHUB_OUTPUT"
- name: Cache Trivy (Binary & Vulnerability DB)
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cache/trivy-bin
~/.cache/trivy
key: trivy-${{ inputs.trivy-version }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.vars.outputs.date }}
key: trivy-${{ steps.vars.outputs.version }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.vars.outputs.date }}
restore-keys: |
trivy-${{ inputs.trivy-version }}-${{ runner.os }}-${{ runner.arch }}-
trivy-${{ steps.vars.outputs.version }}-${{ runner.os }}-${{ runner.arch }}-
- name: Install and Verify Trivy
shell: bash
run: |
mkdir -p "$HOME/.cache/trivy-bin" "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
set -euo pipefail
ARCH=$(uname -m)
case "$ARCH" in
mkdir -p "$HOME/.local/bin" "$HOME/.cache/trivy-bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) T_ARCH="64bit" ;;
aarch64|arm64) T_ARCH="ARM64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
*) echo "::error::Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
tarball="trivy_${{ inputs.trivy-version }}_Linux-${T_ARCH}.tar.gz"
checksums_file="${{ github.action_path }}/checksums.txt"
tarball="trivy_${{ steps.vars.outputs.version }}_Linux-${T_ARCH}.tar.gz"
if [ -f "$HOME/.cache/trivy-bin/${tarball}" ] && (cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -); then
echo "Cached Trivy archive verified against lockfile."
else
echo "Archive missing or checksum mismatch; downloading..."
curl -sfLO "https://github.com/aquasecurity/trivy/releases/download/v${{ inputs.trivy-version }}/${tarball}" --output-dir "$HOME/.cache/trivy-bin"
curl -sfLO "https://github.com/aquasecurity/trivy/releases/download/v${{ steps.vars.outputs.version }}/${tarball}" --output-dir "$HOME/.cache/trivy-bin"
(cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -)
fi
@@ -85,108 +95,449 @@ runs:
shell: bash
run: |
export PATH="$HOME/.local/bin:$PATH"
export TRIVY_SKIP_VERSION_CHECK="true"
mkdir -p sboms/head sboms/target
cleanup_file() {
local f="$1"
if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then
rm -f "$f"
if [ -f "$f" ]; then
if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then
rm -f "$f"
fi
fi
}
# Extract platform SBOMs for head image
echo "Extracting platform SBOMs for head image: ${{ inputs.head-image }}..."
docker buildx imagetools inspect "${{ inputs.head-image }}" \
--format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true
cleanup_file sboms/head/sbom-amd64.spdx.json
head_sbom_err=""
target_sbom_err=""
use_inbuild="${{ inputs.use-inbuild-sbom }}"
docker buildx imagetools inspect "${{ inputs.head-image }}" \
--format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sboms/head/sbom-arm64.spdx.json 2>/dev/null || true
cleanup_file sboms/head/sbom-arm64.spdx.json
# Determine target platforms / architectures
PLATFORMS=()
if [ -n "${{ inputs.architectures }}" ]; then
IFS=',' read -ra RAW_ARCHS <<< "${{ inputs.architectures }}"
for a in "${RAW_ARCHS[@]}"; do
clean_a=$(echo "$a" | xargs)
[ -z "$clean_a" ] && continue
if [[ "$clean_a" != *"/"* ]]; then
PLATFORMS+=("linux/${clean_a}")
else
PLATFORMS+=("$clean_a")
fi
done
else
echo "Auto-discovering architectures from images..."
raw_head=$(docker buildx imagetools inspect --raw "${{ inputs.head-image }}" 2>/dev/null || true)
raw_target=$(docker buildx imagetools inspect --raw "${{ inputs.target-image }}" 2>/dev/null || true)
# If single-arch, extract root .SPDX
if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ]; then
docker buildx imagetools inspect "${{ inputs.head-image }}" \
--format '{{ json .SBOM.SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true
cleanup_file sboms/head/sbom-amd64.spdx.json
discovered=$(node -e '
const raw1 = process.argv[1];
const raw2 = process.argv[2];
const set = new Set();
for (const raw of [raw1, raw2]) {
if (!raw) continue;
try {
const data = JSON.parse(raw);
const manifests = data.manifests || [];
for (const m of manifests) {
if (m.platform && m.platform.architecture && m.platform.architecture !== "unknown") {
const os = m.platform.os || "linux";
const variant = m.platform.variant ? "/" + m.platform.variant : "";
set.add(`${os}/${m.platform.architecture}${variant}`);
}
}
} catch (e) {}
}
console.log(Array.from(set).sort().join("\n"));
' "$raw_head" "$raw_target")
if [ -n "$discovered" ]; then
while IFS= read -r line; do
[ -n "$line" ] && PLATFORMS+=("$line")
done <<< "$discovered"
fi
fi
# Fallback to generating head SBOM with Trivy if attestation missing
if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ]; then
echo "Head image lacks SBOM attestation; generating with Trivy..."
trivy image --format spdx-json --output sboms/head/sbom-amd64.spdx.json "${{ inputs.head-image }}" || true
cleanup_file sboms/head/sbom-amd64.spdx.json
echo "Configured platforms: ${PLATFORMS[*]:-(single platform fallback)}"
if [ "$use_inbuild" = "true" ]; then
echo "ℹ️ Strategy: Built-in SBOM attestation extraction enabled (Docker Scout / Syft). Skips container layer downloads for maximum speed."
echo "ℹ️ Note: Built-in SBOM vulnerability counts may differ from direct filesystem scans ('trivy image') due to binary vs. source package tracking."
else
echo "ℹ️ Strategy: Direct Trivy image scanning enabled (default). Inspects full container rootfs and package databases for maximum fidelity."
fi
# Extract platform SBOMs for target image
echo "Extracting platform SBOMs for target image: ${{ inputs.target-image }}..."
docker buildx imagetools inspect "${{ inputs.target-image }}" \
--format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sboms/target/sbom-amd64.spdx.json 2>/dev/null || true
cleanup_file sboms/target/sbom-amd64.spdx.json
for platform in "${PLATFORMS[@]}"; do
arch_tag=$(echo "$platform" | sed -e 's#^[a-zA-Z0-9_-]*/##' -e 's#/#-#g')
docker buildx imagetools inspect "${{ inputs.target-image }}" \
--format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sboms/target/sbom-arm64.spdx.json 2>/dev/null || true
cleanup_file sboms/target/sbom-arm64.spdx.json
# Head image extraction & fallback
if [ "$use_inbuild" = "true" ]; then
echo "Attempting extraction of built-in SBOM for head image ($platform)..."
docker buildx imagetools inspect "${{ inputs.head-image }}" \
--format "{{ json (index .SBOM \"${platform}\").SPDX }}" > "sboms/head/sbom-${arch_tag}.spdx.json" 2>/dev/null || true
cleanup_file "sboms/head/sbom-${arch_tag}.spdx.json"
[ -f "sboms/head/sbom-${arch_tag}.spdx.json" ] && cp "sboms/head/sbom-${arch_tag}.spdx.json" "sboms/head/inbuild-sbom-${arch_tag}.spdx.json" 2>/dev/null || true
fi
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then
docker buildx imagetools inspect "${{ inputs.target-image }}" \
--format '{{ json .SBOM.SPDX }}' > sboms/target/sbom-amd64.spdx.json 2>/dev/null || true
cleanup_file sboms/target/sbom-amd64.spdx.json
if [ ! -f "sboms/head/sbom-${arch_tag}.spdx.json" ]; then
echo "Generating SBOM for head image ($platform) with Trivy..."
if ! trivy image --skip-version-check --platform "$platform" --format spdx-json --output "sboms/head/sbom-${arch_tag}.spdx.json" "${{ inputs.head-image }}"; then
echo "::warning::Trivy image scan failed for head image ($platform)"
fi
cleanup_file "sboms/head/sbom-${arch_tag}.spdx.json"
fi
# Target image extraction & fallback
if [ "$use_inbuild" = "true" ]; then
echo "Attempting extraction of built-in SBOM for target image ($platform)..."
docker buildx imagetools inspect "${{ inputs.target-image }}" \
--format "{{ json (index .SBOM \"${platform}\").SPDX }}" > "sboms/target/sbom-${arch_tag}.spdx.json" 2>/dev/null || true
cleanup_file "sboms/target/sbom-${arch_tag}.spdx.json"
[ -f "sboms/target/sbom-${arch_tag}.spdx.json" ] && cp "sboms/target/sbom-${arch_tag}.spdx.json" "sboms/target/inbuild-sbom-${arch_tag}.spdx.json" 2>/dev/null || true
fi
if [ ! -f "sboms/target/sbom-${arch_tag}.spdx.json" ]; then
echo "Generating SBOM for target image ($platform) with Trivy..."
if ! trivy image --skip-version-check --platform "$platform" --format spdx-json --output "sboms/target/sbom-${arch_tag}.spdx.json" "${{ inputs.target-image }}"; then
echo "::warning::Trivy image scan failed for target image ($platform)"
fi
cleanup_file "sboms/target/sbom-${arch_tag}.spdx.json"
fi
done
# Fallbacks if no platform-specific SBOMs were extracted (e.g. single-arch image without index)
if [ -z "$(find sboms/head -type f -name 'sbom-*.spdx.json' 2>/dev/null)" ]; then
if [ "$use_inbuild" = "true" ]; then
docker buildx imagetools inspect "${{ inputs.head-image }}" \
--format '{{ json .SBOM.SPDX }}' > sboms/head/sbom-default.spdx.json 2>/dev/null || true
cleanup_file sboms/head/sbom-default.spdx.json
[ -f sboms/head/sbom-default.spdx.json ] && cp sboms/head/sbom-default.spdx.json sboms/head/inbuild-sbom-default.spdx.json 2>/dev/null || true
fi
if [ ! -f sboms/head/sbom-default.spdx.json ]; then
echo "Generating default head SBOM directly with Trivy..."
if ! trivy image --skip-version-check --format spdx-json --output sboms/head/sbom-default.spdx.json "${{ inputs.head-image }}"; then
head_sbom_err="Failed to generate SBOM with Trivy for head image"
fi
cleanup_file sboms/head/sbom-default.spdx.json
fi
fi
# Fallback to generating target SBOM with Trivy if attestation missing
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then
echo "Target image lacks SBOM attestation; attempting to generate with Trivy..."
trivy image --format spdx-json --output sboms/target/sbom-amd64.spdx.json "${{ inputs.target-image }}" || true
cleanup_file sboms/target/sbom-amd64.spdx.json
if [ -z "$(find sboms/target -type f -name 'sbom-*.spdx.json' 2>/dev/null)" ]; then
if [ "$use_inbuild" = "true" ]; then
docker buildx imagetools inspect "${{ inputs.target-image }}" \
--format '{{ json .SBOM.SPDX }}' > sboms/target/sbom-default.spdx.json 2>/dev/null || true
cleanup_file sboms/target/sbom-default.spdx.json
[ -f sboms/target/sbom-default.spdx.json ] && cp sboms/target/sbom-default.spdx.json sboms/target/inbuild-sbom-default.spdx.json 2>/dev/null || true
fi
if [ ! -f sboms/target/sbom-default.spdx.json ]; then
echo "Generating default target SBOM directly with Trivy..."
if ! trivy image --skip-version-check --format spdx-json --output sboms/target/sbom-default.spdx.json "${{ inputs.target-image }}"; then
target_sbom_err="Failed to generate SBOM with Trivy for target image"
fi
cleanup_file sboms/target/sbom-default.spdx.json
fi
fi
shopt -s nullglob
head_files=(sboms/head/sbom-*.spdx.json)
target_files=(sboms/target/sbom-*.spdx.json)
if [ ${#head_files[@]} -eq 0 ] && [ -z "$head_sbom_err" ]; then
head_sbom_err="No SBOM generated and head image could not be inspected"
fi
if [ ${#target_files[@]} -eq 0 ] && [ -z "$target_sbom_err" ]; then
target_sbom_err="No SBOM generated and target baseline image could not be inspected"
fi
# Diagnostic overview
echo ""
echo "================================================================================"
echo " SBOM EXTRACTION DIAGNOSTIC OVERVIEW "
echo "================================================================================"
if [ -n "$head_sbom_err" ]; then
echo "HEAD Image (${{ inputs.head-image }}): ❌ FAILED - $head_sbom_err"
else
echo "HEAD Image (${{ inputs.head-image }}): ✅ SUCCESS"
for f in "${head_files[@]}"; do
echo " - $(basename "$f") ($(wc -c < "$f") bytes)"
done
fi
if [ -n "$target_sbom_err" ]; then
echo "TARGET Baseline Image (${{ inputs.target-image }}): ❌ FAILED - $target_sbom_err"
else
echo "TARGET Baseline Image (${{ inputs.target-image }}): ✅ SUCCESS"
for f in "${target_files[@]}"; do
echo " - $(basename "$f") ($(wc -c < "$f") bytes)"
done
fi
echo "================================================================================"
echo ""
has_sbom_err=false
if [ -n "$head_sbom_err" ]; then
echo "::error::Head SBOM extraction failed: $head_sbom_err"
has_sbom_err=true
fi
if [ -n "$target_sbom_err" ]; then
echo "::error::Target baseline SBOM extraction failed: $target_sbom_err"
has_sbom_err=true
fi
if [ "$has_sbom_err" = "true" ]; then
echo "::error::Both head and target images must have valid SBOMs to proceed with differential scanning."
exit 1
fi
# Expose extracted head SBOMs to workspace root for workflow artifact uploads
[ -f sboms/head/sbom-amd64.spdx.json ] && cp sboms/head/sbom-amd64.spdx.json sbom-amd64.spdx.json || true
[ -f sboms/head/sbom-arm64.spdx.json ] && cp sboms/head/sbom-arm64.spdx.json sbom-arm64.spdx.json || true
for f in sboms/head/sbom-*.spdx.json; do
[ -f "$f" ] || continue
cp "$f" "$(basename "$f")" || true
done
- name: Scan Multi-Platform SBOMs and Generate Delta
shell: bash
run: |
export PATH="$HOME/.local/bin:$PATH"
export TRIVY_SKIP_VERSION_CHECK="true"
mkdir -p cve-reports/head cve-reports/target
# Scan each available head SBOM
for f in sboms/head/sbom-*.spdx.json; do
[ -f "$f" ] || continue
arch=$(basename "$f" .spdx.json | sed 's/sbom-//')
echo "Scanning head SBOM for architecture: $arch"
trivy sbom --format json --output "cve-reports/head/cve-${arch}.json" "$f" || true
done
shopt -s nullglob
head_sboms=(sboms/head/sbom-*.spdx.json)
target_sboms=(sboms/target/sbom-*.spdx.json)
# Scan each available target SBOM
for f in sboms/target/sbom-*.spdx.json; do
[ -f "$f" ] || continue
arch=$(basename "$f" .spdx.json | sed 's/sbom-//')
echo "Scanning target SBOM for architecture: $arch"
trivy sbom --format json --output "cve-reports/target/cve-${arch}.json" "$f" || true
done
head_ok=true
target_ok=true
head_report_count=0
target_report_count=0
# Run multi-arch set-based diff
node "${{ github.action_path }}/cve-diff.js" \
"cve-reports/target" \
"cve-reports/head" \
"${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true
head_summary=()
target_summary=()
use_inbuild="${{ inputs.use-inbuild-sbom }}"
# Generate full markdown summary report for primary platform
primary_sbom=""
if [ -f sboms/head/sbom-amd64.spdx.json ]; then
primary_sbom="sboms/head/sbom-amd64.spdx.json"
elif [ -f sboms/head/sbom-arm64.spdx.json ]; then
primary_sbom="sboms/head/sbom-arm64.spdx.json"
detect_distro() {
node -e '
const fs = require("fs");
try {
const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
let name, ver;
for (const pkg of (data.packages || [])) {
for (const ref of (pkg.externalRefs || [])) {
if (ref.referenceType === "purl" && ref.referenceLocator) {
const mName = ref.referenceLocator.match(/[?&]os_name=([^&]+)/);
const mVer = ref.referenceLocator.match(/[?&]os_version=([^&]+)/);
if (mName && mVer) {
name = mName[1];
ver = mVer[1];
break;
}
}
}
if (name && ver) break;
}
if (name && ver) console.log(name + "/" + ver);
} catch (e) {}
' "$1" 2>/dev/null || true
}
# 1. Scan Head SBOMs
if [ ${#head_sboms[@]} -eq 0 ]; then
head_ok=false
head_summary+=(" - ❌ No SBOM files found in sboms/head/")
else
for f in "${head_sboms[@]}"; do
arch=$(basename "$f" .spdx.json | sed 's/sbom-//')
out_json="cve-reports/head/cve-${arch}.json"
err_log="cve-reports/head/cve-${arch}.err"
echo "Scanning head SBOM for architecture: $arch"
distro_arg=""
if [ "$use_inbuild" = "true" ]; then
detected_distro=$(detect_distro "$f")
if [ -n "$detected_distro" ]; then
echo "Detected distribution '${detected_distro}' from in-build SBOM PURLs. Scanning with --distro."
distro_arg="--distro ${detected_distro}"
fi
fi
scan_success=false
if trivy sbom $distro_arg --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
scan_success=true
fi
vuln_cnt=0
if [ -s "$out_json" ]; then
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
cnt=$(echo "$cnt" | tr -d '[:space:]')
[ -n "$cnt" ] && vuln_cnt="$cnt"
fi
# Fallback if in-build SBOM scan produced 0 CVEs (e.g. incompatible third-party format) or scan failed
if [ "$use_inbuild" = "true" ] && { [ "$scan_success" != "true" ] || [ "$vuln_cnt" -eq 0 ]; }; then
echo "::warning::In-build SBOM for head image (${arch}) yielded ${vuln_cnt} CVE findings. Falling back to direct Trivy image scan..."
t_plat="linux/${arch}"
[ "$arch" = "default" ] && t_plat=""
plat_arg=""
[ -n "$t_plat" ] && plat_arg="--platform $t_plat"
if trivy image --skip-version-check $plat_arg --format spdx-json --output "$f" "${{ inputs.head-image }}"; then
echo "Re-scanning head SBOM generated directly by Trivy..."
if trivy sbom --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
scan_success=true
if [ -s "$out_json" ]; then
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
cnt=$(echo "$cnt" | tr -d '[:space:]')
[ -n "$cnt" ] && vuln_cnt="$cnt"
fi
fi
fi
fi
if [ "$scan_success" = "true" ]; then
if [ -s "$out_json" ]; then
if [ "$vuln_cnt" -eq 0 ]; then
echo "::warning::Head image architecture '${arch}' has 0 CVE findings. Verify that OS/packages were recognized."
head_summary+=(" - ${arch}: ⚠️ WARNING (0 vulnerability findings detected)")
else
head_summary+=(" - ${arch}: ✅ OK (${vuln_cnt} vulnerability findings)")
fi
head_report_count=$((head_report_count + 1))
else
head_ok=false
head_summary+=(" - ${arch}: ❌ FAILED (Output report was empty / 0 bytes)")
fi
else
head_ok=false
err_msg=$(tail -n 2 "$err_log" | tr '\n' ' ' | sed 's/ */ /g')
[ -z "$err_msg" ] && err_msg="Trivy scan exited with non-zero status"
head_summary+=(" - ${arch}: ❌ FAILED (${err_msg})")
fi
done
fi
if [ -n "$primary_sbom" ]; then
trivy sbom \
--format template \
--template "@${{ github.action_path }}/markdown.tpl" \
--output "${{ inputs.output-summary }}" \
"$primary_sbom" || true
# 2. Scan Target SBOMs
if [ ${#target_sboms[@]} -eq 0 ]; then
target_ok=false
target_summary+=(" - ❌ No SBOM files found in sboms/target/")
else
for f in "${target_sboms[@]}"; do
arch=$(basename "$f" .spdx.json | sed 's/sbom-//')
out_json="cve-reports/target/cve-${arch}.json"
err_log="cve-reports/target/cve-${arch}.err"
echo "Scanning target SBOM for architecture: $arch"
distro_arg=""
if [ "$use_inbuild" = "true" ]; then
detected_distro=$(detect_distro "$f")
if [ -n "$detected_distro" ]; then
echo "Detected distribution '${detected_distro}' from in-build SBOM PURLs. Scanning with --distro."
distro_arg="--distro ${detected_distro}"
fi
fi
scan_success=false
if trivy sbom $distro_arg --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
scan_success=true
fi
vuln_cnt=0
if [ -s "$out_json" ]; then
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
cnt=$(echo "$cnt" | tr -d '[:space:]')
[ -n "$cnt" ] && vuln_cnt="$cnt"
fi
# Fallback if in-build SBOM scan produced 0 CVEs (e.g. incompatible third-party format) or scan failed
if [ "$use_inbuild" = "true" ] && { [ "$scan_success" != "true" ] || [ "$vuln_cnt" -eq 0 ]; }; then
echo "::warning::In-build SBOM for target image (${arch}) yielded ${vuln_cnt} CVE findings. Falling back to direct Trivy image scan..."
t_plat="linux/${arch}"
[ "$arch" = "default" ] && t_plat=""
plat_arg=""
[ -n "$t_plat" ] && plat_arg="--platform $t_plat"
if trivy image --skip-version-check $plat_arg --format spdx-json --output "$f" "${{ inputs.target-image }}"; then
echo "Re-scanning target SBOM generated directly by Trivy..."
if trivy sbom --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
scan_success=true
if [ -s "$out_json" ]; then
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
cnt=$(echo "$cnt" | tr -d '[:space:]')
[ -n "$cnt" ] && vuln_cnt="$cnt"
fi
fi
fi
fi
if [ "$scan_success" = "true" ]; then
if [ -s "$out_json" ]; then
if [ "$vuln_cnt" -eq 0 ]; then
echo "::warning::Target baseline image architecture '${arch}' has 0 CVE findings. Verify that OS/packages were recognized."
target_summary+=(" - ${arch}: ⚠️ WARNING (0 vulnerability findings detected)")
else
target_summary+=(" - ${arch}: ✅ OK (${vuln_cnt} vulnerability findings)")
fi
target_report_count=$((target_report_count + 1))
else
target_ok=false
target_summary+=(" - ${arch}: ❌ FAILED (Output report was empty / 0 bytes)")
fi
else
target_ok=false
err_msg=$(tail -n 2 "$err_log" | tr '\n' ' ' | sed 's/ */ /g')
[ -z "$err_msg" ] && err_msg="Trivy scan exited with non-zero status"
target_summary+=(" - ${arch}: ❌ FAILED (${err_msg})")
fi
done
fi
# 3. Print Comprehensive Diagnostic Overview
echo ""
echo "================================================================================"
echo " CVE VULNERABILITY SCAN DIAGNOSTIC OVERVIEW "
echo "================================================================================"
if [ "$use_inbuild" = "true" ]; then
echo "Scan Strategy: Built-in OCI Attestation (Syft/Docker Scout) with distro detection"
else
echo "Scan Strategy: Direct Trivy Image Scan (Full rootfs & source package mapping)"
fi
echo "--------------------------------------------------------------------------------"
echo "HEAD Image: ${{ inputs.head-image }}"
for line in "${head_summary[@]}"; do
echo "$line"
done
echo ""
echo "TARGET Baseline Image: ${{ inputs.target-image }} (branch: '${{ inputs.target-branch }}')"
for line in "${target_summary[@]}"; do
echo "$line"
done
echo "================================================================================"
echo ""
# 4. Enforce Gating: Both must have succeeded with valid reports
has_error=false
if [ "$head_ok" != "true" ] || [ "$head_report_count" -eq 0 ]; then
echo "::error::Head image CVE analysis failed or produced no reports."
has_error=true
fi
if [ "$target_ok" != "true" ] || [ "$target_report_count" -eq 0 ]; then
echo "::error::Target baseline CVE analysis failed or produced no reports."
has_error=true
fi
if [ "$has_error" = "true" ]; then
echo "::error::Aborting differential analysis: both head and target scans must succeed before diffing."
exit 1
fi
# 5. Run multi-arch set-based diff and generate consolidated full summary report
echo "Both head and target scans succeeded. Computing set-based vulnerability delta..."
if ! node "${{ github.action_path }}/cve-diff.js" \
"cve-reports/target" \
"cve-reports/head" \
"${{ inputs.target-branch }}" \
"${{ inputs.output-summary }}" > "${{ inputs.output-delta }}"; then
echo "::error::Failed to generate CVE differential report"
exit 1
fi
- name: Publish Reports to Step Summary
+172 -45
View File
@@ -24,6 +24,7 @@ const SEVERITY_BADGES = {
*/
function loadReportsForArg(arg) {
const cves = new Map();
const scannedArchs = new Set();
const filePaths = [];
for (const part of arg.split(',').map((s) => s.trim())) {
@@ -41,36 +42,57 @@ function loadReportsForArg(arg) {
}
}
if (filePaths.length === 0) {
console.error(`::error::No CVE JSON report files found for path: ${arg}`);
process.exit(1);
}
let loadedFiles = 0;
for (const filePath of filePaths) {
let stat;
try {
stat = fs.statSync(filePath);
} catch {
continue;
} catch (e) {
console.error(`::error::Failed to access report file ${filePath}: ${e.message}`);
process.exit(1);
}
if (stat.size === 0) {
console.error(`::error::CVE report file is empty (0 bytes): ${filePath}`);
process.exit(1);
}
if (stat.size === 0) continue;
const baseName = path.basename(filePath).toLowerCase();
const baseName = path.basename(filePath, '.json').toLowerCase();
let arch = 'all';
if (baseName.includes('amd64')) {
const cveMatch = baseName.match(/^cve-([a-z0-9_.-]+)$/i);
if (cveMatch) {
arch = cveMatch[1];
} else if (baseName.includes('amd64')) {
arch = 'amd64';
} else if (baseName.includes('arm64') || baseName.includes('aarch64')) {
arch = 'arm64';
}
if (arch !== 'all') {
scannedArchs.add(arch);
}
let data;
try {
const content = fs.readFileSync(filePath, 'utf8');
data = JSON.parse(content);
} catch {
continue;
} catch (e) {
console.error(`::error::Invalid JSON in CVE report file ${filePath}: ${e.message}`);
process.exit(1);
}
if (!data || typeof data !== 'object' || !Array.isArray(data.Results)) {
continue;
if (!data || typeof data !== 'object') {
console.error(`::error::Malformed Trivy report structure in: ${filePath}`);
process.exit(1);
}
for (const result of data.Results) {
const results = Array.isArray(data.Results) ? data.Results : [];
loadedFiles++;
for (const result of results) {
const target = result.Target || result.Type || 'App';
const vulns = Array.isArray(result.Vulnerabilities) ? result.Vulnerabilities : [];
for (const v of vulns) {
@@ -88,15 +110,23 @@ function loadReportsForArg(arg) {
}
}
if (loadedFiles === 0) {
console.error(`::error::No valid CVE reports could be loaded for path: ${arg}`);
process.exit(1);
}
cves.scannedArchs = scannedArchs;
return cves;
}
/**
* Formats a vulnerability record into a Markdown table row.
* @param {object} v
* @param {Set<string>} [allScannedArchs]
* @param {boolean} [showArch]
* @returns {string}
*/
function formatVulnRow(v) {
function formatVulnRow(v, allScannedArchs = new Set(), showArch = true) {
const sev = v.Severity || 'UNKNOWN';
const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`;
const vid = v.VulnerabilityID || '';
@@ -104,22 +134,29 @@ function formatVulnRow(v) {
const link = url ? `[${vid}](${url})` : vid;
const pkg = v.PkgName || '';
const archs = v._archs || new Set(['all']);
let archBadge;
if ((archs.has('amd64') && archs.has('arm64')) || archs.has('all')) {
archBadge = '`all`';
} else {
archBadge = `\`${Array.from(archs).sort().join(', ')}\``;
}
const installed = v.InstalledVersion || '-';
const fixed = v.FixedVersion || '-';
let title = (v.Title || v.Description || '').replace(/\n/g, ' ').trim();
let title = (v.Title || v.Description || '').replace(/\n/g, ' ').replace(/\|/g, '\\|').trim();
if (title.length > 80) {
title = title.slice(0, 77) + '...';
}
return `| ${badge} | ${link} | \`${pkg}\` | ${archBadge} | ${installed} | ${fixed} | ${title} |`;
if (showArch) {
const archs = v._archs || new Set(['all']);
let archBadge;
if (
archs.has('all') ||
(allScannedArchs && allScannedArchs.size > 1 && archs.size === allScannedArchs.size) ||
(archs.has('amd64') && archs.has('arm64'))
) {
archBadge = '`all`';
} else {
archBadge = `\`${Array.from(archs).sort().join(', ')}\``;
}
return `| ${badge} | ${link} | \`${pkg}\` | ${archBadge} | ${installed} | ${fixed} | ${title} |`;
}
return `| ${badge} | ${link} | \`${pkg}\` | ${installed} | ${fixed} | ${title} |`;
}
/**
@@ -130,19 +167,17 @@ function formatVulnRow(v) {
* @returns {string}
*/
function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
if (baseCves.size === 0) {
return [
`### 🔄 Vulnerability Delta (vs \`${baseName}\`)`,
'',
'> [!NOTE]',
`> No baseline scan from \`${baseName}\` was available to compare against.`,
'',
].join('\n');
}
const baseKeys = new Set(baseCves.keys());
const branchKeys = new Set(branchCves.keys());
if (branchCves.size === 0) {
console.error('::warning::Head image contains 0 reported vulnerabilities. Verify that packages and dependencies were recognized.');
}
if (baseCves.size === 0) {
console.error(`::warning::Baseline target (${baseName}) contains 0 reported vulnerabilities.`);
}
const newKeys = [];
let unchangedCount = 0;
for (const key of branchKeys) {
@@ -171,26 +206,52 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
return aPkg.localeCompare(bPkg);
}
const allScannedArchs = new Set([
...(baseCves.scannedArchs || []),
...(branchCves.scannedArchs || []),
]);
const showArch = allScannedArchs.size > 1;
const newVulns = newKeys.map((k) => branchCves.get(k)).sort(sortVulns);
const resolvedVulns = resolvedKeys.map((k) => baseCves.get(k)).sort(sortVulns);
const lines = [
`### 🔄 Vulnerability Delta (vs \`${baseName}\`)`,
'',
'| Metric | Count |',
'| :--- | :--- |',
`| 🚨 **New Vulnerabilities** | ${newVulns.length > 0 ? `🚨 **${newVulns.length}**` : '0'} |`,
`| ✅ **Resolved Vulnerabilities** | ${resolvedVulns.length > 0 ? `✅ **${resolvedVulns.length}**` : '0'} |`,
`| ⚪ **Unchanged Vulnerabilities** | ${unchangedCount} |`,
'',
`#### 🚨 New Vulnerabilities Introduced (${newVulns.length})`,
];
if (branchCves.size === 0 && baseCves.size === 0) {
lines.push('> [!WARNING]');
lines.push('> Both the head image and baseline target have **0 reported vulnerabilities**.');
lines.push('');
} else if (branchCves.size === 0) {
lines.push('> [!WARNING]');
lines.push('> The head image has **0 reported vulnerabilities**. Verify that OS packages and language dependencies were properly indexed in the SBOM.');
lines.push('');
} else if (baseCves.size === 0) {
lines.push('> [!WARNING]');
lines.push(`> The baseline image (\`${baseName}\`) has **0 reported vulnerabilities**.`);
lines.push('');
}
lines.push('| Metric | Count |');
lines.push('| :--- | :--- |');
lines.push(`| 🚨 **New Vulnerabilities** | ${newVulns.length > 0 ? `🚨 **${newVulns.length}**` : '0'} |`);
lines.push(`| ✅ **Resolved Vulnerabilities** | ${resolvedVulns.length > 0 ? `✅ **${resolvedVulns.length}**` : '0'} |`);
lines.push(`| ⚪ **Unchanged Vulnerabilities** | ${unchangedCount} |`);
lines.push('');
lines.push(`#### 🚨 New Vulnerabilities Introduced (${newVulns.length})`);
if (newVulns.length > 0) {
lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |');
if (showArch) {
lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |');
} else {
lines.push('| Severity | CVE | Package | Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- |');
}
for (const v of newVulns) {
lines.push(formatVulnRow(v));
lines.push(formatVulnRow(v, allScannedArchs, showArch));
}
} else {
lines.push('✅ No new vulnerabilities introduced in this build.');
@@ -199,10 +260,15 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
lines.push(`#### ✅ Resolved / Removed Vulnerabilities (${resolvedVulns.length})`);
if (resolvedVulns.length > 0) {
lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |');
if (showArch) {
lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |');
} else {
lines.push('| Severity | CVE | Package | Previously Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- |');
}
for (const v of resolvedVulns) {
lines.push(formatVulnRow(v));
lines.push(formatVulnRow(v, allScannedArchs, showArch));
}
} else {
lines.push('⚪ No existing vulnerabilities were resolved.');
@@ -212,22 +278,82 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
return lines.join('\n');
}
/**
* Renders a full markdown summary report of all vulnerabilities in a scan.
* @param {Map<string, object>} cves
* @returns {string}
*/
function renderFullSummary(cves) {
const allScannedArchs = cves.scannedArchs || new Set();
const showArch = allScannedArchs.size > 1;
const lines = [
'### 🛡️ Security Scan Report',
'',
];
if (cves.size === 0) {
lines.push('✅ No vulnerabilities found.');
lines.push('');
return lines.join('\n');
}
function sortVulns(a, b) {
const aSev = a.Severity || 'UNKNOWN';
const bSev = b.Severity || 'UNKNOWN';
const aIdx = SEVERITY_ORDER.indexOf(aSev) !== -1 ? SEVERITY_ORDER.indexOf(aSev) : 99;
const bIdx = SEVERITY_ORDER.indexOf(bSev) !== -1 ? SEVERITY_ORDER.indexOf(bSev) : 99;
if (aIdx !== bIdx) return aIdx - bIdx;
const aPkg = a.PkgName || '';
const bPkg = b.PkgName || '';
return aPkg.localeCompare(bPkg);
}
const sortedVulns = Array.from(cves.values()).sort(sortVulns);
lines.push(`| Metric | Count |`);
lines.push(`| :--- | :--- |`);
lines.push(`| Total Vulnerabilities | **${sortedVulns.length}** |`);
lines.push('');
if (showArch) {
lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |');
} else {
lines.push('| Severity | CVE | Package | Installed | Fixed In | Title |');
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- |');
}
for (const v of sortedVulns) {
lines.push(formatVulnRow(v, allScannedArchs, showArch));
}
lines.push('');
return lines.join('\n');
}
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: cve-diff.js <base_report(s)> <branch_report(s)> [base_name]');
console.error('Usage: cve-diff.js <base_report(s)> <branch_report(s)> [base_name] [summary_output_path]');
process.exit(1);
}
const baseArg = args[0];
const branchArg = args[1];
const baseName = args[2] || 'main';
const summaryOutputPath = args[3];
const baseCves = loadReportsForArg(baseArg);
const branchCves = loadReportsForArg(branchArg);
const output = renderMarkdownDiff(baseCves, branchCves, baseName);
console.log(output);
if (summaryOutputPath) {
const summaryOutput = renderFullSummary(branchCves);
fs.writeFileSync(summaryOutputPath, summaryOutput, 'utf8');
}
}
if (require.main === module) {
@@ -238,4 +364,5 @@ module.exports = {
loadReportsForArg,
formatVulnRow,
renderMarkdownDiff,
renderFullSummary,
};
+3 -3
View File
@@ -6,11 +6,11 @@
"customManagers": [
{
"customType": "regex",
"fileMatch": [
"^action\\.ya?ml$"
"managerFilePatterns": [
"/^action\\.ya?ml$/"
],
"matchStrings": [
"default:\\s*(?<currentValue>[0-9]+\\.[0-9]+\\.[0-9]+)\\s*#\\s*trivy-version"
"TRIVY_VERSION:\\s*(?<currentValue>[0-9]+\\.[0-9]+\\.[0-9]+)\\s*#\\s*trivy-version"
],
"depNameTemplate": "aquasecurity/trivy",
"datasourceTemplate": "github-releases",
+111
View File
@@ -0,0 +1,111 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const {
loadReportsForArg,
formatVulnRow,
renderMarkdownDiff,
renderFullSummary,
} = require('../cve-diff.js');
const FIXTURES_DIR = path.join(__dirname, 'fixtures');
test('loadReportsForArg loads valid reports and extracts multi-arch metadata', () => {
const targetDir = path.join(FIXTURES_DIR, 'target');
const cves = loadReportsForArg(targetDir);
assert.equal(cves.size, 2);
assert.ok(cves.has('CVE-2023-0001:::glibc'));
assert.ok(cves.has('CVE-2023-0002:::openssl'));
const glibc = cves.get('CVE-2023-0001:::glibc');
assert.equal(glibc.Severity, 'CRITICAL');
assert.deepEqual(Array.from(glibc._archs).sort(), ['amd64', 'arm64']);
assert.deepEqual(Array.from(cves.scannedArchs).sort(), ['amd64', 'arm64']);
const openssl = cves.get('CVE-2023-0002:::openssl');
assert.equal(openssl.Severity, 'HIGH');
assert.deepEqual(Array.from(openssl._archs), ['amd64']);
});
test('loadReportsForArg handles reports with missing or null Results array without crashing', () => {
const emptyDir = path.join(FIXTURES_DIR, 'empty');
const cves = loadReportsForArg(emptyDir);
assert.equal(cves.size, 0);
});
test('renderMarkdownDiff produces correct introduced, resolved, and unchanged sets', () => {
const targetDir = path.join(FIXTURES_DIR, 'target');
const headDir = path.join(FIXTURES_DIR, 'head');
const baseCves = loadReportsForArg(targetDir);
const branchCves = loadReportsForArg(headDir);
const diff = renderMarkdownDiff(baseCves, branchCves, 'main');
// Introduced: CVE-2023-0003 (curl)
assert.ok(diff.includes('New Vulnerabilities Introduced (1)'));
assert.ok(diff.includes('CVE-2023-0003'));
assert.ok(diff.includes('`curl`'));
// Resolved: CVE-2023-0001 (glibc)
assert.ok(diff.includes('Resolved / Removed Vulnerabilities (1)'));
assert.ok(diff.includes('CVE-2023-0001'));
assert.ok(diff.includes('`glibc`'));
// Unchanged count
assert.ok(diff.includes('| ⚪ **Unchanged Vulnerabilities** | 1 |'));
// Multi-arch is present in fixtures (amd64, arm64), so Arch column should be shown
assert.ok(diff.includes('| Arch |'));
});
test('renderMarkdownDiff hides Arch column when single architecture is scanned', () => {
const targetSingle = path.join(FIXTURES_DIR, 'target', 'cve-amd64.json');
const headSingle = path.join(FIXTURES_DIR, 'head', 'cve-amd64.json');
const baseCves = loadReportsForArg(targetSingle);
const branchCves = loadReportsForArg(headSingle);
const diff = renderMarkdownDiff(baseCves, branchCves, 'main');
assert.ok(!diff.includes('| Arch |'));
assert.ok(diff.includes('New Vulnerabilities Introduced (1)'));
assert.ok(diff.includes('Resolved / Removed Vulnerabilities (1)'));
});
test('renderFullSummary outputs summary table and lists all vulnerabilities', () => {
const headDir = path.join(FIXTURES_DIR, 'head');
const headCves = loadReportsForArg(headDir);
const summary = renderFullSummary(headCves);
assert.ok(summary.includes('### 🛡️ Security Scan Report'));
assert.ok(summary.includes('| Total Vulnerabilities | **2** |'));
assert.ok(summary.includes('CVE-2023-0002'));
assert.ok(summary.includes('CVE-2023-0003'));
});
test('formatVulnRow properly formats badge, architecture tag, and escape pipes', () => {
const vuln = {
Severity: 'CRITICAL',
VulnerabilityID: 'CVE-2023-0001',
PrimaryURL: 'https://avd.aquasec.com/nvd/cve-2023-0001',
PkgName: 'glibc',
InstalledVersion: '2.31',
FixedVersion: '2.32',
Title: 'A vulnerability with | pipe symbol',
_archs: new Set(['amd64']),
};
const allArchs = new Set(['amd64', 'arm64']);
const rowWithArch = formatVulnRow(vuln, allArchs, true);
assert.ok(rowWithArch.includes('🔴 CRITICAL'));
assert.ok(rowWithArch.includes('`amd64`'));
assert.ok(rowWithArch.includes('\\|')); // Pipe escaped
const rowWithoutArch = formatVulnRow(vuln, allArchs, false);
assert.ok(!rowWithoutArch.includes('`amd64`'));
});
+8
View File
@@ -0,0 +1,8 @@
{
"SchemaVersion": 2,
"ArtifactName": "debian:bullseye-slim",
"ArtifactType": "spdx",
"Metadata": {
"OS": null
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"SchemaVersion": 2,
"ArtifactName": "debian:bookworm-slim",
"ArtifactType": "container_image",
"Results": [
{
"Target": "debian:bookworm-slim (debian 12)",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2023-0002",
"PkgName": "openssl",
"InstalledVersion": "1.1.1n-0+deb11u4",
"FixedVersion": "1.1.1n-0+deb11u5",
"Severity": "HIGH",
"Title": "Timing attack in OpenSSL"
},
{
"VulnerabilityID": "CVE-2023-0003",
"PkgName": "curl",
"InstalledVersion": "7.88.1-10",
"FixedVersion": "7.88.1-10+deb12u1",
"Severity": "MEDIUM",
"Title": "Buffer overflow in curl"
}
]
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"SchemaVersion": 2,
"ArtifactName": "debian:bookworm-slim",
"ArtifactType": "container_image",
"Results": [
{
"Target": "debian:bookworm-slim (debian 12)",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2023-0003",
"PkgName": "curl",
"InstalledVersion": "7.88.1-10",
"FixedVersion": "7.88.1-10+deb12u1",
"Severity": "MEDIUM",
"Title": "Buffer overflow in curl"
}
]
}
]
}
+36
View File
@@ -0,0 +1,36 @@
{
"SchemaVersion": 2,
"ArtifactName": "debian:bullseye-slim",
"ArtifactType": "container_image",
"Metadata": {
"OS": {
"Family": "debian",
"Name": "11"
}
},
"Results": [
{
"Target": "debian:bullseye-slim (debian 11)",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2023-0001",
"PkgName": "glibc",
"InstalledVersion": "2.31-13+deb11u5",
"FixedVersion": "2.31-13+deb11u6",
"Severity": "CRITICAL",
"Title": "Memory corruption in glibc"
},
{
"VulnerabilityID": "CVE-2023-0002",
"PkgName": "openssl",
"InstalledVersion": "1.1.1n-0+deb11u4",
"FixedVersion": "1.1.1n-0+deb11u5",
"Severity": "HIGH",
"Title": "Timing attack in OpenSSL"
}
]
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"SchemaVersion": 2,
"ArtifactName": "debian:bullseye-slim",
"ArtifactType": "container_image",
"Results": [
{
"Target": "debian:bullseye-slim (debian 11)",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2023-0001",
"PkgName": "glibc",
"InstalledVersion": "2.31-13+deb11u5",
"FixedVersion": "2.31-13+deb11u6",
"Severity": "CRITICAL",
"Title": "Memory corruption in glibc"
}
]
}
]
}