From 44c4b5a304e3d2a912f7e59aa0d8b55fc9ef1c1a Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 03:03:32 +0200 Subject: [PATCH 1/8] ci: add matrix test workflow for debian slim images in .gitea --- .gitea/workflows/test.yaml | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .gitea/workflows/test.yaml diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml new file mode 100644 index 0000000..8478c50 --- /dev/null +++ b/.gitea/workflows/test.yaml @@ -0,0 +1,87 @@ +name: CI Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + test: + name: ${{ matrix.image_set.name }} (sbom=${{ matrix.use_inbuild_sbom }}) + 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@v4 + + - 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)." -- 2.54.0 From 40209bbeed307bd302e5cdf2e99cbbff162c6280 Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 03:06:49 +0200 Subject: [PATCH 2/8] style: format test workflow to comply with pretty-format-yaml --- .gitea/workflows/test.yaml | 174 ++++++++++++++++++------------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 8478c50..60cda89 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -1,87 +1,87 @@ -name: CI Tests - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - test: - name: ${{ matrix.image_set.name }} (sbom=${{ matrix.use_inbuild_sbom }}) - 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@v4 - - - 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: CI Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + test: + name: ${{ matrix.image_set.name }} (sbom=${{ matrix.use_inbuild_sbom }}) + 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@v4 + + - 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)." -- 2.54.0 From d89fb25d7b2eadff75595beedc37ba7f78cc92da Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 22:27:49 +0200 Subject: [PATCH 3/8] test: add unit tests for cve-diff and fallback for in-build sbom scans --- .gitea/workflows/test.yaml | 16 ++++ .gitignore | 1 + action.yaml | 71 +++++++++++++++++- cve-diff.js | 9 ++- test/cve-diff.test.js | 111 ++++++++++++++++++++++++++++ test/fixtures/empty/cve-amd64.json | 8 ++ test/fixtures/head/cve-amd64.json | 30 ++++++++ test/fixtures/head/cve-arm64.json | 22 ++++++ test/fixtures/target/cve-amd64.json | 36 +++++++++ test/fixtures/target/cve-arm64.json | 22 ++++++ 10 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 test/cve-diff.test.js create mode 100644 test/fixtures/empty/cve-amd64.json create mode 100644 test/fixtures/head/cve-amd64.json create mode 100644 test/fixtures/head/cve-arm64.json create mode 100644 test/fixtures/target/cve-amd64.json create mode 100644 test/fixtures/target/cve-arm64.json diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 60cda89..0d30e82 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -46,6 +46,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Run JS Unit Tests + shell: bash + run: node --test + - name: Test CVE Differential Action uses: ./ with: @@ -85,3 +89,15 @@ jobs: fi fi echo "✅ All assertions passed (including >= 3 CVEs verification)." + + - name: Archive SBOMs and CVE Reports + if: always() + uses: actions/upload-artifact@v4 + 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 diff --git a/.gitignore b/.gitignore index 363f8b8..e8a1dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ cve-*.json cve-delta.md cve-summary.md node_modules/ +!test/fixtures/** diff --git a/action.yaml b/action.yaml index 2257cd4..83df1e9 100644 --- a/action.yaml +++ b/action.yaml @@ -168,6 +168,7 @@ runs: 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/head/sbom-${arch_tag}.spdx.json" ]; then @@ -184,6 +185,7 @@ runs: 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 @@ -201,6 +203,7 @@ runs: 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 @@ -217,6 +220,7 @@ runs: 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 @@ -304,6 +308,7 @@ runs: head_summary=() target_summary=() + use_inbuild="${{ inputs.use-inbuild-sbom }}" # 1. Scan Head SBOMs if [ ${#head_sboms[@]} -eq 0 ]; then @@ -316,9 +321,40 @@ runs: err_log="cve-reports/head/cve-${arch}.err" echo "Scanning head SBOM for architecture: $arch" + scan_success=false if trivy sbom --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 - vuln_cnt=$(grep -o '"VulnerabilityID"' "$out_json" 2>/dev/null | wc -l || echo 0) 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)") @@ -350,9 +386,40 @@ runs: err_log="cve-reports/target/cve-${arch}.err" echo "Scanning target SBOM for architecture: $arch" + scan_success=false if trivy sbom --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 - vuln_cnt=$(grep -o '"VulnerabilityID"' "$out_json" 2>/dev/null | wc -l || echo 0) 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)") diff --git a/cve-diff.js b/cve-diff.js index f0de854..b0cac43 100755 --- a/cve-diff.js +++ b/cve-diff.js @@ -84,14 +84,15 @@ function loadReportsForArg(arg) { process.exit(1); } - if (!data || typeof data !== 'object' || !Array.isArray(data.Results)) { - console.error(`::error::Malformed Trivy report structure (missing Results array) in: ${filePath}`); + if (!data || typeof data !== 'object') { + console.error(`::error::Malformed Trivy report structure in: ${filePath}`); process.exit(1); } + const results = Array.isArray(data.Results) ? data.Results : []; loadedFiles++; - for (const result of data.Results) { + 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) { @@ -135,7 +136,7 @@ function formatVulnRow(v, allScannedArchs = new Set(), showArch = true) { 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) + '...'; } diff --git a/test/cve-diff.test.js b/test/cve-diff.test.js new file mode 100644 index 0000000..0b07766 --- /dev/null +++ b/test/cve-diff.test.js @@ -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`')); +}); diff --git a/test/fixtures/empty/cve-amd64.json b/test/fixtures/empty/cve-amd64.json new file mode 100644 index 0000000..8573fa6 --- /dev/null +++ b/test/fixtures/empty/cve-amd64.json @@ -0,0 +1,8 @@ +{ + "SchemaVersion": 2, + "ArtifactName": "debian:bullseye-slim", + "ArtifactType": "spdx", + "Metadata": { + "OS": null + } +} diff --git a/test/fixtures/head/cve-amd64.json b/test/fixtures/head/cve-amd64.json new file mode 100644 index 0000000..caf2619 --- /dev/null +++ b/test/fixtures/head/cve-amd64.json @@ -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" + } + ] + } + ] +} diff --git a/test/fixtures/head/cve-arm64.json b/test/fixtures/head/cve-arm64.json new file mode 100644 index 0000000..7ef472c --- /dev/null +++ b/test/fixtures/head/cve-arm64.json @@ -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" + } + ] + } + ] +} diff --git a/test/fixtures/target/cve-amd64.json b/test/fixtures/target/cve-amd64.json new file mode 100644 index 0000000..6b65a9c --- /dev/null +++ b/test/fixtures/target/cve-amd64.json @@ -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" + } + ] + } + ] +} diff --git a/test/fixtures/target/cve-arm64.json b/test/fixtures/target/cve-arm64.json new file mode 100644 index 0000000..cf6373f --- /dev/null +++ b/test/fixtures/target/cve-arm64.json @@ -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" + } + ] + } + ] +} -- 2.54.0 From f31be5eef6a5bd41c4d616a39d64e8da77ae930d Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 22:29:21 +0200 Subject: [PATCH 4/8] ci: move unit tests into dedicated unit-test job --- .gitea/workflows/test.yaml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 0d30e82..f26619c 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -10,8 +10,20 @@ on: workflow_dispatch: jobs: + unit-test: + name: JS Unit Tests + runs-on: k8s + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - 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 @@ -46,10 +58,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Run JS Unit Tests - shell: bash - run: node --test - - name: Test CVE Differential Action uses: ./ with: -- 2.54.0 From 790c1aa3dee2a08a6d2810ae53813e63c102bad1 Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 22:34:12 +0200 Subject: [PATCH 5/8] docs: update README with in-build fallback and unit test details --- README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index eb6ca08..970aca1 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,12 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p ## ✨ Features - **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**: Uses `docker buildx imagetools inspect` to extract platform-specific SBOMs directly from registry manifest indices in seconds without downloading layer blobs. -- **Graceful Fallbacks**: If SBOM attestations are missing for any platform, automatically falls back to generating SBOMs directly with Trivy per architecture. +- **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`). --- @@ -28,7 +27,7 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p | `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, generates SBOMs directly with Trivy | No | `false` | +| `use-inbuild-sbom` | Whether to inspect and use built-in image SBOM attestations. If false, generates SBOMs directly with Trivy. If true, extracts attestations and automatically falls back to Trivy image generation if 0 CVEs are detected | No | `false` | --- @@ -51,18 +50,15 @@ By default, the action uses `trivy image --format spdx-json` to generate the SBO - **Accurate OS & Language Package Detection**: Successfully 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 (`use-inbuild-sbom: true`) +### 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 Buildx (`sbom: true`) uses BuildKit's embedded Syft scanner (`moby/buildkit-syft-scanner`). -- **Known Limitations with Trivy**: - - **Testing/Rolling Distributions**: Embedded Syft versions may not recognize newer or unreleased distributions (such as Debian 13 "Trixie"), recording the OS as `family="none"` and skipping OS package vulnerability matching. - - **Multi-Stage Builds**: Analyzing layer diffs rather than the flattened container rootfs can cause package database paths to be omitted or detached from the OS manifest. - - **Schema/PURL Discrepancies**: Syft and Trivy express certain package qualifiers and namespaces differently in SPDX. When Trivy scans a Syft SPDX file that lacks explicit distro qualifiers, Trivy will silently skip OS vulnerability checks. - - **Differential Skew**: If a `head` image uses an incomplete Syft SBOM (detecting only language packages) while `target` is scanned directly with Trivy (detecting 400+ OS packages), the differential report will erroneously show hundreds of "resolved" CVEs. +- **Underlying Generator (Syft / Docker Scout)**: Docker Buildx (`sbom: true`) uses BuildKit's embedded Syft scanner (`moby/buildkit-syft-scanner`). +- **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. > [!TIP] -> If your builds run on stable base images where BuildKit's Syft scanner accurately captures all OS packages, you can set `use-inbuild-sbom: 'true'` to speed up CI runs. Otherwise, leave it as `false` to ensure consistent and comprehensive scans. +> 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. --- -- 2.54.0 From 3b9043a746bb1b3c43ebfc45b4beaa9cc659748a Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 23:25:09 +0200 Subject: [PATCH 6/8] feat: detect distro from in-build SBOM PURLs and pass --distro to trivy --- action.yaml | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/action.yaml b/action.yaml index 83df1e9..5142aa4 100644 --- a/action.yaml +++ b/action.yaml @@ -321,8 +321,25 @@ runs: err_log="cve-reports/head/cve-${arch}.err" echo "Scanning head SBOM for architecture: $arch" + distro_arg="" + if [ "$use_inbuild" = "true" ]; then + detected_distro=$(node -e ' + const fs = require("fs"); + const content = fs.readFileSync(process.argv[1], "utf8"); + const nameMatch = content.match(/[?&]os_name=([^&"\s]+)/); + const verMatch = content.match(/[?&]os_version=([^&"\s]+)/); + if (nameMatch && verMatch) { + console.log(`${nameMatch[1]}/${verMatch[1]}`); + } + ' "$f" 2>/dev/null || true) + 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 --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then + if trivy sbom $distro_arg --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then scan_success=true fi @@ -386,8 +403,25 @@ runs: err_log="cve-reports/target/cve-${arch}.err" echo "Scanning target SBOM for architecture: $arch" + distro_arg="" + if [ "$use_inbuild" = "true" ]; then + detected_distro=$(node -e ' + const fs = require("fs"); + const content = fs.readFileSync(process.argv[1], "utf8"); + const nameMatch = content.match(/[?&]os_name=([^&"\s]+)/); + const verMatch = content.match(/[?&]os_version=([^&"\s]+)/); + if (nameMatch && verMatch) { + console.log(`${nameMatch[1]}/${verMatch[1]}`); + } + ' "$f" 2>/dev/null || true) + 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 --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then + if trivy sbom $distro_arg --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then scan_success=true fi -- 2.54.0 From 64747ec4665a214f42483012f669d9558e44c21a Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Mon, 14 Sep 2026 23:37:07 +0200 Subject: [PATCH 7/8] fix: robustly parse distro from in-build SBOM package PURLs --- action.yaml | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/action.yaml b/action.yaml index 5142aa4..c76ede5 100644 --- a/action.yaml +++ b/action.yaml @@ -310,6 +310,31 @@ runs: target_summary=() use_inbuild="${{ inputs.use-inbuild-sbom }}" + 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 @@ -323,15 +348,7 @@ runs: echo "Scanning head SBOM for architecture: $arch" distro_arg="" if [ "$use_inbuild" = "true" ]; then - detected_distro=$(node -e ' - const fs = require("fs"); - const content = fs.readFileSync(process.argv[1], "utf8"); - const nameMatch = content.match(/[?&]os_name=([^&"\s]+)/); - const verMatch = content.match(/[?&]os_version=([^&"\s]+)/); - if (nameMatch && verMatch) { - console.log(`${nameMatch[1]}/${verMatch[1]}`); - } - ' "$f" 2>/dev/null || true) + 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}" @@ -405,15 +422,7 @@ runs: echo "Scanning target SBOM for architecture: $arch" distro_arg="" if [ "$use_inbuild" = "true" ]; then - detected_distro=$(node -e ' - const fs = require("fs"); - const content = fs.readFileSync(process.argv[1], "utf8"); - const nameMatch = content.match(/[?&]os_name=([^&"\s]+)/); - const verMatch = content.match(/[?&]os_version=([^&"\s]+)/); - if (nameMatch && verMatch) { - console.log(`${nameMatch[1]}/${verMatch[1]}`); - } - ' "$f" 2>/dev/null || true) + 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}" -- 2.54.0 From def42c37a88c4e19a8e282aa97fcb5aa0f5fe1e6 Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Tue, 15 Sep 2026 00:12:42 +0200 Subject: [PATCH 8/8] docs: document trade-offs and differences between in-build and direct scanning --- README.md | 22 ++++++++++++++++++++-- action.yaml | 14 +++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 970aca1..9de5e11 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p | `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, generates SBOMs directly with Trivy. If true, extracts attestations and automatically falls back to Trivy image generation if 0 CVEs are detected | No | `false` | +| `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` | --- @@ -47,16 +47,34 @@ 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**: Successfully catalogs both OS packages (Debian, Alpine, RHEL, etc.) and application dependencies (Python, Node.js, Go, etc.). +- **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. diff --git a/action.yaml b/action.yaml index c76ede5..34865c6 100644 --- a/action.yaml +++ b/action.yaml @@ -29,7 +29,7 @@ inputs: required: false default: 'true' use-inbuild-sbom: - description: Whether to inspect and use built-in image SBOM attestations. If false, generates SBOM directly from images with Trivy. + 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' @@ -158,6 +158,12 @@ runs: fi 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 for platform in "${PLATFORMS[@]}"; do arch_tag=$(echo "$platform" | sed -e 's#^[a-zA-Z0-9_-]*/##' -e 's#/#-#g') @@ -488,6 +494,12 @@ runs: 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" -- 2.54.0