-
Notifications
You must be signed in to change notification settings - Fork 147
399 lines (354 loc) · 17.3 KB
/
weekly-security-audit.yml
File metadata and controls
399 lines (354 loc) · 17.3 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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
name: Weekly Security Audit
on:
schedule:
- cron: "0 8 * * 1" # Monday 8:00 UTC
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
dependency-confusion-check:
name: Dependency Confusion Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Scan for unregistered pip install targets
id: dep-check
run: |
python scripts/check_dependency_confusion.py --strict \
$(find . -name "*.md" -o -name "*.py" -o -name "*.ts" -o -name "*.txt" -o -name "*.yaml" -o -name "*.svg" -o -name "*.ipynb" \
| grep -v node_modules | grep -v .git | grep -v __pycache__ | grep -v .venv) \
> dep-confusion-report.txt 2>&1 || true
if [ -s dep-confusion-report.txt ]; then
echo "has-findings=true" >> "$GITHUB_OUTPUT"
echo "### ⚠️ Dependency Confusion Findings" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
cat dep-confusion-report.txt >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "has-findings=false" >> "$GITHUB_OUTPUT"
echo "### ✅ No dependency confusion findings" >> "$GITHUB_STEP_SUMMARY"
fi
security-skills-scan:
name: Security Skills Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Install dependencies
run: pip install --no-cache-dir pyyaml==6.0.2
- name: Run security skills scan
continue-on-error: true
run: |
python scripts/security_scan.py packages/ \
--exclude-tests \
--min-severity high \
--format text | tee security-report.txt
- name: Generate JSON report
if: always()
run: |
python scripts/security_scan.py packages/ \
--exclude-tests \
--format json > weekly-security-report.json || true
- name: Upload reports
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4.6.2
with:
name: weekly-security-audit
path: |
weekly-security-report.json
dep-confusion-report.txt
retention-days: 180
weak-crypto-check:
name: Weak Cryptography Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check for MD5/SHA1 in non-test code
run: |
echo "### Weak Cryptography Check" >> "$GITHUB_STEP_SUMMARY"
FINDINGS=$(grep -rn "hashlib\.md5\|hashlib\.sha1" --include="*.py" packages/ \
| grep -v "test_" | grep -v "text_tool" | grep -v "security_skills" \
| grep -v "example" | grep -v "benchmark" | grep -v "red_team" || true)
if [ -n "$FINDINGS" ]; then
echo "⚠️ MD5/SHA1 found in production code:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$FINDINGS" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No weak cryptography in production code" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check for pickle in non-test code
run: |
FINDINGS=$(grep -rn "pickle\.load" --include="*.py" packages/ \
| grep -v "test_" | grep -v "security_skills" | grep -v "# " || true)
if [ -n "$FINDINGS" ]; then
echo "⚠️ pickle usage found:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$FINDINGS" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No pickle deserialization in production code" >> "$GITHUB_STEP_SUMMARY"
fi
# ── MSRC-111178 regression check ──────────────────────────────────────
# Ensures the pull_request_target RCE fix (PR #303, #353) is never reverted.
workflow-security-check:
name: Workflow Security Regression Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check pull_request_target safety
run: |
echo "### Workflow Security Check (MSRC-111178)" >> "$GITHUB_STEP_SUMMARY"
FAIL=0
# No workflow should checkout HEAD ref on pull_request_target
HEAD_REFS=$(grep -rn 'ref:.*head\.sha\|ref:.*head_sha' .github/workflows/*.yml \
| grep -v '#' | grep -v 'workflow_run' || true)
if [ -n "$HEAD_REFS" ]; then
echo "🔴 CRITICAL: pull_request_target workflow checks out HEAD ref:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$HEAD_REFS" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
FAIL=1
else
echo "✅ No HEAD ref checkout in pull_request_target workflows" >> "$GITHUB_STEP_SUMMARY"
fi
# All pull_request_target checkouts must have persist-credentials: false
for f in .github/workflows/ai-*.yml; do
if grep -q "pull_request_target" "$f" && grep -q "actions/checkout" "$f"; then
if ! grep -q "persist-credentials: false" "$f"; then
echo "⚠️ $f: checkout missing persist-credentials: false" >> "$GITHUB_STEP_SUMMARY"
FAIL=1
fi
fi
done
if [ "$FAIL" -eq 0 ]; then
echo "✅ All pull_request_target workflows are safe" >> "$GITHUB_STEP_SUMMARY"
fi
exit $FAIL
# ── CI injection check ────────────────────────────────────────────────
# Detects github.event.* expressions injected directly into run: blocks
expression-injection-check:
name: Expression Injection Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check for unsafe expression interpolation
run: |
echo "### Expression Injection Check" >> "$GITHUB_STEP_SUMMARY"
FAIL=0
# Patterns that should NEVER appear directly in run: blocks
UNSAFE_PATTERNS=(
'github\.event\.issue\.title'
'github\.event\.issue\.body'
'github\.event\.pull_request\.title'
'github\.event\.pull_request\.body'
'github\.event\.comment\.body'
'github\.event\.discussion\.title'
'github\.event\.discussion\.body'
'steps\.[^.]*\.outputs\.all_changed_files'
)
for pattern in "${UNSAFE_PATTERNS[@]}"; do
# Find ${{ pattern }} in run: blocks (not in env: blocks which are safe)
MATCHES=$(grep -rn "\${{.*${pattern}" .github/workflows/*.yml \
| grep -v "env:" | grep -v "#" || true)
if [ -n "$MATCHES" ]; then
echo "⚠️ Potential injection: \`${pattern}\` in run: block:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$MATCHES" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
FAIL=1
fi
done
if [ "$FAIL" -eq 0 ]; then
echo "✅ No unsafe expression interpolation found" >> "$GITHUB_STEP_SUMMARY"
fi
# ── Docker and infrastructure check ───────────────────────────────────
docker-security-check:
name: Docker & Infrastructure Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check Dockerfiles for root user
run: |
echo "### Docker Security Check" >> "$GITHUB_STEP_SUMMARY"
# Check for containers running as root
ROOT_DOCKERFILES=()
while IFS= read -r dockerfile; do
if ! grep -q "^USER " "$dockerfile"; then
ROOT_DOCKERFILES+=("$dockerfile")
fi
done < <(find . -name "Dockerfile*" -not -path "*/node_modules/*" -not -path "*/.git/*")
if [ ${#ROOT_DOCKERFILES[@]} -gt 0 ]; then
echo "⚠️ Dockerfiles running as root (${#ROOT_DOCKERFILES[@]}):" >> "$GITHUB_STEP_SUMMARY"
for df in "${ROOT_DOCKERFILES[@]}"; do
echo " - \`$df\`" >> "$GITHUB_STEP_SUMMARY"
done
else
echo "✅ All Dockerfiles define a non-root USER" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check for wildcard CORS
run: |
CORS=$(grep -rn 'allow_origins.*\[.*"\*"' --include="*.py" packages/ \
| grep -v test_ | grep -v example || true)
if [ -n "$CORS" ]; then
echo "⚠️ Wildcard CORS found:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$CORS" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No wildcard CORS in production code" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check for hardcoded passwords
run: |
PASSWORDS=$(grep -rn 'PASSWORD.*=.*["\x27]' --include="*.yml" --include="*.yaml" \
packages/ docker-compose*.yml \
| grep -vi 'CHANGE_ME\|example\|template\|\${' || true)
if [ -n "$PASSWORDS" ]; then
echo "⚠️ Potential hardcoded passwords:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$PASSWORDS" | sed 's/=.*/=***REDACTED***/' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No hardcoded passwords found" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check for 0.0.0.0 bindings
run: |
BINDINGS=$(grep -rn 'host.*=.*"0\.0\.0\.0"\|default.*"0\.0\.0\.0"' --include="*.py" packages/ \
| grep -v test_ | grep -v example || true)
if [ -n "$BINDINGS" ]; then
echo "⚠️ Services binding to 0.0.0.0:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$BINDINGS" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No all-interface bindings in production code" >> "$GITHUB_STEP_SUMMARY"
fi
# ── XSS and unsafe DOM check ──────────────────────────────────────────
xss-check:
name: XSS & Unsafe DOM Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check for innerHTML without escaping
run: |
echo "### XSS & Unsafe DOM Check" >> "$GITHUB_STEP_SUMMARY"
# innerHTML in TypeScript (excluding test files and node_modules)
INNERHTML=$(grep -rn 'innerHTML' --include="*.ts" --include="*.tsx" packages/ \
| grep -v node_modules | grep -v test | grep -v "\.d\.ts" \
| grep -v "escHtml\|escapeHtml\|esc(" || true)
if [ -n "$INNERHTML" ]; then
echo "⚠️ innerHTML usage without visible escaping:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$INNERHTML" | head -20 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No unescaped innerHTML in TypeScript" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check for unsafe Python patterns
run: |
# eval/exec in production code
EVALS=$(grep -rn 'eval(\|exec(' --include="*.py" packages/ \
| grep -v test_ | grep -v security_skills | grep -v "# " \
| grep -v scanner | grep -v detect | grep -v "example" || true)
if [ -n "$EVALS" ]; then
echo "⚠️ eval/exec in production code:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$EVALS" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No eval/exec in production code" >> "$GITHUB_STEP_SUMMARY"
fi
# yaml.load (not safe_load)
YAML_UNSAFE=$(grep -rn 'yaml\.load(' --include="*.py" packages/ \
| grep -v safe_load | grep -v test_ | grep -v "# " | grep -v scanner || true)
if [ -n "$YAML_UNSAFE" ]; then
echo "⚠️ Unsafe yaml.load() found:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$YAML_UNSAFE" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No unsafe yaml.load() in production code" >> "$GITHUB_STEP_SUMMARY"
fi
# shell=True in subprocess
SHELL_TRUE=$(grep -rn 'shell=True' --include="*.py" packages/ \
| grep -v test_ | grep -v "# " | grep -v scanner || true)
if [ -n "$SHELL_TRUE" ]; then
echo "⚠️ subprocess shell=True found:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$SHELL_TRUE" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No shell=True in production code" >> "$GITHUB_STEP_SUMMARY"
fi
# ── Action pinning check ──────────────────────────────────────────────
action-pinning-check:
name: Action SHA Pinning Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check all actions are SHA-pinned
run: |
echo "### Action SHA Pinning Check" >> "$GITHUB_STEP_SUMMARY"
# Find uses: lines that reference external actions without SHA
UNPINNED=$(grep -rn "uses:" .github/workflows/*.yml \
| grep -v "./" | grep -v "#" \
| grep -vE "@[a-f0-9]{40}" || true)
if [ -n "$UNPINNED" ]; then
echo "⚠️ Unpinned actions found:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$UNPINNED" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ All external actions are SHA-pinned" >> "$GITHUB_STEP_SUMMARY"
fi
# ── Supply chain version pinning check ────────────────────────────────
version-pinning-check:
name: Version Pinning Compliance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check pyproject.toml upper bounds
run: |
echo "### Version Pinning Compliance" >> "$GITHUB_STEP_SUMMARY"
# Find >= without < upper bound in pyproject.toml
UNBOUNDED=$(grep -rn '>=.*"' --include="pyproject.toml" packages/ \
| grep -v '<' | grep -v '#' | grep -v 'requires-python' || true)
if [ -n "$UNBOUNDED" ]; then
echo "⚠️ Dependencies without upper bounds:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$UNBOUNDED" | head -20 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ All pyproject.toml deps have upper bounds" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check Docker image pinning
run: |
LATEST=$(grep -rn ':latest' --include="*.yml" --include="*.yaml" \
--include="Dockerfile*" packages/ docker-compose*.yml \
| grep -v '#' | grep -v node_modules || true)
if [ -n "$LATEST" ]; then
echo "⚠️ Docker images using :latest tag:" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "$LATEST" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ No :latest Docker image tags" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Check license field format (PyPI compliance)
run: |
# Cargo.toml must use bare string, pyproject.toml must use table
CARGO_BAD=$(grep -rn 'license = {' --include="Cargo.toml" packages/ || true)
PYPI_BAD=$(grep -rn '^license = "' --include="pyproject.toml" packages/ || true)
if [ -n "$CARGO_BAD" ] || [ -n "$PYPI_BAD" ]; then
echo "⚠️ License format issues:" >> "$GITHUB_STEP_SUMMARY"
[ -n "$CARGO_BAD" ] && echo "Cargo.toml should use bare string: $CARGO_BAD" >> "$GITHUB_STEP_SUMMARY"
[ -n "$PYPI_BAD" ] && echo "pyproject.toml should use table: $PYPI_BAD" >> "$GITHUB_STEP_SUMMARY"
else
echo "✅ License fields correctly formatted" >> "$GITHUB_STEP_SUMMARY"
fi