Public Access
generated from Philipp/template-git
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
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.
This commit is contained in:
13 files changed
+1144
-468
No files matched your search
+21
-468
@@ -65,480 +65,33 @@ runs:
|
||||
|
||||
- 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"
|
||||
env:
|
||||
TRIVY_VERSION: ${{ steps.vars.outputs.version }}
|
||||
run: node "${{ github.action_path }}/lib/install-trivy.js"
|
||||
|
||||
- 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
|
||||
env:
|
||||
PATH: ${{ env.HOME }}/.local/bin:${{ env.PATH }}
|
||||
TRIVY_SKIP_VERSION_CHECK: 'true'
|
||||
INPUT_HEAD_IMAGE: ${{ inputs.head-image }}
|
||||
INPUT_TARGET_IMAGE: ${{ inputs.target-image }}
|
||||
INPUT_ARCHITECTURES: ${{ inputs.architectures }}
|
||||
INPUT_USE_INBUILD_SBOM: ${{ inputs.use-inbuild-sbom }}
|
||||
run: node "${{ github.action_path }}/lib/extract-sboms.js"
|
||||
|
||||
- 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
|
||||
env:
|
||||
PATH: ${{ env.HOME }}/.local/bin:${{ env.PATH }}
|
||||
TRIVY_SKIP_VERSION_CHECK: 'true'
|
||||
INPUT_HEAD_IMAGE: ${{ inputs.head-image }}
|
||||
INPUT_TARGET_IMAGE: ${{ inputs.target-image }}
|
||||
INPUT_TARGET_BRANCH: ${{ inputs.target-branch }}
|
||||
INPUT_USE_INBUILD_SBOM: ${{ inputs.use-inbuild-sbom }}
|
||||
INPUT_OUTPUT_DELTA: ${{ inputs.output-delta }}
|
||||
INPUT_OUTPUT_SUMMARY: ${{ inputs.output-summary }}
|
||||
run: node "${{ github.action_path }}/lib/scan-sboms.js"
|
||||
|
||||
- name: Publish Reports to Step Summary
|
||||
shell: bash
|
||||
|
||||
Reference in new issue
Block a user