-
Notifications
You must be signed in to change notification settings - Fork 0
233 lines (219 loc) · 10.2 KB
/
Copy pathscheduled-image-rebuild.yml
File metadata and controls
233 lines (219 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
name: Scheduled image rebuild
# Weekly rebuild of ghcr.io/kerberosmansour/zaprun against the current state
# of main, so that Wolfi base + ZAP add-on + transitive Rust dep CVE
# disclosures get re-scanned by Trivy on a fixed cadence.
#
# What this workflow does:
# 1. Dispatch `build-zap-image.yml` against main (which rebuilds, scans,
# pushes a new SHA-tagged image, signs + attests it).
# 2. Emit a job summary listing each `.trivyignore` entry's tracking
# issue and its age, so the founder can see at a glance which
# suppressions have aged beyond the 60-day review cadence documented
# in `.trivyignore`.
#
# The "open auto-bump PR for the pin files" step in the publish runbook's
# M6 contract is deferred — that requires diffing the post-build digest
# against the canonical pin and is complex enough to warrant its own
# follow-up. The scheduled rebuild here delivers the cadence + visibility;
# the diff-and-PR-bump can follow.
on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC.
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
rebuild-and-audit:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: write # to dispatch build-zap-image.yml
issues: read # to inspect tracking issue ages for stale-warning
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
fetch-depth: 1
persist-credentials: false
- name: Dispatch build-zap-image
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
echo "Dispatching build-zap-image.yml against main..."
gh workflow run build-zap-image.yml --ref main
echo "Dispatched."
- name: Trivy auto-clear-on-resolved-CVE check
# Pulls the CANONICAL pinned image (from `references/zap-image-pin.toml`'s
# `[ours].digest`) and runs Trivy without --ignorefile, so we can see
# what's actually flagged. Cross-references against `.trivyignore`:
# a CVE no longer in Trivy's output is a candidate for removal from
# the suppression list (the upstream bump that fixes it has either
# landed in our bundled deps OR the CVE has been retracted upstream
# — either way the suppression is no longer load-bearing).
# This is detection only — manual review + PR is still required to
# remove the line, since "no longer flagged in THIS scan" can be a
# transient (Trivy DB caught up later) and we don't want the
# scheduled job to flap.
env:
IMAGE_NAME: ghcr.io/kerberosmansour/zaprun
TRIVY_IMAGE: docker.io/aquasec/trivy@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e
run: |
set -euo pipefail
if [ ! -f .trivyignore ]; then
echo " (no .trivyignore — skipping auto-clear check)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# Resolve the canonical [ours].digest from the pin file. release.yml's
# bump-pins job keeps this current with the latest stable release.
PIN_DIGEST="$(awk -F'"' '/^\[ours\]/{f=1} f && /^digest = /{print $2; exit}' references/zap-image-pin.toml)"
if [ -z "$PIN_DIGEST" ]; then
echo " (could not resolve [ours].digest from references/zap-image-pin.toml — skipping)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
IMAGE_REF="${IMAGE_NAME}@${PIN_DIGEST}"
echo "Scanning canonical pinned image: ${IMAGE_REF}"
mkdir -p .tmp/trivy-output
chmod 0777 .tmp/trivy-output
# Run trivy WITHOUT --ignorefile to see the unfiltered HIGH+CRITICAL set.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$PWD/.tmp/trivy-output:/out" \
"$TRIVY_IMAGE" image \
--severity HIGH,CRITICAL \
--scanners vuln \
--format json \
--output /out/scan.json \
--exit-code 0 \
"${IMAGE_REF}"
# Also scan extracted ZAP add-ons (the CVE bundle source for our
# currently-suppressed Netty entries).
rm -rf .tmp/zap-addons
mkdir -p .tmp/zap-addons
chmod 0777 .tmp/zap-addons
docker run --rm \
-v "$PWD/.tmp/zap-addons:/zap-addons" \
--entrypoint /bin/sh \
"${IMAGE_REF}" \
-c '
set -eu
for addon in /opt/zap/plugin/*.zap; do
[ -e "$addon" ] || continue
name="$(basename "$addon" .zap)"
mkdir -p "/zap-addons/$name"
python3 -m zipfile -e "$addon" "/zap-addons/$name"
done
'
docker run --rm \
-v "$PWD/.tmp/zap-addons:/scan:ro" \
-v "$PWD/.tmp/trivy-output:/out" \
"$TRIVY_IMAGE" fs \
--severity HIGH,CRITICAL \
--scanners vuln \
--format json \
--output /out/scan-addons.json \
--exit-code 0 \
/scan
# Collect all CVE IDs that Trivy still flags (image + add-ons).
flagged="$(python3 -c '
import json
flagged = set()
for f in [".tmp/trivy-output/scan.json", ".tmp/trivy-output/scan-addons.json"]:
try:
with open(f) as fh:
data = json.load(fh)
for res in data.get("Results", []) or []:
for vuln in res.get("Vulnerabilities", []) or []:
vid = vuln.get("VulnerabilityID")
if vid:
flagged.add(vid)
except FileNotFoundError:
pass
for v in sorted(flagged):
print(v)
')"
{
echo ""
echo "## Auto-clear-on-resolved-CVE check"
echo ""
echo "For every \`.trivyignore\` entry, check whether Trivy still flags it on the most recent \`:edge\` build."
echo ""
echo "| Suppressed CVE | Still flagged by Trivy? | Action |"
echo "|---|---|---|"
} >> "$GITHUB_STEP_SUMMARY"
while IFS= read -r cve; do
cve="$(echo "$cve" | xargs)"
[ -z "$cve" ] && continue
if echo "$flagged" | grep -qFx "$cve"; then
echo "| \`${cve}\` | ✅ yes — keep suppressed | (no change) |" >> "$GITHUB_STEP_SUMMARY"
else
echo "| \`${cve}\` | 🎯 NO LONGER FLAGGED | candidate for removal — manually verify Trivy DB freshness then drop the \`.trivyignore\` line + close the tracking issue |" >> "$GITHUB_STEP_SUMMARY"
fi
done < <(grep -vE '^\s*(#|$)' .trivyignore)
- name: Stale .trivyignore review
# For every CVE entry in `.trivyignore`, find the linked tracking
# issue (referenced by `#<num>` in the comment block above the CVE
# ID), pull its created_at via the GH API, and warn if the issue
# is open and > 60 days old. Doesn't fail the run — informational.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
{
echo "## Stale-.trivyignore review"
echo ""
echo "Cadence: each .trivyignore entry is expected to be re-evaluated within 60 days of its tracking issue's open date."
echo ""
echo "| CVE | Tracking issue | Open since | Age (days) | Status |"
echo "|---|---|---|---|---|"
} >> "$GITHUB_STEP_SUMMARY"
# Parse .trivyignore: each non-comment line is a CVE id. The
# block above it is its comment header containing `#<num>` issue
# link. The two CVE entries currently in the file both point at
# the same tracking issue (#11) — the loop below tolerates a 1:N
# mapping.
if [ ! -f .trivyignore ]; then
echo "No .trivyignore present; nothing to review." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
today_epoch=$(date -u +%s)
last_issue_block=""
while IFS= read -r line; do
if [[ "$line" =~ ^# ]]; then
last_issue_block+="${line}"$'\n'
continue
fi
if [[ -z "$line" ]]; then
last_issue_block=""
continue
fi
cve="$line"
# Extract first `#<num>` from the comment block above the CVE.
issue_num="$(echo "$last_issue_block" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)"
if [ -z "$issue_num" ]; then
echo "| \`${cve}\` | (no tracking issue link found) | — | — | ⚠ MISSING TRACKING |" >> "$GITHUB_STEP_SUMMARY"
last_issue_block=""
continue
fi
issue_json="$(gh api "/repos/${GITHUB_REPOSITORY}/issues/${issue_num}" 2>/dev/null || echo '{}')"
created_at="$(echo "$issue_json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("created_at",""))')"
state="$(echo "$issue_json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("state",""))')"
if [ -z "$created_at" ]; then
echo "| \`${cve}\` | #${issue_num} (not found) | — | — | ⚠ ISSUE MISSING |" >> "$GITHUB_STEP_SUMMARY"
last_issue_block=""
continue
fi
opened_epoch=$(python3 -c "from datetime import datetime; import sys; print(int(datetime.fromisoformat('${created_at}'.replace('Z','+00:00')).timestamp()))")
age_days=$(( (today_epoch - opened_epoch) / 86400 ))
status_icon="✅ within window"
if [ "$state" = "closed" ]; then
status_icon="🎯 closed — review .trivyignore entry for removal"
elif [ "$age_days" -gt 60 ]; then
status_icon="⚠️ STALE — exceeded 60-day cadence"
fi
echo "| \`${cve}\` | #${issue_num} | ${created_at} | ${age_days} | ${status_icon} |" >> "$GITHUB_STEP_SUMMARY"
last_issue_block=""
done < <(grep -vE '^\s*(#|$)' .trivyignore)