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