Public Access
generated from Philipp/template-git
refactor(action): replace bash blocks with zero-dependency Node.js modules
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m53s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m53s
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
+1145
-347
No files matched your search
+22
-347
@@ -29,7 +29,7 @@ inputs:
|
||||
required: false
|
||||
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.
|
||||
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'
|
||||
|
||||
@@ -65,358 +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)}"
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
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
|
||||
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=()
|
||||
|
||||
# 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)
|
||||
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"
|
||||
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
|
||||
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