/
/
/
1# Dependency Security Check Workflow
2# Checks Python dependencies for security vulnerabilities and supply chain risks
3
4name: Dependency Security Check
5
6on:
7 pull_request_target:
8 types: [opened, synchronize, reopened, labeled]
9 paths:
10 - "requirements_all.txt"
11 - "**/manifest.json"
12 - "pyproject.toml"
13 branches:
14 - stable
15 - dev
16
17permissions:
18 contents: read
19 pull-requests: write
20 issues: write # Needed to post PR comments
21
22jobs:
23 security-check:
24 runs-on: ubuntu-latest
25 steps:
26 - name: Check out code from GitHub
27 uses: actions/checkout@v6
28 with:
29 ref: ${{ github.event.pull_request.head.sha }}
30 fetch-depth: 0 # Need full history for diff
31
32 - name: Detect automated dependency PRs
33 id: pr_type
34 run: |
35 PR_AUTHOR="${{ github.event.pull_request.user.login }}"
36 PR_LABELS="${{ toJson(github.event.pull_request.labels.*.name) }}"
37
38 # Check if PR is from dependabot, renovate, or has auto-merge label
39 if [[ "$PR_AUTHOR" == "dependabot[bot]" ]] || \
40 [[ "$PR_AUTHOR" == "renovate[bot]" ]] || \
41 echo "$PR_LABELS" | grep -q "auto-merge"; then
42 echo "is_automated=true" >> $GITHUB_OUTPUT
43 echo "â
Detected automated dependency update PR - will auto-approve security checks"
44 else
45 echo "is_automated=false" >> $GITHUB_OUTPUT
46 fi
47
48 - name: Set up Python
49 uses: actions/[email protected]
50 with:
51 python-version: "3.12"
52
53 - name: Install security tools
54 run: |
55 pip install pip-audit
56
57 # Step 1: Verify requirements_all.txt is in sync
58 - name: Check requirements_all.txt sync
59 id: req_sync
60 run: |
61 # Save current requirements_all.txt
62 cp requirements_all.txt requirements_all.txt.original
63
64 # Regenerate requirements_all.txt
65 python3 scripts/gen_requirements_all.py
66
67 # Check if it changed
68 if ! diff -q requirements_all.txt.original requirements_all.txt > /dev/null; then
69 echo "status=out_of_sync" >> $GITHUB_OUTPUT
70 echo "â ï¸ **requirements_all.txt is out of sync**" > sync_report.md
71 echo "" >> sync_report.md
72 echo "The \`requirements_all.txt\` file should be auto-generated from \`pyproject.toml\` and provider manifests." >> sync_report.md
73 echo "" >> sync_report.md
74 echo "**Action required:** Run \`python scripts/gen_requirements_all.py\` and commit the changes." >> sync_report.md
75 # Restore original
76 mv requirements_all.txt.original requirements_all.txt
77 else
78 echo "status=synced" >> $GITHUB_OUTPUT
79 echo "â
requirements_all.txt is properly synchronized" > sync_report.md
80 rm requirements_all.txt.original
81 fi
82
83 # Step 2: Run pip-audit for known vulnerabilities
84 - name: Run pip-audit on all requirements
85 id: pip_audit
86 continue-on-error: true
87 run: |
88 echo "## ð Vulnerability Scan Results" > audit_report.md
89 echo "" >> audit_report.md
90
91 if pip-audit -r requirements_all.txt --desc --format=markdown >> audit_report.md 2>&1; then
92 echo "status=pass" >> $GITHUB_OUTPUT
93 echo "â
No known vulnerabilities found" >> audit_report.md
94 else
95 echo "status=fail" >> $GITHUB_OUTPUT
96 echo "" >> audit_report.md
97 echo "â ï¸ **Vulnerabilities detected! Please review the findings above.**" >> audit_report.md
98 fi
99
100 cat audit_report.md
101
102 # Step 2: Detect new or changed dependencies
103 - name: Detect dependency changes
104 id: deps_check
105 run: |
106 # Get base branch (dev or stable)
107 BASE_BRANCH="${{ github.base_ref }}"
108
109 # Check for changes in requirements_all.txt
110 if git diff origin/$BASE_BRANCH...HEAD -- requirements_all.txt > /dev/null 2>&1; then
111 # Extract added lines (new or modified dependencies)
112 git diff origin/$BASE_BRANCH...HEAD -- requirements_all.txt | \
113 grep "^+" | grep -v "^+++" | sed 's/^+//' > new_deps_raw.txt || true
114
115 # Also check for version changes (lines that were modified)
116 git diff origin/$BASE_BRANCH...HEAD -- requirements_all.txt | \
117 grep "^-" | grep -v "^---" | sed 's/^-//' > old_deps_raw.txt || true
118
119 if [ -s new_deps_raw.txt ]; then
120 echo "has_changes=true" >> $GITHUB_OUTPUT
121 echo "## ð¦ Dependency Changes Detected" > deps_report.md
122 echo "" >> deps_report.md
123 echo "The following dependencies were added or modified:" >> deps_report.md
124 echo "" >> deps_report.md
125 echo '```diff' >> deps_report.md
126 git diff origin/$BASE_BRANCH...HEAD -- requirements_all.txt >> deps_report.md
127 echo '```' >> deps_report.md
128 echo "" >> deps_report.md
129
130 # Extract just package names for safety check
131 cat new_deps_raw.txt | grep -v "^#" | grep -v "^$" > new_deps.txt || true
132
133 if [ -s new_deps.txt ]; then
134 echo "New/modified packages to review:" >> deps_report.md
135 cat new_deps.txt | while read line; do
136 echo "- \`$line\`" >> deps_report.md
137 done
138 fi
139 else
140 echo "has_changes=false" >> $GITHUB_OUTPUT
141 echo "No dependency changes detected in requirements_all.txt" > deps_report.md
142 fi
143 else
144 echo "has_changes=false" >> $GITHUB_OUTPUT
145 echo "No dependency changes detected" > deps_report.md
146 fi
147
148 cat deps_report.md
149
150 # Step 3: Check manifest.json changes
151 - name: Check provider manifest changes
152 id: manifest_check
153 run: |
154 BASE_BRANCH="${{ github.base_ref }}"
155
156 # Find all changed manifest.json files
157 CHANGED_MANIFESTS=$(git diff --name-only origin/$BASE_BRANCH...HEAD | grep "manifest.json" || true)
158
159 if [ -n "$CHANGED_MANIFESTS" ]; then
160 echo "## ð Provider Manifest Changes" > manifest_report.md
161 echo "" >> manifest_report.md
162
163 HAS_REQ_CHANGES=false
164
165 for manifest in $CHANGED_MANIFESTS; do
166 # Check if requirements actually changed
167 OLD_REQS=$(git show origin/$BASE_BRANCH:$manifest 2>/dev/null | python3 -c "import sys, json; data=json.load(sys.stdin); print(' '.join(data.get('requirements', [])))" 2>/dev/null || echo "")
168 NEW_REQS=$(cat $manifest | python3 -c "import sys, json; data=json.load(sys.stdin); print(' '.join(data.get('requirements', [])))" 2>/dev/null || echo "")
169
170 if [ "$OLD_REQS" != "$NEW_REQS" ]; then
171 HAS_REQ_CHANGES=true
172 echo "### \`$manifest\`" >> manifest_report.md
173 echo "" >> manifest_report.md
174
175 # Save old and new versions for comparison
176 git show origin/$BASE_BRANCH:$manifest > /tmp/old_manifest.json 2>/dev/null || echo '{"requirements":[]}' > /tmp/old_manifest.json
177 cp $manifest /tmp/new_manifest.json
178
179 # Use Python script to parse dependency changes
180 python3 scripts/parse_manifest_deps.py /tmp/old_manifest.json /tmp/new_manifest.json >> manifest_report.md
181 echo "" >> manifest_report.md
182 fi
183 done
184
185 if [ "$HAS_REQ_CHANGES" = "true" ]; then
186 echo "has_changes=true" >> $GITHUB_OUTPUT
187 else
188 echo "has_changes=false" >> $GITHUB_OUTPUT
189 echo "Manifest files changed but no dependency changes detected" > manifest_report.md
190 fi
191 else
192 echo "has_changes=false" >> $GITHUB_OUTPUT
193 echo "No provider manifest changes detected" > manifest_report.md
194 fi
195
196 cat manifest_report.md
197
198 # Step 4: Run package safety check on new dependencies
199 - name: Check new package safety
200 id: safety_check
201 if: steps.deps_check.outputs.has_changes == 'true'
202 continue-on-error: true
203 run: |
204 echo "## ð¡ï¸ Supply Chain Security Check" > safety_report.md
205 echo "" >> safety_report.md
206
207 if [ -f new_deps.txt ] && [ -s new_deps.txt ]; then
208 # Run our custom safety check script
209 python scripts/check_package_safety.py new_deps.txt > safety_output.txt 2>&1
210 SAFETY_EXIT=$?
211
212 cat safety_output.txt >> safety_report.md
213 echo "" >> safety_report.md
214
215 # Parse automated check results
216 if grep -q "â
.*Trusted Sources.*All packages" safety_output.txt; then
217 echo "trusted_sources=pass" >> $GITHUB_OUTPUT
218 else
219 echo "trusted_sources=fail" >> $GITHUB_OUTPUT
220 fi
221
222 if grep -q "â
.*Typosquatting.*No suspicious" safety_output.txt; then
223 echo "typosquatting=pass" >> $GITHUB_OUTPUT
224 else
225 echo "typosquatting=fail" >> $GITHUB_OUTPUT
226 fi
227
228 if grep -q "â
.*License.*All licenses" safety_output.txt; then
229 echo "license=pass" >> $GITHUB_OUTPUT
230 else
231 echo "license=fail" >> $GITHUB_OUTPUT
232 fi
233
234 if [ $SAFETY_EXIT -eq 2 ]; then
235 echo "status=high_risk" >> $GITHUB_OUTPUT
236 echo "" >> safety_report.md
237 echo "â ï¸ **HIGH RISK PACKAGES DETECTED**" >> safety_report.md
238 echo "Manual security review is **required** before merging this PR." >> safety_report.md
239 elif [ $SAFETY_EXIT -eq 1 ]; then
240 echo "status=medium_risk" >> $GITHUB_OUTPUT
241 echo "" >> safety_report.md
242 echo "â ï¸ **MEDIUM RISK PACKAGES DETECTED**" >> safety_report.md
243 echo "Please review the warnings above before merging." >> safety_report.md
244 else
245 echo "status=pass" >> $GITHUB_OUTPUT
246 fi
247 else
248 echo "No new dependencies to check" >> safety_report.md
249 echo "status=pass" >> $GITHUB_OUTPUT
250 echo "trusted_sources=pass" >> $GITHUB_OUTPUT
251 echo "typosquatting=pass" >> $GITHUB_OUTPUT
252 echo "license=pass" >> $GITHUB_OUTPUT
253 fi
254
255 cat safety_report.md
256
257 # Step 5: Combine all reports and post as PR comment
258 - name: Create combined security report
259 id: report
260 run: |
261 echo "# ð Dependency Security Report" > security_report.md
262 echo "" >> security_report.md
263
264 if [ "${{ steps.deps_check.outputs.has_changes }}" == "true" ] || [ "${{ steps.manifest_check.outputs.has_changes }}" == "true" ]; then
265 # 1. Show sync status if out of sync
266 if [ "${{ steps.req_sync.outputs.status }}" == "out_of_sync" ]; then
267 cat sync_report.md >> security_report.md
268 echo "" >> security_report.md
269 echo "---" >> security_report.md
270 echo "" >> security_report.md
271 fi
272
273 # 2. Modified Dependencies Section (consolidated)
274 echo "## ð¦ Modified Dependencies" >> security_report.md
275 echo "" >> security_report.md
276
277 # Combine requirements_all.txt and manifest changes
278 HAS_DEPS=false
279
280 if [ "${{ steps.manifest_check.outputs.has_changes }}" == "true" ]; then
281 cat manifest_report.md | grep -v "^## " >> security_report.md
282 HAS_DEPS=true
283 fi
284
285 if [ "${{ steps.deps_check.outputs.has_changes }}" == "true" ]; then
286 if [ "$HAS_DEPS" = "true" ]; then
287 echo "" >> security_report.md
288 fi
289 cat deps_report.md | grep -v "^## " >> security_report.md
290 fi
291
292 echo "" >> security_report.md
293 echo "---" >> security_report.md
294 echo "" >> security_report.md
295
296 # 3. Vulnerability Scan Results
297 cat audit_report.md >> security_report.md
298 echo "" >> security_report.md
299 echo "---" >> security_report.md
300 echo "" >> security_report.md
301
302 # 4. Automated Security Checks
303 echo "### Automated Security Checks" >> security_report.md
304 echo "" >> security_report.md
305
306 # Vulnerability scan check
307 if [ "${{ steps.pip_audit.outputs.status }}" == "fail" ]; then
308 echo "- â **Vulnerability Scan**: Failed - Known vulnerabilities detected" >> security_report.md
309 else
310 echo "- â
**Vulnerability Scan**: Passed - No known vulnerabilities" >> security_report.md
311 fi
312
313 # Trusted sources check
314 if [ "${{ steps.safety_check.outputs.trusted_sources }}" == "fail" ]; then
315 echo "- â **Trusted Sources**: Some packages missing source repository" >> security_report.md
316 else
317 echo "- â
**Trusted Sources**: All packages have verified source repositories" >> security_report.md
318 fi
319
320 # Typosquatting check
321 if [ "${{ steps.safety_check.outputs.typosquatting }}" == "fail" ]; then
322 echo "- â **Typosquatting Check**: Suspicious package names detected!" >> security_report.md
323 else
324 echo "- â
**Typosquatting Check**: No suspicious package names detected" >> security_report.md
325 fi
326
327 # License compatibility check
328 if [ "${{ steps.safety_check.outputs.license }}" == "fail" ]; then
329 echo "- â ï¸ **License Compatibility**: Some licenses may not be compatible" >> security_report.md
330 else
331 echo "- â
**License Compatibility**: All licenses are OSI-approved and compatible" >> security_report.md
332 fi
333
334 # Supply chain risk check
335 if [ "${{ steps.safety_check.outputs.status }}" == "high_risk" ]; then
336 echo "- â **Supply Chain Risk**: High risk packages detected" >> security_report.md
337 elif [ "${{ steps.safety_check.outputs.status }}" == "medium_risk" ]; then
338 echo "- â ï¸ **Supply Chain Risk**: Medium risk - review recommended" >> security_report.md
339 else
340 echo "- â
**Supply Chain Risk**: Passed - packages appear mature and maintained" >> security_report.md
341 fi
342
343 echo "" >> security_report.md
344
345 # Check if automated PR
346 if [ "${{ steps.pr_type.outputs.is_automated }}" == "true" ]; then
347 echo "> ð¤ **Automated dependency update** - This PR is from a trusted source (dependabot/renovate) and will be auto-approved if all checks pass." >> security_report.md
348 echo "" >> security_report.md
349 fi
350
351 echo "### Manual Review" >> security_report.md
352 echo "" >> security_report.md
353 echo "**Maintainer approval required:**" >> security_report.md
354 echo "" >> security_report.md
355 echo "- [ ] **I have reviewed the changes above and approve these dependency updates**" >> security_report.md
356 echo "" >> security_report.md
357
358 if [ "${{ steps.pr_type.outputs.is_automated }}" == "true" ]; then
359 echo "_Automated PRs with all checks passing will be auto-approved._" >> security_report.md
360 else
361 echo "**To approve:** Comment \`/approve-dependencies\` or manually add the \`dependencies-reviewed\` label." >> security_report.md
362 fi
363 else
364 echo "â
No dependency changes detected in this PR." >> security_report.md
365 fi
366
367 cat security_report.md
368
369 # Add to GitHub job summary (always available, even for forks)
370 cat security_report.md >> $GITHUB_STEP_SUMMARY
371
372 # Step 6: Post comment to PR
373 - name: Post security report to PR
374 uses: actions/github-script@v8
375 with:
376 script: |
377 const fs = require('fs');
378 const report = fs.readFileSync('security_report.md', 'utf8');
379
380 // Find existing bot comment
381 const comments = await github.rest.issues.listComments({
382 owner: context.repo.owner,
383 repo: context.repo.repo,
384 issue_number: context.issue.number,
385 });
386
387 const botComment = comments.data.find(comment =>
388 comment.user.type === 'Bot' &&
389 comment.body.includes('ð Dependency Security Report')
390 );
391
392 if (botComment) {
393 // Update existing comment
394 await github.rest.issues.updateComment({
395 owner: context.repo.owner,
396 repo: context.repo.repo,
397 comment_id: botComment.id,
398 body: report
399 });
400 } else {
401 // Create new comment
402 await github.rest.issues.createComment({
403 owner: context.repo.owner,
404 repo: context.repo.repo,
405 issue_number: context.issue.number,
406 body: report
407 });
408 }
409
410 # Step 7: Check for approval label (if dependencies changed)
411 - name: Check for security review approval
412 if: |
413 (steps.deps_check.outputs.has_changes == 'true' ||
414 steps.manifest_check.outputs.has_changes == 'true')
415 uses: actions/github-script@v8
416 with:
417 script: |
418 const labels = context.payload.pull_request.labels.map(l => l.name);
419 const hasReviewLabel = labels.includes('dependencies-reviewed');
420 const isAutomated = '${{ steps.pr_type.outputs.is_automated }}' === 'true';
421 const isHighRisk = '${{ steps.safety_check.outputs.status }}' === 'high_risk';
422 const hasFailed = '${{ steps.pip_audit.outputs.status }}' === 'fail';
423
424 if (isHighRisk) {
425 core.setFailed('ð´ HIGH RISK dependencies detected! This PR requires thorough security review before merging.');
426 } else if (hasFailed) {
427 core.setFailed('ð´ Known vulnerabilities detected! Please address the security issues above.');
428 } else if (isAutomated) {
429 // Auto-approve automated PRs if security checks passed
430 core.info('â
Automated dependency update with passing security checks - auto-approved');
431
432 // Optionally add the label automatically
433 await github.rest.issues.addLabels({
434 owner: context.repo.owner,
435 repo: context.repo.repo,
436 issue_number: context.issue.number,
437 labels: ['dependencies-reviewed']
438 });
439 } else if (!hasReviewLabel) {
440 core.setFailed('â ï¸ Dependency changes detected. A maintainer must add the "dependencies-reviewed" label after security review.');
441 } else {
442 core.info('â
Security review approved via "dependencies-reviewed" label');
443 }
444
445 # Step 8: Fail the check if high-risk or vulnerabilities found
446 - name: Final security status
447 if: always()
448 run: |
449 if [ "${{ steps.pip_audit.outputs.status }}" == "fail" ]; then
450 echo "â Known vulnerabilities found!"
451 exit 1
452 fi
453
454 if [ "${{ steps.safety_check.outputs.status }}" == "high_risk" ]; then
455 echo "â High-risk packages detected!"
456 exit 1
457 fi
458
459 if [ "${{ steps.deps_check.outputs.has_changes }}" == "true" ] || [ "${{ steps.manifest_check.outputs.has_changes }}" == "true" ]; then
460 echo "â ï¸ Dependency changes require review"
461 # Don't fail here - the label check above will handle it
462 fi
463
464 echo "â
Security checks completed"
465