-
Notifications
You must be signed in to change notification settings - Fork 98
241 lines (223 loc) · 8.98 KB
/
Copy pathsecurity-scan.yml
File metadata and controls
241 lines (223 loc) · 8.98 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
# Security scanning workflow (issue #372).
#
# Runs [Trivy](https://github.com/aquasecurity/trivy) on every push to
# main, every pull request against main, and on a weekly schedule so
# newly disclosed CVEs surface automatically. Two scan targets:
#
# 1. Filesystem (`trivy fs`) — walks the checked-out source and
# surfaces CVEs in `package.json` lockfiles + the language-level
# manifests. This is the cheap, always-on scan.
#
# 2. Docker images (`trivy image`) — builds each production image
# locally and scans the resulting layers. OS package CVEs only
# show up here, not in the fs scan, so both targets are needed
# for full coverage.
#
# Severity is filtered to CRITICAL + HIGH to keep noise manageable;
# MEDIUM is left for the optional weekly schedule run (where we have
# more time to act on findings). Results are uploaded as SARIF to the
# GitHub Security tab so maintainers see them in the Code Scanning
# dashboard and can navigate to the offending line.
name: Security Scan
on:
push:
branches: [main]
pull_request_target:
# Single PR event so the scan and the PR-comment job share the
# same workflow run; otherwise the comment job's `download-artifact`
# would find an empty directory and post a misleading "0 critical,
# 0 high" summary. `pull_request_target` runs with the secrets
# the SARIF upload requires AND in the base repo's context, so
# the workflow code itself is always the version on main. The
# only code under inspection is the Trivy scanner, which reads
# files without executing PR code.
branches: [main]
schedule:
# Every Monday at 06:15 UTC — 15 minutes after Dependabot opens
# weekly PRs so we can re-check the repo shortly after a bump.
- cron: "15 6 * * 1"
# `security-events: write` is required by the CodeQL `upload-sarif`
# action to upload findings into GitHub's Code Scanning dashboard.
# `pull-requests: write` lets us post a summary comment on PRs.
# GitHub strips write permissions from pull_request events originating
# in forks, so SARIF uploads silently fail there. To still surface
# findings for fork PRs we mirror the high-permission jobs to
# `pull_request_target` and re-test the same revisions with the
# secrets it needs. Fork contributors don't run anything privileged;
# both target events only run the open-source `aquasecurity/trivy-action`
# against the immutable checkout ref.
permissions:
contents: read
pull-requests: write
security-events: write
# Cancel any in-flight job for the same PR / branch when a new
# commit lands. Without this, force-pushes can stack up scans.
concurrency:
group: security-scan-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ----------------------------------------------------------------
# Filesystem scan — fast, runs first, surfaces lockfile CVEs.
# ----------------------------------------------------------------
trivy-fs:
name: Trivy Filesystem Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history so Trivy can look at the lockfile even when
# the working tree only carries a partial checkout.
fetch-depth: 0
- name: Run Trivy filesystem scan (text)
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: "fs"
scan-ref: "."
format: "table"
ignore-unfixed: true
# Plain table — easy to scan in the workflow logs.
severity: "CRITICAL,HIGH"
# Don't fail the run on findings. Dependabot + the weekly
# schedule are the remediation channel; this step exists
# to keep the picture visible in the Security tab.
exit-code: "0"
- name: Generate Trivy filesystem scan (sarif)
if: always()
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: "fs"
scan-ref: "."
format: "sarif"
output: "trivy-fs.sarif"
ignore-unfixed: true
severity: "CRITICAL,HIGH,MEDIUM"
- name: Upload SARIF to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-fs.sarif
category: trivy-fs
# ----------------------------------------------------------------
# Docker image scan — runs per production image in parallel.
# Builds the image with BuildKit so we don't push to a registry
# (load:true keeps the layers on the runner), then scans the
# resulting tag with `trivy image`.
# ----------------------------------------------------------------
image-scan:
name: Trivy Image Scan (${{ matrix.image }})
runs-on: ubuntu-latest
needs: trivy-fs
strategy:
fail-fast: false
matrix:
include:
- image: api
context: api
- image: app
context: app
- image: processing
context: xstreamroll-processing
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
push: false
# Load into the runner's local daemon so `trivy image`
# can read the layers without a registry round trip.
load: true
tags: scan-target:${{ matrix.image }}
- name: Run Trivy image scan (text)
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: scan-target:${{ matrix.image }}
format: "table"
ignore-unfixed: true
severity: "CRITICAL,HIGH"
exit-code: "0"
- name: Generate Trivy image scan (sarif)
if: always()
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: scan-target:${{ matrix.image }}
format: "sarif"
output: trivy-image-${{ matrix.image }}.sarif
ignore-unfixed: true
severity: "CRITICAL,HIGH,MEDIUM"
- name: Upload SARIF to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-image-${{ matrix.image }}.sarif
category: trivy-image-${{ matrix.image }}
# ----------------------------------------------------------------
# PR comment summarising high+critical counts across all scanners
# so reviewers see at a glance whether their branch introduces new
# vulnerabilities. Best-effort; failures don't fail the workflow.
# ----------------------------------------------------------------
pr-summary:
name: Security Scan Summary
runs-on: ubuntu-latest
needs: [trivy-fs, image-scan]
# Only PR events have an issue to comment on; push and the weekly
# schedule skip this job. `pull_request_target` covers both
# same-repo and fork PRs since it's the only PR event we listen
# for above.
if: github.event_name == 'pull_request_target'
steps:
- name: Download SARIF artifacts
uses: actions/download-artifact@v4
with:
path: sarif
- name: Comment PR with vulnerability counts
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const path = require('path');
const dir = 'sarif';
if (!fs.existsSync(dir)) {
console.log('No SARIF directory found; skipping PR summary.');
return;
}
let totalCritical = 0;
let totalHigh = 0;
const perScanner = [];
for (const name of fs.readdirSync(dir)) {
if (!name.endsWith('.sarif')) continue;
const text = fs.readFileSync(path.join(dir, name), 'utf8');
const doc = JSON.parse(text);
const counts = { CRITICAL: 0, HIGH: 0 };
for (const run of doc.runs ?? []) {
for (const result of run.results ?? []) {
const sev = (result.level || '').toUpperCase();
if (sev === 'ERROR') counts.CRITICAL += 1;
else if (sev === 'WARNING') counts.HIGH += 1;
}
}
totalCritical += counts.CRITICAL;
totalHigh += counts.HIGH;
perScanner.push(`- \`${name}\`: ${counts.CRITICAL} critical, ${counts.HIGH} high`);
}
const body = [
'## 🔒 Security scan summary',
'',
`- **Critical:** ${totalCritical}`,
`- **High:** ${totalHigh}`,
'',
'Per-scanner breakdown:',
...perScanner,
'',
'> SARIF results are also available in the GitHub Security tab.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body,
});