Files
cve-differential/action.yaml
T

271 lines
11 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'
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