-
-
Notifications
You must be signed in to change notification settings - Fork 140
270 lines (239 loc) · 11.5 KB
/
Copy pathsecurity.yml
File metadata and controls
270 lines (239 loc) · 11.5 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
263
264
265
266
267
268
269
270
name: Security Scan
# Standalone, independent workflow. It can go red on its own without affecting
# the Unit Tests / Integration Tests / build workflows (they are separate
# workflows and never depend on this one). A red result here does NOT block
# merge unless this check is explicitly added to branch protection's required
# status checks - by default it is not.
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
permissions:
contents: read
jobs:
security-scan:
runs-on: ubuntu-latest
# Surface findings IN the pull request so they are reviewable before merge:
# Bandit -> code scanning annotations on the PR diff + a checks entry;
# pip-audit -> a job summary on the run (linked from the PR checks).
# NOTE: on a public repo these PR surfaces are visible to anyone who can
# see the PR, not only maintainers.
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install scanners
run: |
python -m pip install --upgrade pip
pip install "bandit[sarif]"
# Gate - high severity + high confidence fails this job (red check).
- name: Bandit SAST gate (fails on high-severity, high-confidence findings)
run: |
bandit -r . \
-x ./test,./.venv,./.venv-windows,./screenshot \
--severity-level high --confidence-level high
# medium+ report -> SARIF (for PR annotations + Security tab) and JSON (summary).
- name: Bandit report (SARIF + JSON, non-blocking)
if: always()
run: |
bandit -r . \
-x ./test,./.venv,./.venv-windows,./screenshot \
--severity-level medium --confidence-level medium \
-f sarif -o bandit.sarif --exit-zero
bandit -r . \
-x ./test,./.venv,./.venv-windows,./screenshot \
--severity-level medium --confidence-level medium \
-f json -o bandit.json --exit-zero
# Upload on BOTH push and pull_request so code scanning annotations appear
# inline on the PR diff before merge. category keeps Bandit separate from CodeQL.
- name: Upload Bandit SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit.sarif
category: bandit
# Counts on the run summary, linked from the PR checks.
- name: Bandit job summary
if: always()
run: |
python - <<'PY'
import json, os, collections
try:
data = json.load(open("bandit.json"))
except FileNotFoundError:
data = {"results": []}
results = data.get("results", [])
sev = collections.Counter(r["issue_severity"] for r in results)
conf = collections.Counter(r["issue_confidence"] for r in results)
lines = ["## Bandit SAST (medium+ report)", "",
"Total findings: %d" % len(results), "",
"| Level | Severity count | Confidence count |", "|---|---|---|"]
for lvl in ("HIGH", "MEDIUM", "LOW"):
lines.append("| %s | %d | %d |" % (lvl, sev.get(lvl, 0), conf.get(lvl, 0)))
lines += ["", "Inline findings annotate the PR diff; full list: Security tab -> Code scanning -> bandit."]
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
f.write("\n".join(lines) + "\n")
PY
# Downloadable full reports.
- name: Upload Bandit reports as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: bandit-reports
path: |
bandit.sarif
bandit.json
retention-days: 7
# Dependency CVEs. The official action renders a job summary (visible from
# the PR checks) and FAILS this job on any CVE (red check). It does NOT block
# merge unless this check is added to branch protection's required checks.
# Single accepted-CVE allowlist: one id per line, the text after '#' is the
# reason. Here '#' is a real shell comment, so the reason is stripped and only
# the bare id reaches pip-audit (the action's ignore-vulns is whitespace-split
# and cannot carry comments itself). Edit THIS list to accept/remove a CVE.
- name: Assemble pip-audit CVE allowlist
id: cve_allowlist
if: always()
run: |
ids=""
accept() { ids="$ids $1"; }
accept PYSEC-2025-217 # transformers X-CLIP checkpoint-conversion RCE - unreachable (no X-CLIP / checkpoint conversion)
accept CVE-2026-1839 # transformers Trainer torch.load RCE - unreachable (no Trainer); fix only in 5.0.0rc3 (major RC)
accept CVE-2026-4372 # transformers config _attn_implementation_internal RCE - unreachable (no AutoModelForCausalLM, only trusted bundled models, optional 'kernels' pkg not installed); fix only in 5.x major
accept PYSEC-2026-113 # pyarrow C++ Use-After-Free - Python binding is NOT vulnerable per the advisory
accept PYSEC-2026-169 # distributed (Dask) dashboard XSS - needs Jupyter Lab + jupyter-server-proxy + a Dask cluster; none run here
accept PYSEC-2026-2290 # transformers LightGlue trust_remote_code override RCE - unreachable (no LightGlue, no untrusted model repos, only trusted bundled models); fix only in 5.x major
echo "ids=$(echo $ids | xargs)" >> "$GITHUB_OUTPUT"
# Shared deps - full resolution, so transitive CVEs are caught too.
- name: pip-audit dependency CVEs (common)
if: always()
uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0
with:
inputs: requirements/common.txt
ignore-vulns: ${{ steps.cve_allowlist.outputs.ids }}
# CPU + NVIDIA GPU extras (onnxruntime / onnxruntime-gpu / cupy / cuml plus
# the deps cuml/RAPIDS resolves in). The noavx2 variant is intentionally NOT audited.
- name: pip-audit dependency CVEs (cpu + nvidia gpu)
if: always()
uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0
with:
no-deps: true
inputs: |
requirements/cpu.txt
requirements/gpu.txt
ignore-vulns: ${{ steps.cve_allowlist.outputs.ids }}
# Image / supply-chain layer: Trivy scans the container DEFINITION (Dockerfile
# + deployment manifests) for misconfigurations and the working tree for leaked
# secrets -- coverage pip-audit (Python CVEs) and Bandit (Python SAST) do not
# provide. The config scan (Dockerfile/manifest misconfig) is report-only
# (exit-code 0); the filesystem scan GATES (exit-code 1) so HIGH/CRITICAL
# dependency CVEs or leaked secrets turn this check red, matching pip-audit's
# fail-on-CVE behaviour. A red check does NOT block merge unless this workflow
# is added to branch protection's required checks. Findings are uploaded as
# SARIF to code scanning so the detailed table lives in the private Security
# tab (write/maintain/admin only) instead of the public Actions run log. A
# full built-image OS scan can later be hooked into the image-publish pipeline.
trivy:
runs-on: ubuntu-latest
# security-events:write lets the SARIF upload publish to the Security tab.
permissions:
contents: read
security-events: write
actions: read
steps:
- uses: actions/checkout@v4
- name: Install Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin
# Each scan emits native JSON once: it feeds both a numeric recap on the run
# summary (0 shown when clean) and, via `trivy convert`, the SARIF uploaded
# to the Security tab -- one scan, no second pass over the vuln DB.
# Accepted / not-applicable findings are listed in .trivyignore (the Trivy
# counterpart of the pip-audit allowlist above). Config scan is report-only.
- name: Trivy config scan (Dockerfiles + deployment manifests)
if: always()
run: |
trivy config --severity HIGH,CRITICAL --exit-code 0 \
--ignorefile .trivyignore \
--format json --output trivy-config.json .
- name: Convert Trivy config report to SARIF
if: always()
run: trivy convert --format sarif --output trivy-config.sarif trivy-config.json
- name: Upload Trivy config SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-config.sarif
category: trivy-config
# Filesystem scan GATES on HIGH/CRITICAL. --file-patterns adds the shipped
# requirements/*.txt to the pip vuln scan (Trivy's pip scanner otherwise only
# reads files named exactly requirements.txt); the -noavx2 legacy stacks and
# the dev-only test/requirements.txt are skipped. The detailed finding table
# reaches the Security tab via the converted SARIF (kept out of the public run
# log); only the counts are public. The step still exits non-zero on findings
# so the check goes red.
- name: Trivy filesystem scan (dependency CVEs + leaked secrets, fails on HIGH/CRITICAL)
run: |
trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 \
--ignorefile .trivyignore \
--file-patterns "pip:requirements/.*\.txt" \
--skip-files requirements/common-noavx2.txt,requirements/cpu-noavx2.txt,test/requirements.txt \
--format json --output trivy-fs.json \
--skip-dirs .venv,.venv-windows,native-build,test/songs,model,dist .
- name: Convert Trivy filesystem report to SARIF
if: always()
run: trivy convert --format sarif --output trivy-fs.sarif trivy-fs.json
- name: Upload Trivy filesystem SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-fs.sarif
category: trivy-fs
# Counts on the run summary (0 when clean), mirroring the Bandit summary.
- name: Trivy job summary
if: always()
run: |
python3 - <<'PY'
import json, os
def load(path):
try:
with open(path) as fh:
return json.load(fh)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def tally(report, key):
total = high = crit = 0
for res in (report.get("Results") or []):
for item in (res.get(key) or []):
total += 1
sev = (item.get("Severity") or "").upper()
if sev == "HIGH":
high += 1
elif sev == "CRITICAL":
crit += 1
return total, high, crit
cfg = load("trivy-config.json")
fs = load("trivy-fs.json")
rows = [
("Config misconfig (Dockerfiles / manifests)",) + tally(cfg, "Misconfigurations"),
("Dependency CVEs (filesystem)",) + tally(fs, "Vulnerabilities"),
("Leaked secrets (filesystem)",) + tally(fs, "Secrets"),
]
total = sum(r[1] for r in rows)
lines = ["## Trivy (HIGH / CRITICAL)", "",
"Total findings: %d" % total, "",
"| Scan | Findings | HIGH | CRITICAL |", "|---|---|---|---|"]
for name, t, h, c in rows:
lines.append("| %s | %d | %d | %d |" % (name, t, h, c))
lines += ["", "Detail for any non-zero row: Security tab -> Code scanning -> trivy-config / trivy-fs."]
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as fh:
fh.write("\n".join(lines) + "\n")
PY