Public Access
generated from Philipp/template-git
CI Tests / JS Unit Tests (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=false) (push) Skipped
CI Tests / cross-check-mixed-sbom (sbom=true) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / without-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=false) (push) Skipped
CI Tests / with-inbuild-sbom (sbom=true) (push) Skipped
CI Tests / JS Unit Tests (pull_request) Successful in 9s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 2m34s
CI Tests / without-inbuild-sbom (sbom=false) (pull_request) Successful in 4m20s
CI Tests / cross-check-mixed-sbom (sbom=false) (pull_request) Successful in 6m15s
CI Tests / cross-check-mixed-sbom (sbom=true) (pull_request) Successful in 4m45s
CI Tests / without-inbuild-sbom (sbom=true) (pull_request) Successful in 3m50s
CI Tests / with-inbuild-sbom (sbom=false) (pull_request) Successful in 3m54s
CI Tests / with-inbuild-sbom (sbom=true) (pull_request) Successful in 3m35s
This reverts commit17e5dab1b2, reversing changes made to46c4fedc47.
554 lines
24 KiB
YAML
554 lines
24 KiB
YAML
name: Trivy Multi-Arch CVE Delta Comparison
|
||
description: Downloads and verifies Trivy, extracts multi-platform SBOMs, and computes a consolidated set-based CVE delta report
|
||
|
||
inputs:
|
||
head-image:
|
||
description: Head container image reference (e.g. host/server/gitea:git-12345678)
|
||
required: true
|
||
target-image:
|
||
description: Target baseline container image reference (e.g. host/server/gitea:latest)
|
||
required: true
|
||
target-branch:
|
||
description: Target branch name for delta heading (e.g. main)
|
||
required: false
|
||
default: main
|
||
architectures:
|
||
description: Comma-separated list of architectures or platforms to scan (e.g. 'amd64,arm64' or 'linux/amd64,linux/arm64'). If empty, scans all architectures found in the container image.
|
||
required: false
|
||
default: ''
|
||
output-delta:
|
||
description: Output markdown file for CVE delta report
|
||
required: false
|
||
default: cve-delta.md
|
||
output-summary:
|
||
description: Output markdown file for full vulnerability summary
|
||
required: false
|
||
default: cve-summary.md
|
||
write-summary:
|
||
description: Whether to append the generated reports to the job summary ($GITHUB_STEP_SUMMARY)
|
||
required: false
|
||
default: 'true'
|
||
use-inbuild-sbom:
|
||
description: Whether to inspect and use built-in image SBOM attestations. If false (recommended default), generates SBOMs directly with Trivy for maximum vulnerability detection fidelity. If true, extracts attestations without pulling layers (faster, but findings may differ from direct scans due to binary vs source package tracking; falls back to Trivy image scan if 0 CVEs detected).
|
||
required: false
|
||
default: 'false'
|
||
|
||
outputs:
|
||
delta-report:
|
||
description: Path to CVE delta markdown report
|
||
value: ${{ inputs.output-delta }}
|
||
summary-report:
|
||
description: Path to full vulnerability markdown report
|
||
value: ${{ inputs.output-summary }}
|
||
|
||
runs:
|
||
using: composite
|
||
steps:
|
||
- name: Prepare Cache Keys and Environment
|
||
id: vars
|
||
shell: bash
|
||
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)
|
||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||
with:
|
||
path: |
|
||
~/.cache/trivy-bin
|
||
~/.cache/trivy
|
||
key: trivy-${{ steps.vars.outputs.version }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.vars.outputs.date }}
|
||
restore-keys: |
|
||
trivy-${{ steps.vars.outputs.version }}-${{ runner.os }}-${{ runner.arch }}-
|
||
|
||
- name: Install and Verify Trivy
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
mkdir -p "$HOME/.local/bin" "$HOME/.cache/trivy-bin"
|
||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||
ARCH="$(uname -m)"
|
||
case "${ARCH}" in
|
||
x86_64) T_ARCH="64bit" ;;
|
||
aarch64|arm64) T_ARCH="ARM64" ;;
|
||
*) echo "::error::Unsupported architecture: ${ARCH}"; exit 1 ;;
|
||
esac
|
||
|
||
checksums_file="${{ github.action_path }}/checksums.txt"
|
||
tarball="trivy_${{ steps.vars.outputs.version }}_Linux-${T_ARCH}.tar.gz"
|
||
|
||
if [ -f "$HOME/.cache/trivy-bin/${tarball}" ] && (cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -); then
|
||
echo "Cached Trivy archive verified against lockfile."
|
||
else
|
||
echo "Archive missing or checksum mismatch; downloading..."
|
||
curl -sfLO "https://github.com/aquasecurity/trivy/releases/download/v${{ steps.vars.outputs.version }}/${tarball}" --output-dir "$HOME/.cache/trivy-bin"
|
||
(cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -)
|
||
fi
|
||
|
||
tar -xzf "$HOME/.cache/trivy-bin/${tarball}" -C "$HOME/.local/bin" trivy
|
||
chmod +x "$HOME/.local/bin/trivy"
|
||
|
||
- name: Extract Multi-Platform SBOMs
|
||
shell: bash
|
||
run: |
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
export TRIVY_SKIP_VERSION_CHECK="true"
|
||
mkdir -p sboms/head sboms/target
|
||
|
||
cleanup_file() {
|
||
local f="$1"
|
||
if [ -f "$f" ]; then
|
||
if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then
|
||
rm -f "$f"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
head_sbom_err=""
|
||
target_sbom_err=""
|
||
use_inbuild="${{ inputs.use-inbuild-sbom }}"
|
||
|
||
# Determine target platforms / architectures
|
||
PLATFORMS=()
|
||
if [ -n "${{ inputs.architectures }}" ]; then
|
||
IFS=',' read -ra RAW_ARCHS <<< "${{ inputs.architectures }}"
|
||
for a in "${RAW_ARCHS[@]}"; do
|
||
clean_a=$(echo "$a" | xargs)
|
||
[ -z "$clean_a" ] && continue
|
||
if [[ "$clean_a" != *"/"* ]]; then
|
||
PLATFORMS+=("linux/${clean_a}")
|
||
else
|
||
PLATFORMS+=("$clean_a")
|
||
fi
|
||
done
|
||
else
|
||
echo "Auto-discovering architectures from images..."
|
||
raw_head=$(docker buildx imagetools inspect --raw "${{ inputs.head-image }}" 2>/dev/null || true)
|
||
raw_target=$(docker buildx imagetools inspect --raw "${{ inputs.target-image }}" 2>/dev/null || true)
|
||
|
||
discovered=$(node -e '
|
||
const raw1 = process.argv[1];
|
||
const raw2 = process.argv[2];
|
||
const set = new Set();
|
||
for (const raw of [raw1, raw2]) {
|
||
if (!raw) continue;
|
||
try {
|
||
const data = JSON.parse(raw);
|
||
const manifests = data.manifests || [];
|
||
for (const m of manifests) {
|
||
if (m.platform && m.platform.architecture && m.platform.architecture !== "unknown") {
|
||
const os = m.platform.os || "linux";
|
||
const variant = m.platform.variant ? "/" + m.platform.variant : "";
|
||
set.add(`${os}/${m.platform.architecture}${variant}`);
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
console.log(Array.from(set).sort().join("\n"));
|
||
' "$raw_head" "$raw_target")
|
||
|
||
if [ -n "$discovered" ]; then
|
||
while IFS= read -r line; do
|
||
[ -n "$line" ] && PLATFORMS+=("$line")
|
||
done <<< "$discovered"
|
||
fi
|
||
fi
|
||
|
||
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')
|
||
|
||
# Head image extraction & fallback
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
echo "Attempting extraction of built-in SBOM for head image ($platform)..."
|
||
docker buildx imagetools inspect "${{ inputs.head-image }}" \
|
||
--format "{{ json (index .SBOM \"${platform}\").SPDX }}" > "sboms/head/sbom-${arch_tag}.spdx.json" 2>/dev/null || true
|
||
cleanup_file "sboms/head/sbom-${arch_tag}.spdx.json"
|
||
[ -f "sboms/head/sbom-${arch_tag}.spdx.json" ] && cp "sboms/head/sbom-${arch_tag}.spdx.json" "sboms/head/inbuild-sbom-${arch_tag}.spdx.json" 2>/dev/null || true
|
||
fi
|
||
|
||
if [ ! -f "sboms/head/sbom-${arch_tag}.spdx.json" ]; then
|
||
echo "Generating SBOM for head image ($platform) with Trivy..."
|
||
if ! trivy image --skip-version-check --platform "$platform" --format spdx-json --output "sboms/head/sbom-${arch_tag}.spdx.json" "${{ inputs.head-image }}"; then
|
||
echo "::warning::Trivy image scan failed for head image ($platform)"
|
||
fi
|
||
cleanup_file "sboms/head/sbom-${arch_tag}.spdx.json"
|
||
fi
|
||
|
||
# Target image extraction & fallback
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
echo "Attempting extraction of built-in SBOM for target image ($platform)..."
|
||
docker buildx imagetools inspect "${{ inputs.target-image }}" \
|
||
--format "{{ json (index .SBOM \"${platform}\").SPDX }}" > "sboms/target/sbom-${arch_tag}.spdx.json" 2>/dev/null || true
|
||
cleanup_file "sboms/target/sbom-${arch_tag}.spdx.json"
|
||
[ -f "sboms/target/sbom-${arch_tag}.spdx.json" ] && cp "sboms/target/sbom-${arch_tag}.spdx.json" "sboms/target/inbuild-sbom-${arch_tag}.spdx.json" 2>/dev/null || true
|
||
fi
|
||
|
||
if [ ! -f "sboms/target/sbom-${arch_tag}.spdx.json" ]; then
|
||
echo "Generating SBOM for target image ($platform) with Trivy..."
|
||
if ! trivy image --skip-version-check --platform "$platform" --format spdx-json --output "sboms/target/sbom-${arch_tag}.spdx.json" "${{ inputs.target-image }}"; then
|
||
echo "::warning::Trivy image scan failed for target image ($platform)"
|
||
fi
|
||
cleanup_file "sboms/target/sbom-${arch_tag}.spdx.json"
|
||
fi
|
||
done
|
||
|
||
# Fallbacks if no platform-specific SBOMs were extracted (e.g. single-arch image without index)
|
||
if [ -z "$(find sboms/head -type f -name 'sbom-*.spdx.json' 2>/dev/null)" ]; then
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
docker buildx imagetools inspect "${{ inputs.head-image }}" \
|
||
--format '{{ json .SBOM.SPDX }}' > sboms/head/sbom-default.spdx.json 2>/dev/null || true
|
||
cleanup_file sboms/head/sbom-default.spdx.json
|
||
[ -f sboms/head/sbom-default.spdx.json ] && cp sboms/head/sbom-default.spdx.json sboms/head/inbuild-sbom-default.spdx.json 2>/dev/null || true
|
||
fi
|
||
|
||
if [ ! -f sboms/head/sbom-default.spdx.json ]; then
|
||
echo "Generating default head SBOM directly with Trivy..."
|
||
if ! trivy image --skip-version-check --format spdx-json --output sboms/head/sbom-default.spdx.json "${{ inputs.head-image }}"; then
|
||
head_sbom_err="Failed to generate SBOM with Trivy for head image"
|
||
fi
|
||
cleanup_file sboms/head/sbom-default.spdx.json
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$(find sboms/target -type f -name 'sbom-*.spdx.json' 2>/dev/null)" ]; then
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
docker buildx imagetools inspect "${{ inputs.target-image }}" \
|
||
--format '{{ json .SBOM.SPDX }}' > sboms/target/sbom-default.spdx.json 2>/dev/null || true
|
||
cleanup_file sboms/target/sbom-default.spdx.json
|
||
[ -f sboms/target/sbom-default.spdx.json ] && cp sboms/target/sbom-default.spdx.json sboms/target/inbuild-sbom-default.spdx.json 2>/dev/null || true
|
||
fi
|
||
|
||
if [ ! -f sboms/target/sbom-default.spdx.json ]; then
|
||
echo "Generating default target SBOM directly with Trivy..."
|
||
if ! trivy image --skip-version-check --format spdx-json --output sboms/target/sbom-default.spdx.json "${{ inputs.target-image }}"; then
|
||
target_sbom_err="Failed to generate SBOM with Trivy for target image"
|
||
fi
|
||
cleanup_file sboms/target/sbom-default.spdx.json
|
||
fi
|
||
fi
|
||
|
||
shopt -s nullglob
|
||
head_files=(sboms/head/sbom-*.spdx.json)
|
||
target_files=(sboms/target/sbom-*.spdx.json)
|
||
|
||
if [ ${#head_files[@]} -eq 0 ] && [ -z "$head_sbom_err" ]; then
|
||
head_sbom_err="No SBOM generated and head image could not be inspected"
|
||
fi
|
||
|
||
if [ ${#target_files[@]} -eq 0 ] && [ -z "$target_sbom_err" ]; then
|
||
target_sbom_err="No SBOM generated and target baseline image could not be inspected"
|
||
fi
|
||
|
||
# Diagnostic overview
|
||
echo ""
|
||
echo "================================================================================"
|
||
echo " SBOM EXTRACTION DIAGNOSTIC OVERVIEW "
|
||
echo "================================================================================"
|
||
if [ -n "$head_sbom_err" ]; then
|
||
echo "HEAD Image (${{ inputs.head-image }}): ❌ FAILED - $head_sbom_err"
|
||
else
|
||
echo "HEAD Image (${{ inputs.head-image }}): ✅ SUCCESS"
|
||
for f in "${head_files[@]}"; do
|
||
echo " - $(basename "$f") ($(wc -c < "$f") bytes)"
|
||
done
|
||
fi
|
||
|
||
if [ -n "$target_sbom_err" ]; then
|
||
echo "TARGET Baseline Image (${{ inputs.target-image }}): ❌ FAILED - $target_sbom_err"
|
||
else
|
||
echo "TARGET Baseline Image (${{ inputs.target-image }}): ✅ SUCCESS"
|
||
for f in "${target_files[@]}"; do
|
||
echo " - $(basename "$f") ($(wc -c < "$f") bytes)"
|
||
done
|
||
fi
|
||
echo "================================================================================"
|
||
echo ""
|
||
|
||
has_sbom_err=false
|
||
if [ -n "$head_sbom_err" ]; then
|
||
echo "::error::Head SBOM extraction failed: $head_sbom_err"
|
||
has_sbom_err=true
|
||
fi
|
||
if [ -n "$target_sbom_err" ]; then
|
||
echo "::error::Target baseline SBOM extraction failed: $target_sbom_err"
|
||
has_sbom_err=true
|
||
fi
|
||
|
||
if [ "$has_sbom_err" = "true" ]; then
|
||
echo "::error::Both head and target images must have valid SBOMs to proceed with differential scanning."
|
||
exit 1
|
||
fi
|
||
|
||
# Expose extracted head SBOMs to workspace root for workflow artifact uploads
|
||
for f in sboms/head/sbom-*.spdx.json; do
|
||
[ -f "$f" ] || continue
|
||
cp "$f" "$(basename "$f")" || true
|
||
done
|
||
|
||
- name: Scan Multi-Platform SBOMs and Generate Delta
|
||
shell: bash
|
||
run: |
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
export TRIVY_SKIP_VERSION_CHECK="true"
|
||
mkdir -p cve-reports/head cve-reports/target
|
||
|
||
shopt -s nullglob
|
||
head_sboms=(sboms/head/sbom-*.spdx.json)
|
||
target_sboms=(sboms/target/sbom-*.spdx.json)
|
||
|
||
head_ok=true
|
||
target_ok=true
|
||
head_report_count=0
|
||
target_report_count=0
|
||
|
||
head_summary=()
|
||
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
|
||
head_summary+=(" - ❌ No SBOM files found in sboms/head/")
|
||
else
|
||
for f in "${head_sboms[@]}"; do
|
||
arch=$(basename "$f" .spdx.json | sed 's/sbom-//')
|
||
out_json="cve-reports/head/cve-${arch}.json"
|
||
err_log="cve-reports/head/cve-${arch}.err"
|
||
|
||
echo "Scanning head SBOM for architecture: $arch"
|
||
distro_arg=""
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
detected_distro=$(detect_distro "$f")
|
||
if [ -n "$detected_distro" ]; then
|
||
echo "Detected distribution '${detected_distro}' from in-build SBOM PURLs. Scanning with --distro."
|
||
distro_arg="--distro ${detected_distro}"
|
||
fi
|
||
fi
|
||
|
||
scan_success=false
|
||
if trivy sbom $distro_arg --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
|
||
scan_success=true
|
||
fi
|
||
|
||
vuln_cnt=0
|
||
if [ -s "$out_json" ]; then
|
||
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
|
||
cnt=$(echo "$cnt" | tr -d '[:space:]')
|
||
[ -n "$cnt" ] && vuln_cnt="$cnt"
|
||
fi
|
||
|
||
# Fallback if in-build SBOM scan produced 0 CVEs (e.g. incompatible third-party format) or scan failed
|
||
if [ "$use_inbuild" = "true" ] && { [ "$scan_success" != "true" ] || [ "$vuln_cnt" -eq 0 ]; }; then
|
||
echo "::warning::In-build SBOM for head image (${arch}) yielded ${vuln_cnt} CVE findings. Falling back to direct Trivy image scan..."
|
||
t_plat="linux/${arch}"
|
||
[ "$arch" = "default" ] && t_plat=""
|
||
plat_arg=""
|
||
[ -n "$t_plat" ] && plat_arg="--platform $t_plat"
|
||
if trivy image --skip-version-check $plat_arg --format spdx-json --output "$f" "${{ inputs.head-image }}"; then
|
||
echo "Re-scanning head SBOM generated directly by Trivy..."
|
||
if trivy sbom --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
|
||
scan_success=true
|
||
if [ -s "$out_json" ]; then
|
||
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
|
||
cnt=$(echo "$cnt" | tr -d '[:space:]')
|
||
[ -n "$cnt" ] && vuln_cnt="$cnt"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ "$scan_success" = "true" ]; then
|
||
if [ -s "$out_json" ]; then
|
||
if [ "$vuln_cnt" -eq 0 ]; then
|
||
echo "::warning::Head image architecture '${arch}' has 0 CVE findings. Verify that OS/packages were recognized."
|
||
head_summary+=(" - ${arch}: ⚠️ WARNING (0 vulnerability findings detected)")
|
||
else
|
||
head_summary+=(" - ${arch}: ✅ OK (${vuln_cnt} vulnerability findings)")
|
||
fi
|
||
head_report_count=$((head_report_count + 1))
|
||
else
|
||
head_ok=false
|
||
head_summary+=(" - ${arch}: ❌ FAILED (Output report was empty / 0 bytes)")
|
||
fi
|
||
else
|
||
head_ok=false
|
||
err_msg=$(tail -n 2 "$err_log" | tr '\n' ' ' | sed 's/ */ /g')
|
||
[ -z "$err_msg" ] && err_msg="Trivy scan exited with non-zero status"
|
||
head_summary+=(" - ${arch}: ❌ FAILED (${err_msg})")
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# 2. Scan Target SBOMs
|
||
if [ ${#target_sboms[@]} -eq 0 ]; then
|
||
target_ok=false
|
||
target_summary+=(" - ❌ No SBOM files found in sboms/target/")
|
||
else
|
||
for f in "${target_sboms[@]}"; do
|
||
arch=$(basename "$f" .spdx.json | sed 's/sbom-//')
|
||
out_json="cve-reports/target/cve-${arch}.json"
|
||
err_log="cve-reports/target/cve-${arch}.err"
|
||
|
||
echo "Scanning target SBOM for architecture: $arch"
|
||
distro_arg=""
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
detected_distro=$(detect_distro "$f")
|
||
if [ -n "$detected_distro" ]; then
|
||
echo "Detected distribution '${detected_distro}' from in-build SBOM PURLs. Scanning with --distro."
|
||
distro_arg="--distro ${detected_distro}"
|
||
fi
|
||
fi
|
||
|
||
scan_success=false
|
||
if trivy sbom $distro_arg --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
|
||
scan_success=true
|
||
fi
|
||
|
||
vuln_cnt=0
|
||
if [ -s "$out_json" ]; then
|
||
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
|
||
cnt=$(echo "$cnt" | tr -d '[:space:]')
|
||
[ -n "$cnt" ] && vuln_cnt="$cnt"
|
||
fi
|
||
|
||
# Fallback if in-build SBOM scan produced 0 CVEs (e.g. incompatible third-party format) or scan failed
|
||
if [ "$use_inbuild" = "true" ] && { [ "$scan_success" != "true" ] || [ "$vuln_cnt" -eq 0 ]; }; then
|
||
echo "::warning::In-build SBOM for target image (${arch}) yielded ${vuln_cnt} CVE findings. Falling back to direct Trivy image scan..."
|
||
t_plat="linux/${arch}"
|
||
[ "$arch" = "default" ] && t_plat=""
|
||
plat_arg=""
|
||
[ -n "$t_plat" ] && plat_arg="--platform $t_plat"
|
||
if trivy image --skip-version-check $plat_arg --format spdx-json --output "$f" "${{ inputs.target-image }}"; then
|
||
echo "Re-scanning target SBOM generated directly by Trivy..."
|
||
if trivy sbom --skip-version-check --format json --output "$out_json" "$f" 2>"$err_log"; then
|
||
scan_success=true
|
||
if [ -s "$out_json" ]; then
|
||
cnt=$(grep -c '"VulnerabilityID"' "$out_json" 2>/dev/null || true)
|
||
cnt=$(echo "$cnt" | tr -d '[:space:]')
|
||
[ -n "$cnt" ] && vuln_cnt="$cnt"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ "$scan_success" = "true" ]; then
|
||
if [ -s "$out_json" ]; then
|
||
if [ "$vuln_cnt" -eq 0 ]; then
|
||
echo "::warning::Target baseline image architecture '${arch}' has 0 CVE findings. Verify that OS/packages were recognized."
|
||
target_summary+=(" - ${arch}: ⚠️ WARNING (0 vulnerability findings detected)")
|
||
else
|
||
target_summary+=(" - ${arch}: ✅ OK (${vuln_cnt} vulnerability findings)")
|
||
fi
|
||
target_report_count=$((target_report_count + 1))
|
||
else
|
||
target_ok=false
|
||
target_summary+=(" - ${arch}: ❌ FAILED (Output report was empty / 0 bytes)")
|
||
fi
|
||
else
|
||
target_ok=false
|
||
err_msg=$(tail -n 2 "$err_log" | tr '\n' ' ' | sed 's/ */ /g')
|
||
[ -z "$err_msg" ] && err_msg="Trivy scan exited with non-zero status"
|
||
target_summary+=(" - ${arch}: ❌ FAILED (${err_msg})")
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# 3. Print Comprehensive Diagnostic Overview
|
||
echo ""
|
||
echo "================================================================================"
|
||
echo " CVE VULNERABILITY SCAN DIAGNOSTIC OVERVIEW "
|
||
echo "================================================================================"
|
||
if [ "$use_inbuild" = "true" ]; then
|
||
echo "Scan Strategy: Built-in OCI Attestation (Syft/Docker Scout) with distro detection"
|
||
else
|
||
echo "Scan Strategy: Direct Trivy Image Scan (Full rootfs & source package mapping)"
|
||
fi
|
||
echo "--------------------------------------------------------------------------------"
|
||
echo "HEAD Image: ${{ inputs.head-image }}"
|
||
for line in "${head_summary[@]}"; do
|
||
echo "$line"
|
||
done
|
||
echo ""
|
||
echo "TARGET Baseline Image: ${{ inputs.target-image }} (branch: '${{ inputs.target-branch }}')"
|
||
for line in "${target_summary[@]}"; do
|
||
echo "$line"
|
||
done
|
||
echo "================================================================================"
|
||
echo ""
|
||
|
||
# 4. Enforce Gating: Both must have succeeded with valid reports
|
||
has_error=false
|
||
if [ "$head_ok" != "true" ] || [ "$head_report_count" -eq 0 ]; then
|
||
echo "::error::Head image CVE analysis failed or produced no reports."
|
||
has_error=true
|
||
fi
|
||
|
||
if [ "$target_ok" != "true" ] || [ "$target_report_count" -eq 0 ]; then
|
||
echo "::error::Target baseline CVE analysis failed or produced no reports."
|
||
has_error=true
|
||
fi
|
||
|
||
if [ "$has_error" = "true" ]; then
|
||
echo "::error::Aborting differential analysis: both head and target scans must succeed before diffing."
|
||
exit 1
|
||
fi
|
||
|
||
# 5. Run multi-arch set-based diff and generate consolidated full summary report
|
||
echo "Both head and target scans succeeded. Computing set-based vulnerability delta..."
|
||
if ! node "${{ github.action_path }}/cve-diff.js" \
|
||
"cve-reports/target" \
|
||
"cve-reports/head" \
|
||
"${{ inputs.target-branch }}" \
|
||
"${{ inputs.output-summary }}" > "${{ inputs.output-delta }}"; then
|
||
echo "::error::Failed to generate CVE differential report"
|
||
exit 1
|
||
fi
|
||
|
||
- name: Publish Reports to Step Summary
|
||
shell: bash
|
||
run: |
|
||
if [ "${{ inputs.write-summary }}" = "true" ] && [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||
if [ -f "${{ inputs.output-delta }}" ]; then
|
||
cat "${{ inputs.output-delta }}" >> "$GITHUB_STEP_SUMMARY"
|
||
fi
|
||
if [ -f "${{ inputs.output-summary }}" ]; then
|
||
cat "${{ inputs.output-summary }}" >> "$GITHUB_STEP_SUMMARY"
|
||
fi
|
||
fi
|