-
Notifications
You must be signed in to change notification settings - Fork 0
262 lines (256 loc) · 11.1 KB
/
security-scan.yml
File metadata and controls
262 lines (256 loc) · 11.1 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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
name: security-scan
# Re-runs the same two security scanners that gate PRs (ci.yml's
# `image-scan` job from #68 and `govulncheck` from `security` in #41)
# against the latest `main` SHA on a daily schedule. The goal is to
# surface CVEs disclosed against deps that have NOT changed since the
# last PR — without a periodic re-scan these are invisible until the
# next time someone bumps the affected dep. A failing scanner job
# produces a red row in the Actions list AND triggers `file-issue`
# below, which opens a labelled GitHub issue so the regression has an
# owner (see #73).
on:
schedule:
# 06:00 UTC daily. Off-peak for US/EU/JP and keeps the
# disclosure-to-detection window under ~24h. See § Schedule
# cadence in 72-periodic-security-scan.md for rationale.
- cron: '0 6 * * *'
workflow_dispatch:
# `contents: read` is the only workflow-level permission. The
# `issues: write` widening this workflow needs lives in the
# `file-issue` job below, scoped to that single job (NOT widened
# here). AC #3 on #73 mandates job-level scoping; a future
# maintainer should NOT promote `issues: write` to this block.
permissions:
contents: read
# Coalesce overlapping runs (a manual `workflow_dispatch` firing
# while a scheduled run is in flight, or vice versa). Queueing rather
# than cancelling avoids leaving artifacts half-uploaded between the
# scanner jobs and `file-issue`.
concurrency:
group: security-scan
cancel-in-progress: false
jobs:
image-scan:
runs-on: ubuntu-latest
# Belt-and-suspenders mirror of ci.yml's image-scan job: even
# though the workflow-level block already grants only
# `contents: read`, a future top-level escalation must not
# silently widen this job. Same convention as ci.yml.
permissions:
contents: read
steps:
# Explicit `ref: main` so a future change to the default-branch
# name or a manual `workflow_dispatch` invocation from a feature
# branch still scans `main` (the ticket's AC #2 names `main`).
- uses: actions/checkout@v6
with:
ref: main
- name: build image
# Same artifact construction as ci.yml's image-scan job.
run: docker build -t pyrycode-relay:${{ github.sha }} .
# Tracks: aquasecurity/trivy-action@v0.36.0
# Pinned by commit SHA so a tag-swap upstream cannot change
# what runs against our image between Renovate bumps. Mirror
# of ci.yml's image-scan pin — must move in lockstep with that
# file when Renovate bumps the action.
#
# `format: json` + `output: trivy.json` differs from ci.yml's
# `format: table`: the cron path machine-parses the output to
# render an issue body, the PR path only needs human-readable
# console output. Policy flags (severity, ignore-unfixed,
# vuln-type, exit-code) are unchanged.
- name: trivy image scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25
with:
image-ref: pyrycode-relay:${{ github.sha }}
format: json
output: trivy.json
severity: CRITICAL,HIGH
ignore-unfixed: true
vuln-type: os,library
exit-code: '1'
- name: render issue from trivy output
if: failure()
run: |
set -euo pipefail
primary_cve=$(jq -r '
[ .Results[]?.Vulnerabilities[]? ]
| sort_by(.Severity) | reverse | .[0].VulnerabilityID // empty
' trivy.json)
primary_pkg=$(jq -r '
[ .Results[]?.Vulnerabilities[]? ]
| sort_by(.Severity) | reverse | .[0]
| "\(.PkgName // "unknown")@\(.InstalledVersion // "unknown")"
' trivy.json)
# If trivy.json has no parseable vulnerabilities (e.g. the
# action failed before producing output, or produced an
# empty results array), there is nothing to file — skip
# rendering and leave the red workflow row as the sole
# signal. The upload step's hashFiles() guard ensures no
# artifact is uploaded in this case.
if [ -z "$primary_cve" ]; then
echo "no parseable CVEs in trivy.json — skipping issue render"
exit 0
fi
printf 'security-scan regression: %s in image package %s\n' \
"$primary_cve" "$primary_pkg" > issue-title.txt
{
printf '## Periodic security-scan regression\n\n'
printf '**Scanner:** Trivy (image scan)\n'
printf '**Primary finding:** `%s` in `%s`\n' "$primary_cve" "$primary_pkg"
printf '**Workflow run:** %s/%s/actions/runs/%s\n\n' \
"${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_RUN_ID}"
printf '### All findings\n\n```\n'
jq -r '
.Results[]?.Vulnerabilities[]?
| "\(.Severity)\t\(.VulnerabilityID)\t\(.PkgName)@\(.InstalledVersion)\t\(.FixedVersion // "no fix")"
' trivy.json | sort -u | head -100
printf '```\n\n### Triage\n'
printf -- '- Bump the offending dependency or base-image digest.\n'
printf -- '- Verify the next `security-scan` workflow run is green.\n'
printf -- '- Close this issue once the next run passes.\n'
} > issue-body.md
- name: upload issue artifact
if: failure() && hashFiles('issue-title.txt') != ''
uses: actions/upload-artifact@v5
with:
name: regression-image-scan
path: |
issue-title.txt
issue-body.md
retention-days: 7
if-no-files-found: ignore
govulncheck:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
ref: main
- uses: actions/setup-go@v6
with:
go-version: '1.26.x'
# Tracks: golang.org/x/vuln/cmd/govulncheck@v1.1.4
# Mirror of ci.yml's `security` job pin — must move in lockstep
# with that file when Renovate bumps the tag. Same rationale
# for a semver tag vs commit SHA documented in #41's spec
# (Go module checksum-DB makes a tag effectively immutable for
# `go install`).
- name: install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
# `-json` differs from ci.yml's plain `govulncheck ./...`: the
# cron path machine-parses the output to render an issue body.
# `set -o pipefail` documents intent and protects future
# refactors that pipe through `tee` for log visibility.
- name: govulncheck
run: |
set -o pipefail
govulncheck -json ./... > govulncheck.json
- name: render issue from govulncheck output
if: failure()
run: |
set -euo pipefail
# govulncheck -json emits newline-delimited records; pick
# out the OSV.id from the "osv" record kind.
primary_id=$(jq -rs '
[ .[] | select(.osv?.id?) | .osv.id ] | .[0] // empty
' govulncheck.json)
primary_mod=$(jq -rs '
[ .[] | select(.osv?.affected?) | .osv.affected[0].package.name ]
| .[0] // "unknown"
' govulncheck.json)
if [ -z "$primary_id" ]; then
echo "no parseable vulns in govulncheck.json — skipping issue render"
exit 0
fi
printf 'security-scan regression: %s in Go module %s\n' \
"$primary_id" "$primary_mod" > issue-title.txt
{
printf '## Periodic security-scan regression\n\n'
printf '**Scanner:** govulncheck (Go modules)\n'
printf '**Primary finding:** `%s` in `%s`\n' "$primary_id" "$primary_mod"
printf '**Workflow run:** %s/%s/actions/runs/%s\n\n' \
"${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_RUN_ID}"
printf '### All findings\n\n```\n'
jq -rs '
.[] | select(.osv?.id?)
| "\(.osv.id)\t\(.osv.affected[0].package.name // "unknown")"
' govulncheck.json | sort -u | head -100
printf '```\n\n### Triage\n'
printf -- '- Run `govulncheck ./...` locally against the named module to see call-site traces.\n'
printf -- '- Bump the offending dependency.\n'
printf -- '- Verify the next `security-scan` workflow run is green.\n'
printf -- '- Close this issue once the next run passes.\n'
} > issue-body.md
- name: upload issue artifact
if: failure() && hashFiles('issue-title.txt') != ''
uses: actions/upload-artifact@v5
with:
name: regression-govulncheck
path: |
issue-title.txt
issue-body.md
retention-days: 7
if-no-files-found: ignore
file-issue:
needs: [image-scan, govulncheck]
if: failure()
runs-on: ubuntu-latest
# `issues: write` is granted at the JOB level — per AC #3 on #73
# it must NOT be promoted to the workflow-level permissions block.
# The scanner jobs above run untrusted scanner code and keep
# `contents: read` only; this job does no scanning and only posts
# pre-rendered content via `gh`.
permissions:
contents: read
issues: write
steps:
- name: download regression artifacts
uses: actions/download-artifact@v5
with:
# Matches both `regression-image-scan` and
# `regression-govulncheck`. Missing artifacts (e.g. one
# scanner failed for a non-CVE reason and produced no
# artifact) are silently absent.
pattern: regression-*
path: artifacts
merge-multiple: false
- name: file issues
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
shopt -s nullglob
for dir in artifacts/regression-*; do
title=$(cat "$dir/issue-title.txt")
body_file="$dir/issue-body.md"
# Dedup: search open security-sensitive issues for one
# whose title matches the full deterministic string we
# are about to file. `in:title` avoids matching unrelated
# issues whose body happens to mention the CVE id. State
# filter is `open` so a regression of a previously-closed
# CVE files a fresh issue.
existing=$(gh issue list \
--repo "$REPO" \
--state open \
--label security-sensitive \
--search "in:title \"$title\"" \
--json number \
--jq 'length')
if [ "$existing" -gt 0 ]; then
echo "duplicate suppressed for: $title"
continue
fi
# `--body-file` (not `--body "<...>"`) so scanner-derived
# content (CVE descriptions, package version strings with
# backticks or `$`) never passes through shell-argument
# expansion. Title is the rendered single-line string we
# wrote ourselves — ASCII by construction.
gh issue create \
--repo "$REPO" \
--title "$title" \
--body-file "$body_file" \
--label security-sensitive
done