Public Access
generated from Philipp/template-git
feat: add dual-side diagnostic overview and fail-safe gating #10
No files matched your search
+179
-26
@@ -86,16 +86,22 @@ 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=""
|
||||||
|
target_sbom_err=""
|
||||||
|
|
||||||
|
# --- Head Image SBOM Extraction ---
|
||||||
echo "Extracting platform SBOMs for head image: ${{ inputs.head-image }}..."
|
echo "Extracting platform SBOMs for head image: ${{ inputs.head-image }}..."
|
||||||
docker buildx imagetools inspect "${{ inputs.head-image }}" \
|
docker buildx imagetools inspect "${{ inputs.head-image }}" \
|
||||||
--format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true
|
--format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true
|
||||||
@@ -115,11 +121,17 @@ runs:
|
|||||||
# Fallback to generating head SBOM with Trivy if attestation missing
|
# Fallback to generating head SBOM with Trivy if attestation missing
|
||||||
if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ]; then
|
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..."
|
echo "Head image lacks SBOM attestation; generating with Trivy..."
|
||||||
trivy image --format spdx-json --output sboms/head/sbom-amd64.spdx.json "${{ inputs.head-image }}" || true
|
if ! trivy image --skip-version-check --format spdx-json --output sboms/head/sbom-amd64.spdx.json "${{ inputs.head-image }}"; then
|
||||||
|
head_sbom_err="Failed to generate fallback SBOM with Trivy for head image"
|
||||||
|
fi
|
||||||
cleanup_file sboms/head/sbom-amd64.spdx.json
|
cleanup_file sboms/head/sbom-amd64.spdx.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract platform SBOMs for target image
|
if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ] && [ -z "$head_sbom_err" ]; then
|
||||||
|
head_sbom_err="No SBOM attestation found and head image could not be inspected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Target Image SBOM Extraction ---
|
||||||
echo "Extracting platform SBOMs for target image: ${{ inputs.target-image }}..."
|
echo "Extracting platform SBOMs for target image: ${{ inputs.target-image }}..."
|
||||||
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 (index .SBOM "linux/amd64").SPDX }}' > sboms/target/sbom-amd64.spdx.json 2>/dev/null || true
|
||||||
@@ -138,10 +150,54 @@ runs:
|
|||||||
# Fallback to generating target SBOM with Trivy if attestation missing
|
# Fallback to generating target SBOM with Trivy if attestation missing
|
||||||
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then
|
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then
|
||||||
echo "Target image lacks SBOM attestation; attempting to generate with Trivy..."
|
echo "Target image lacks SBOM attestation; attempting to generate with Trivy..."
|
||||||
trivy image --format spdx-json --output sboms/target/sbom-amd64.spdx.json "${{ inputs.target-image }}" || true
|
if ! trivy image --skip-version-check --format spdx-json --output sboms/target/sbom-amd64.spdx.json "${{ inputs.target-image }}"; then
|
||||||
|
target_sbom_err="Failed to generate fallback SBOM with Trivy for target image"
|
||||||
|
fi
|
||||||
cleanup_file sboms/target/sbom-amd64.spdx.json
|
cleanup_file sboms/target/sbom-amd64.spdx.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ] && [ -z "$target_sbom_err" ]; then
|
||||||
|
target_sbom_err="No SBOM attestation found and target baseline image could not be inspected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report on both sides
|
||||||
|
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"
|
||||||
|
[ -f sboms/head/sbom-amd64.spdx.json ] && echo " - sbom-amd64.spdx.json ($(wc -c < sboms/head/sbom-amd64.spdx.json) bytes)"
|
||||||
|
[ -f sboms/head/sbom-arm64.spdx.json ] && echo " - sbom-arm64.spdx.json ($(wc -c < sboms/head/sbom-arm64.spdx.json) bytes)"
|
||||||
|
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"
|
||||||
|
[ -f sboms/target/sbom-amd64.spdx.json ] && echo " - sbom-amd64.spdx.json ($(wc -c < sboms/target/sbom-amd64.spdx.json) bytes)"
|
||||||
|
[ -f sboms/target/sbom-arm64.spdx.json ] && echo " - sbom-arm64.spdx.json ($(wc -c < sboms/target/sbom-arm64.spdx.json) bytes)"
|
||||||
|
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
|
# 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
|
[ -f sboms/head/sbom-amd64.spdx.json ] && cp sboms/head/sbom-amd64.spdx.json sbom-amd64.spdx.json || true
|
||||||
[ -f sboms/head/sbom-arm64.spdx.json ] && cp sboms/head/sbom-arm64.spdx.json sbom-arm64.spdx.json || true
|
[ -f sboms/head/sbom-arm64.spdx.json ] && cp sboms/head/sbom-arm64.spdx.json sbom-arm64.spdx.json || true
|
||||||
@@ -150,31 +206,124 @@ 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 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=()
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
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)
|
||||||
|
head_summary+=(" - ${arch}: ✅ OK (${vuln_cnt} vulnerability findings)")
|
||||||
|
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"
|
||||||
|
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)
|
||||||
|
target_summary+=(" - ${arch}: ✅ OK (${vuln_cnt} vulnerability findings)")
|
||||||
|
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 (both are guaranteed present and valid)
|
||||||
|
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/target" \
|
||||||
"cve-reports/head" \
|
"cve-reports/head" \
|
||||||
"${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true
|
"${{ inputs.target-branch }}" > "${{ inputs.output-delta }}"; then
|
||||||
|
echo "::error::Failed to generate CVE differential report"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Generate full markdown summary report for primary platform
|
# 6. Generate full markdown summary report for primary platform
|
||||||
primary_sbom=""
|
primary_sbom=""
|
||||||
if [ -f sboms/head/sbom-amd64.spdx.json ]; then
|
if [ -f sboms/head/sbom-amd64.spdx.json ]; then
|
||||||
primary_sbom="sboms/head/sbom-amd64.spdx.json"
|
primary_sbom="sboms/head/sbom-amd64.spdx.json"
|
||||||
@@ -183,11 +332,15 @@ runs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$primary_sbom" ]; then
|
if [ -n "$primary_sbom" ]; then
|
||||||
trivy sbom \
|
if ! trivy sbom \
|
||||||
|
--skip-version-check \
|
||||||
--format template \
|
--format template \
|
||||||
--template "@${{ github.action_path }}/markdown.tpl" \
|
--template "@${{ github.action_path }}/markdown.tpl" \
|
||||||
--output "${{ inputs.output-summary }}" \
|
--output "${{ inputs.output-summary }}" \
|
||||||
"$primary_sbom" || true
|
"$primary_sbom"; then
|
||||||
|
echo "::error::Trivy summary generation failed for primary SBOM: $primary_sbom"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish Reports to Step Summary
|
- name: Publish Reports to Step Summary
|
||||||
|
|||||||
+25
-6
@@ -41,14 +41,24 @@ 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).toLowerCase();
|
||||||
let arch = 'all';
|
let arch = 'all';
|
||||||
@@ -62,14 +72,18 @@ function loadReportsForArg(arg) {
|
|||||||
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,6 +102,11 @@ function loadReportsForArg(arg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loadedFiles === 0) {
|
||||||
|
console.error(`::error::No valid CVE reports could be loaded for path: ${arg}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
return cves;
|
return cves;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user