Skip to content

Scheduled CVE rescan #7

Scheduled CVE rescan

Scheduled CVE rescan #7

name: Scheduled CVE rescan
# Re-scan the latest successfully-built `main` image for newly-disclosed CVEs.
# Phase 2.6 FR-004~FR-007.
on:
schedule:
- cron: "0 6 * * MON"
workflow_dispatch:
permissions:
contents: read
packages: read
issues: write # to auto-open an issue when a new CVE shows up
actions: read # required for `gh run list` to find the latest image build
jobs:
scan:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Resolve latest successful build SHA
id: resolve
run: |
# FR-005: pick the latest *successful* image build run on main.
SHA=$(gh run list --workflow=image.yml --branch=main --status=success \
--limit=1 --json headSha --jq '.[0].headSha[0:7]')
if [ -z "$SHA" ]; then
echo "::error::no successful image build found on main"; exit 1
fi
IMAGE="ghcr.io/${REPO}:sha-${SHA}"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "Will scan: $IMAGE"
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Always emit a JSON report (regardless of severity) so we can introspect.
# We DO NOT fail the job on CVEs — the gate is "open an issue per new CVE".
- name: Trivy scan (json)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
version: v0.70.0 # FR-002
image-ref: ${{ steps.resolve.outputs.image }}
severity: HIGH,CRITICAL
exit-code: '0' # do not fail the job
ignore-unfixed: true
trivyignores: .trivyignore
format: json
output: trivy-report.json
- name: Open issues for new CVEs (deduped by CVE id)
run: |
set -euo pipefail
IMAGE="${{ steps.resolve.outputs.image }}"
# Extract all unique CVE ids found (HIGH/CRITICAL only).
CVES=$(jq -r '
[.Results[]?.Vulnerabilities[]?
| select(.Severity == "HIGH" or .Severity == "CRITICAL")
| .VulnerabilityID
] | unique | .[]
' trivy-report.json 2>/dev/null || true)
if [ -z "$CVES" ]; then
echo "::notice::clean — no HIGH/CRITICAL CVEs found in $IMAGE"
exit 0
fi
echo "Found CVEs:"
echo "$CVES"
for CVE in $CVES; do
# FR-007: dedup by CVE id in title
EXISTING=$(gh issue list --state open --search "[$CVE] in:title" \
--json number --jq '.[0].number // empty' || true)
if [ -n "$EXISTING" ]; then
echo "skip: issue #$EXISTING already tracks $CVE"
continue
fi
DATE=$(date -u +%Y-%m-%d)
SUMMARY=$(jq -r --arg cve "$CVE" '
[.Results[]?.Vulnerabilities[]?
| select(.VulnerabilityID == $cve)][0]
| "**Package**: \(.PkgName) \(.InstalledVersion)\n" +
"**Fixed in**: \(.FixedVersion // "—")\n" +
"**Severity**: \(.Severity)\n" +
"**Title**: \(.Title // .Description // "")"
' trivy-report.json)
BODY=$(printf '%s\n' \
"Scheduled rescan of \`$IMAGE\` on $DATE detected:" \
"" \
"$SUMMARY" \
"" \
"Detected by: \`.github/workflows/scheduled-scan.yml\`" \
"Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
"" \
"To suppress (if not exploitable), add to \`.trivyignore\` with a reason and link this issue.")
gh issue create \
--title "[$CVE] new HIGH/CRITICAL in image ($DATE)" \
--label cve,security \
--body "$BODY"
done
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: trivy-report-${{ steps.resolve.outputs.sha }}
path: trivy-report.json
retention-days: 30