11 Commits
Author SHA1 Message Date
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 / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 2m38s
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
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 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 / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m35s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 2m59s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 3m6s
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 / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 2m42s
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
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 / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m11s
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
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 / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m5s
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
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 / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 3m40s
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
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 / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 5m58s
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
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=true) (pull_request) Successful in 3m0s
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
2026-09-14 22:27:49 +02:00
Philipp 40209bbeed style: format test workflow to comply with pretty-format-yaml
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Failing after 4m58s
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
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=true) (pull_request) Failing after 9m33s
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
2026-09-14 03:03:32 +02:00
10 changed files with 373 additions and 17 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@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
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: 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
+1
View File
@@ -13,3 +13,4 @@ cve-*.json
cve-delta.md cve-delta.md
cve-summary.md cve-summary.md
node_modules/ node_modules/
!test/fixtures/**
+27 -13
View File
@@ -7,13 +7,12 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p
## ✨ Features ## ✨ Features
- **Dynamic Multi-Platform Support**: Automatically discovers and scans all architectures present in container images, or iterates over an explicit list of target architectures. - **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. - **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.
- **Graceful Fallbacks**: If SBOM attestations are missing for any platform, automatically falls back to generating SBOMs directly with Trivy per architecture.
- **Set-Based Differential Analysis**: Categorizes vulnerabilities into: - **Set-Based Differential Analysis**: Categorizes vulnerabilities into:
- 🔴 **Introduced Vulnerabilities** (newly added in this branch / commit) - 🔴 **Introduced Vulnerabilities** (newly added in this branch / commit)
- 🟢 **Fixed Vulnerabilities** (present in target baseline, resolved in this branch) - 🟢 **Fixed Vulnerabilities** (present in target baseline, resolved in this branch)
- ⚪ **Unchanged Vulnerabilities** (pre-existing in target baseline) - ⚪ **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-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` | | `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` | | `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 (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` |
--- ---
@@ -48,21 +47,36 @@ This action supports two modes for obtaining SBOMs:
### 1. Direct Image Scanning with Trivy (`use-inbuild-sbom: false` — Default & Recommended) ### 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: 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). - **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. - **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`: 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. - **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`). - **Underlying Generator (Syft / Docker Scout)**: Docker Buildx (`sbom: true`) uses BuildKit's embedded Syft scanner (`moby/buildkit-syft-scanner`).
- **Known Limitations with Trivy**: - **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.
- **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. - **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.
- **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. - **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.
- **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. ### ⚖️ 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] > [!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.
--- ---
+5 -4
View File
@@ -84,14 +84,15 @@ function loadReportsForArg(arg) {
process.exit(1); process.exit(1);
} }
if (!data || typeof data !== 'object' || !Array.isArray(data.Results)) { if (!data || typeof data !== 'object') {
console.error(`::error::Malformed Trivy report structure (missing Results array) in: ${filePath}`); console.error(`::error::Malformed Trivy report structure in: ${filePath}`);
process.exit(1); process.exit(1);
} }
const results = Array.isArray(data.Results) ? data.Results : [];
loadedFiles++; loadedFiles++;
for (const result of data.Results) { for (const result of results) {
const target = result.Target || result.Type || 'App'; const target = result.Target || result.Type || 'App';
const vulns = Array.isArray(result.Vulnerabilities) ? result.Vulnerabilities : []; const vulns = Array.isArray(result.Vulnerabilities) ? result.Vulnerabilities : [];
for (const v of vulns) { for (const v of vulns) {
@@ -135,7 +136,7 @@ function formatVulnRow(v, allScannedArchs = new Set(), showArch = true) {
const installed = v.InstalledVersion || '-'; const installed = v.InstalledVersion || '-';
const fixed = v.FixedVersion || '-'; 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) { if (title.length > 80) {
title = title.slice(0, 77) + '...'; title = title.slice(0, 77) + '...';
} }
+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"
}
]
}
]
}