16 Commits
Author SHA1 Message Date
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
4 changed files with 516 additions and 139 deletions

No files matched your search

+33 -11
View File
@@ -6,9 +6,9 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p
## ✨ Features ## ✨ Features
- **Multi-Platform Support**: Automatically detects and extracts SPDX SBOM attestations for multi arch images. - **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 SBOMs directly from registry manifest indices in seconds without downloading layer blobs. - **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 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! - **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)
@@ -20,14 +20,15 @@ A self-contained Gitea and GitHub Actions composite action that extracts multi-p
## 📥 Inputs ## 📥 Inputs
| Input | Description | Required | Default | | Input | Description | Required | Default |
| :--- | :--- | :---: | :--- | | :--- | :--- | :--- | :--- |
| `head-image` | Head container image reference to scan (e.g. `registry.example.com/repo:git-sha`) | **Yes** | — | | `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-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` | | `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-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` |
--- ---
@@ -40,6 +41,31 @@ 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**: 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`)
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.
> [!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.
---
## 🚀 Usage Example ## 🚀 Usage Example
```yaml ```yaml
@@ -47,20 +73,16 @@ jobs:
build-and-scan: build-and-scan:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build and Push Head Container Image - name: Build and Push Head Container Image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
sbom: true
platforms: linux/amd64,linux/arm64
tags: registry.example.com/my-app:git-${{ github.sha }} tags: registry.example.com/my-app:git-${{ github.sha }}
- name: Compare Vulnerabilities (Head vs Target) - 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: with:
head-image: registry.example.com/my-app:git-${{ github.sha }} head-image: registry.example.com/my-app:git-${{ github.sha }}
target-image: registry.example.com/my-app:latest target-image: registry.example.com/my-app:latest
+314 -85
View File
@@ -12,10 +12,10 @@ inputs:
description: Target branch name for delta heading (e.g. main) description: Target branch name for delta heading (e.g. main)
required: false required: false
default: main default: main
trivy-version: architectures:
description: Trivy binary version to install and verify 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 required: false
default: 0.74.0 # trivy-version default: ''
output-delta: output-delta:
description: Output markdown file for CVE delta report description: Output markdown file for CVE delta report
required: false required: false
@@ -28,6 +28,10 @@ inputs:
description: Whether to append the generated reports to the job summary ($GITHUB_STEP_SUMMARY) description: Whether to append the generated reports to the job summary ($GITHUB_STEP_SUMMARY)
required: false required: false
default: 'true' 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.
required: false
default: 'false'
outputs: outputs:
delta-report: delta-report:
@@ -40,10 +44,14 @@ outputs:
runs: runs:
using: composite using: composite
steps: steps:
- name: Prepare Cache Keys - name: Prepare Cache Keys and Environment
id: vars id: vars
shell: bash shell: bash
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" env:
TRIVY_VERSION: 0.74.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) - name: Cache Trivy (Binary & Vulnerability DB)
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
@@ -51,30 +59,32 @@ runs:
path: | path: |
~/.cache/trivy-bin ~/.cache/trivy-bin
~/.cache/trivy ~/.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: | restore-keys: |
trivy-${{ inputs.trivy-version }}-${{ runner.os }}-${{ runner.arch }}- trivy-${{ steps.vars.outputs.version }}-${{ runner.os }}-${{ runner.arch }}-
- name: Install and Verify Trivy - name: Install and Verify Trivy
shell: bash shell: bash
run: | run: |
mkdir -p "$HOME/.cache/trivy-bin" "$HOME/.local/bin" set -euo pipefail
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
ARCH=$(uname -m) mkdir -p "$HOME/.local/bin" "$HOME/.cache/trivy-bin"
case "$ARCH" in echo "$HOME/.local/bin" >> "$GITHUB_PATH"
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) T_ARCH="64bit" ;; x86_64) T_ARCH="64bit" ;;
aarch64|arm64) T_ARCH="ARM64" ;; aarch64|arm64) T_ARCH="ARM64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;; *) echo "::error::Unsupported architecture: ${ARCH}"; exit 1 ;;
esac esac
tarball="trivy_${{ inputs.trivy-version }}_Linux-${T_ARCH}.tar.gz"
checksums_file="${{ github.action_path }}/checksums.txt" 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 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." echo "Cached Trivy archive verified against lockfile."
else else
echo "Archive missing or checksum mismatch; downloading..." 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 -) (cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -)
fi fi
@@ -85,108 +95,327 @@ runs:
shell: bash shell: bash
run: | run: |
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
export TRIVY_SKIP_VERSION_CHECK="true"
mkdir -p sboms/head sboms/target mkdir -p sboms/head sboms/target
cleanup_file() { cleanup_file() {
local f="$1" local f="$1"
if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then if [ -f "$f" ]; then
rm -f "$f" if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then
rm -f "$f"
fi
fi fi
} }
# Extract platform SBOMs for head image head_sbom_err=""
echo "Extracting platform SBOMs for head image: ${{ inputs.head-image }}..." target_sbom_err=""
docker buildx imagetools inspect "${{ inputs.head-image }}" \ use_inbuild="${{ inputs.use-inbuild-sbom }}"
--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
docker buildx imagetools inspect "${{ inputs.head-image }}" \ # Determine target platforms / architectures
--format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sboms/head/sbom-arm64.spdx.json 2>/dev/null || true PLATFORMS=()
cleanup_file sboms/head/sbom-arm64.spdx.json 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 discovered=$(node -e '
if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ]; then const raw1 = process.argv[1];
docker buildx imagetools inspect "${{ inputs.head-image }}" \ const raw2 = process.argv[2];
--format '{{ json .SBOM.SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true const set = new Set();
cleanup_file sboms/head/sbom-amd64.spdx.json 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 fi
# Fallback to generating head SBOM with Trivy if attestation missing echo "Configured platforms: ${PLATFORMS[*]:-(single platform fallback)}"
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..." for platform in "${PLATFORMS[@]}"; do
trivy image --format spdx-json --output sboms/head/sbom-amd64.spdx.json "${{ inputs.head-image }}" || true arch_tag=$(echo "$platform" | sed -e 's#^[a-zA-Z0-9_-]*/##' -e 's#/#-#g')
cleanup_file sboms/head/sbom-amd64.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"
fi
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"
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
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 fi
# Extract platform SBOMs for target image if [ -z "$(find sboms/target -type f -name 'sbom-*.spdx.json' 2>/dev/null)" ]; then
echo "Extracting platform SBOMs for target image: ${{ inputs.target-image }}..." if [ "$use_inbuild" = "true" ]; then
docker buildx imagetools inspect "${{ 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 --format '{{ json .SBOM.SPDX }}' > sboms/target/sbom-default.spdx.json 2>/dev/null || true
cleanup_file sboms/target/sbom-amd64.spdx.json cleanup_file sboms/target/sbom-default.spdx.json
fi
docker buildx imagetools inspect "${{ inputs.target-image }}" \ if [ ! -f sboms/target/sbom-default.spdx.json ]; then
--format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sboms/target/sbom-arm64.spdx.json 2>/dev/null || true echo "Generating default target SBOM directly with Trivy..."
cleanup_file sboms/target/sbom-arm64.spdx.json 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"
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then fi
docker buildx imagetools inspect "${{ inputs.target-image }}" \ cleanup_file sboms/target/sbom-default.spdx.json
--format '{{ json .SBOM.SPDX }}' > sboms/target/sbom-amd64.spdx.json 2>/dev/null || true fi
cleanup_file sboms/target/sbom-amd64.spdx.json
fi fi
# Fallback to generating target SBOM with Trivy if attestation missing shopt -s nullglob
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then head_files=(sboms/head/sbom-*.spdx.json)
echo "Target image lacks SBOM attestation; attempting to generate with Trivy..." target_files=(sboms/target/sbom-*.spdx.json)
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 [ ${#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 fi
# Expose extracted head SBOMs to workspace root for workflow artifact uploads # 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 for f in sboms/head/sbom-*.spdx.json; do
[ -f sboms/head/sbom-arm64.spdx.json ] && cp sboms/head/sbom-arm64.spdx.json sbom-arm64.spdx.json || true [ -f "$f" ] || continue
cp "$f" "$(basename "$f")" || true
done
- name: Scan Multi-Platform SBOMs and Generate Delta - name: Scan Multi-Platform SBOMs and Generate Delta
shell: bash shell: bash
run: | run: |
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
export TRIVY_SKIP_VERSION_CHECK="true"
mkdir -p cve-reports/head cve-reports/target mkdir -p cve-reports/head cve-reports/target
# Scan each available head SBOM shopt -s nullglob
for f in sboms/head/sbom-*.spdx.json; do head_sboms=(sboms/head/sbom-*.spdx.json)
[ -f "$f" ] || continue target_sboms=(sboms/target/sbom-*.spdx.json)
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
# Scan each available target SBOM head_ok=true
for f in sboms/target/sbom-*.spdx.json; do target_ok=true
[ -f "$f" ] || continue head_report_count=0
arch=$(basename "$f" .spdx.json | sed 's/sbom-//') target_report_count=0
echo "Scanning target SBOM for architecture: $arch"
trivy sbom --format json --output "cve-reports/target/cve-${arch}.json" "$f" || true
done
# Run multi-arch set-based diff head_summary=()
node "${{ github.action_path }}/cve-diff.js" \ target_summary=()
"cve-reports/target" \
"cve-reports/head" \
"${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true
# Generate full markdown summary report for primary platform # 1. Scan Head SBOMs
primary_sbom="" if [ ${#head_sboms[@]} -eq 0 ]; then
if [ -f sboms/head/sbom-amd64.spdx.json ]; then head_ok=false
primary_sbom="sboms/head/sbom-amd64.spdx.json" head_summary+=(" - ❌ No SBOM files found in sboms/head/")
elif [ -f sboms/head/sbom-arm64.spdx.json ]; then else
primary_sbom="sboms/head/sbom-arm64.spdx.json" 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"
if trivy sbom --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; 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)")
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 fi
if [ -n "$primary_sbom" ]; then # 2. Scan Target SBOMs
trivy sbom \ if [ ${#target_sboms[@]} -eq 0 ]; then
--format template \ target_ok=false
--template "@${{ github.action_path }}/markdown.tpl" \ target_summary+=(" - ❌ No SBOM files found in sboms/target/")
--output "${{ inputs.output-summary }}" \ else
"$primary_sbom" || true 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"
if trivy sbom --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; 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)")
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 "================================================================================"
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 fi
- name: Publish Reports to Step Summary - name: Publish Reports to Step Summary
+168 -42
View File
@@ -24,6 +24,7 @@ const SEVERITY_BADGES = {
*/ */
function loadReportsForArg(arg) { function loadReportsForArg(arg) {
const cves = new Map(); const cves = new Map();
const scannedArchs = new Set();
const filePaths = []; const filePaths = [];
for (const part of arg.split(',').map((s) => s.trim())) { for (const part of arg.split(',').map((s) => s.trim())) {
@@ -41,35 +42,55 @@ 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) { for (const filePath of filePaths) {
let stat; let stat;
try { try {
stat = fs.statSync(filePath); stat = fs.statSync(filePath);
} catch { } catch (e) {
continue; 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'; 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'; arch = 'amd64';
} else if (baseName.includes('arm64') || baseName.includes('aarch64')) { } else if (baseName.includes('arm64') || baseName.includes('aarch64')) {
arch = 'arm64'; arch = 'arm64';
} }
if (arch !== 'all') {
scannedArchs.add(arch);
}
let data; let data;
try { try {
const content = fs.readFileSync(filePath, 'utf8'); const content = fs.readFileSync(filePath, 'utf8');
data = JSON.parse(content); data = JSON.parse(content);
} catch { } catch (e) {
continue; console.error(`::error::Invalid JSON in CVE report file ${filePath}: ${e.message}`);
process.exit(1);
} }
if (!data || typeof data !== 'object' || !Array.isArray(data.Results)) { if (!data || typeof data !== 'object' || !Array.isArray(data.Results)) {
continue; console.error(`::error::Malformed Trivy report structure (missing Results array) in: ${filePath}`);
process.exit(1);
} }
loadedFiles++;
for (const result of data.Results) { for (const result of data.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 : [];
@@ -88,15 +109,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; return cves;
} }
/** /**
* Formats a vulnerability record into a Markdown table row. * Formats a vulnerability record into a Markdown table row.
* @param {object} v * @param {object} v
* @param {Set<string>} [allScannedArchs]
* @param {boolean} [showArch]
* @returns {string} * @returns {string}
*/ */
function formatVulnRow(v) { function formatVulnRow(v, allScannedArchs = new Set(), showArch = true) {
const sev = v.Severity || 'UNKNOWN'; const sev = v.Severity || 'UNKNOWN';
const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`; const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`;
const vid = v.VulnerabilityID || ''; const vid = v.VulnerabilityID || '';
@@ -104,14 +133,6 @@ function formatVulnRow(v) {
const link = url ? `[${vid}](${url})` : vid; const link = url ? `[${vid}](${url})` : vid;
const pkg = v.PkgName || ''; 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 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, ' ').trim();
@@ -119,7 +140,22 @@ function formatVulnRow(v) {
title = title.slice(0, 77) + '...'; 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 +166,17 @@ function formatVulnRow(v) {
* @returns {string} * @returns {string}
*/ */
function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { 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 baseKeys = new Set(baseCves.keys());
const branchKeys = new Set(branchCves.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 = []; const newKeys = [];
let unchangedCount = 0; let unchangedCount = 0;
for (const key of branchKeys) { for (const key of branchKeys) {
@@ -171,26 +205,52 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
return aPkg.localeCompare(bPkg); 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 newVulns = newKeys.map((k) => branchCves.get(k)).sort(sortVulns);
const resolvedVulns = resolvedKeys.map((k) => baseCves.get(k)).sort(sortVulns); const resolvedVulns = resolvedKeys.map((k) => baseCves.get(k)).sort(sortVulns);
const lines = [ const lines = [
`### 🔄 Vulnerability Delta (vs \`${baseName}\`)`, `### 🔄 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) { if (newVulns.length > 0) {
lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |'); if (showArch) {
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); 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) { for (const v of newVulns) {
lines.push(formatVulnRow(v)); lines.push(formatVulnRow(v, allScannedArchs, showArch));
} }
} else { } else {
lines.push('✅ No new vulnerabilities introduced in this build.'); lines.push('✅ No new vulnerabilities introduced in this build.');
@@ -199,10 +259,15 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
lines.push(`#### ✅ Resolved / Removed Vulnerabilities (${resolvedVulns.length})`); lines.push(`#### ✅ Resolved / Removed Vulnerabilities (${resolvedVulns.length})`);
if (resolvedVulns.length > 0) { if (resolvedVulns.length > 0) {
lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |'); if (showArch) {
lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); 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) { for (const v of resolvedVulns) {
lines.push(formatVulnRow(v)); lines.push(formatVulnRow(v, allScannedArchs, showArch));
} }
} else { } else {
lines.push('⚪ No existing vulnerabilities were resolved.'); lines.push('⚪ No existing vulnerabilities were resolved.');
@@ -212,22 +277,82 @@ function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') {
return lines.join('\n'); 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() { function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length < 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); process.exit(1);
} }
const baseArg = args[0]; const baseArg = args[0];
const branchArg = args[1]; const branchArg = args[1];
const baseName = args[2] || 'main'; const baseName = args[2] || 'main';
const summaryOutputPath = args[3];
const baseCves = loadReportsForArg(baseArg); const baseCves = loadReportsForArg(baseArg);
const branchCves = loadReportsForArg(branchArg); const branchCves = loadReportsForArg(branchArg);
const output = renderMarkdownDiff(baseCves, branchCves, baseName); const output = renderMarkdownDiff(baseCves, branchCves, baseName);
console.log(output); console.log(output);
if (summaryOutputPath) {
const summaryOutput = renderFullSummary(branchCves);
fs.writeFileSync(summaryOutputPath, summaryOutput, 'utf8');
}
} }
if (require.main === module) { if (require.main === module) {
@@ -238,4 +363,5 @@ module.exports = {
loadReportsForArg, loadReportsForArg,
formatVulnRow, formatVulnRow,
renderMarkdownDiff, renderMarkdownDiff,
renderFullSummary,
}; };
+1 -1
View File
@@ -10,7 +10,7 @@
"^action\\.ya?ml$" "^action\\.ya?ml$"
], ],
"matchStrings": [ "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", "depNameTemplate": "aquasecurity/trivy",
"datasourceTemplate": "github-releases", "datasourceTemplate": "github-releases",