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' 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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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: | mkdir -p "$HOME/.cache/trivy-bin" "$HOME/.local/bin" echo "$HOME/.local/bin" >> "$GITHUB_PATH" ARCH=$(uname -m) case "$ARCH" in x86_64) T_ARCH="64bit" ;; aarch64|arm64) T_ARCH="ARM64" ;; *) echo "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" mkdir -p sboms/head sboms/target cleanup_file() { local f="$1" if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then rm -f "$f" fi } # 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 echo "Extracting 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" if [ ! -f "sboms/head/sbom-${arch_tag}.spdx.json" ]; then echo "Head image lacks SBOM attestation for $platform; generating with Trivy..." trivy image --platform "$platform" --format spdx-json --output "sboms/head/sbom-${arch_tag}.spdx.json" "${{ inputs.head-image }}" || true cleanup_file "sboms/head/sbom-${arch_tag}.spdx.json" fi # Target image extraction & fallback echo "Extracting 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" if [ ! -f "sboms/target/sbom-${arch_tag}.spdx.json" ]; then echo "Target image lacks SBOM attestation for $platform; generating with Trivy..." trivy image --platform "$platform" --format spdx-json --output "sboms/target/sbom-${arch_tag}.spdx.json" "${{ inputs.target-image }}" || true 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 echo "Extracting root SBOM or scanning head image directly..." 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 if [ ! -f sboms/head/sbom-default.spdx.json ]; then trivy image --format spdx-json --output sboms/head/sbom-default.spdx.json "${{ inputs.head-image }}" || true 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 echo "Extracting root SBOM or scanning target image directly..." 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 if [ ! -f sboms/target/sbom-default.spdx.json ]; then trivy image --format spdx-json --output sboms/target/sbom-default.spdx.json "${{ inputs.target-image }}" || true cleanup_file sboms/target/sbom-default.spdx.json fi 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" mkdir -p cve-reports/head cve-reports/target # Scan each available head SBOM for f in sboms/head/sbom-*.spdx.json; do [ -f "$f" ] || continue 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 for f in sboms/target/sbom-*.spdx.json; do [ -f "$f" ] || continue arch=$(basename "$f" .spdx.json | sed 's/sbom-//') 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 node "${{ github.action_path }}/cve-diff.js" \ "cve-reports/target" \ "cve-reports/head" \ "${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true # Generate full markdown summary report for primary platform primary_sbom="" if [ -f sboms/head/sbom-amd64.spdx.json ]; then primary_sbom="sboms/head/sbom-amd64.spdx.json" elif [ -f sboms/head/sbom-arm64.spdx.json ]; then primary_sbom="sboms/head/sbom-arm64.spdx.json" else for f in sboms/head/sbom-*.spdx.json; do if [ -f "$f" ]; then primary_sbom="$f" break fi done fi if [ -n "$primary_sbom" ]; then trivy sbom \ --format template \ --template "@${{ github.action_path }}/markdown.tpl" \ --output "${{ inputs.output-summary }}" \ "$primary_sbom" || true 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