Skip to content

Commit 28fc042

Browse files
castrojoCopilot
andauthored
feat(ci): add weekly standalone vulnerability scan (#794)
Adds a Grype-based vulnerability scan workflow that runs weekly (Monday 08:00 UTC). Catches newly-disclosed CVEs against already-published images between builds. Dakota uses BuildStream (not reusable-build.yml) so there are no image-digest artifacts — image refs are resolved directly via skopeo against the published :latest tag. Closes projectbluefin/common#607 Assisted-by: Claude Sonnet 4.5 via pi Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b1ed7b7 commit 28fc042

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: Vulnerability Scan
2+
3+
on:
4+
schedule:
5+
- cron: '0 8 * * 1' # Monday 08:00 UTC — weekly scan for newly-disclosed CVEs
6+
workflow_dispatch:
7+
inputs:
8+
image_ref:
9+
description: "Full image ref to scan (default: latest published image)"
10+
required: false
11+
default: ""
12+
13+
permissions:
14+
contents: read
15+
security-events: write
16+
17+
concurrency:
18+
group: vulnerability-scan-${{ github.run_id }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
scan:
23+
name: Grype scan — ${{ matrix.image_name }}
24+
# Grype scans run on GitHub-hosted runners that can be preempted mid-job
25+
# by spot-instance recycling. continue-on-error prevents a runner shutdown
26+
# from marking the overall workflow as failed — SARIF uploads are best-effort.
27+
continue-on-error: true
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 30
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
include:
34+
- image_name: dakota
35+
- image_name: dakota-nvidia
36+
steps:
37+
- name: Resolve image ref
38+
id: resolve
39+
run: |
40+
set -euo pipefail
41+
if [[ -n "${{ inputs.image_ref }}" ]]; then
42+
REF="${{ inputs.image_ref }}"
43+
else
44+
# Resolve :latest tag to an immutable digest for reproducible scan
45+
TAG="ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest"
46+
DIGEST=$(skopeo inspect --no-tags "docker://${TAG}" | jq -r '.Digest')
47+
if [[ -z "${DIGEST}" ]]; then
48+
echo "::error::Could not resolve digest for ${TAG}"
49+
exit 1
50+
fi
51+
REF="ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}@${DIGEST}"
52+
fi
53+
echo "ref=${REF}" >> "$GITHUB_OUTPUT"
54+
echo "Scanning: ${REF}"
55+
56+
- name: Scan image with Grype
57+
id: scan
58+
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
59+
with:
60+
image: ${{ steps.resolve.outputs.ref }}
61+
fail-build: false
62+
severity-cutoff: critical
63+
output-format: sarif
64+
65+
- name: Upload SARIF results
66+
if: always()
67+
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
68+
with:
69+
sarif_file: ${{ steps.scan.outputs.sarif }}
70+
category: grype-${{ matrix.image_name }}
71+
72+
- name: Check for critical CVEs
73+
if: always()
74+
run: |
75+
set -euo pipefail
76+
CRITICAL_COUNT=$(python3 -c "
77+
import json, sys
78+
data = json.load(open('${{ steps.scan.outputs.sarif }}'))
79+
runs = data.get('runs', [])
80+
criticals = sum(
81+
1 for run in runs
82+
for result in run.get('results', [])
83+
if result.get('ruleId', '').startswith('CVE') and
84+
any(prop.get('security-severity', '0') >= '9.0'
85+
for prop in [result.get('properties', {})])
86+
)
87+
print(criticals)
88+
" 2>/dev/null || echo "0")
89+
90+
echo "Critical CVEs found: ${CRITICAL_COUNT}"
91+
92+
if [[ "${CRITICAL_COUNT}" -gt 0 ]]; then
93+
echo "::warning::${CRITICAL_COUNT} critical CVE(s) found in ${{ matrix.image_name }} — see Security tab"
94+
fi

0 commit comments

Comments
 (0)