@@ -136,27 +136,59 @@ jobs:
136136 fail-build : false
137137 severity-cutoff : critical
138138
139+ # Run Trivy again with JSON for severity breakdown
140+ - name : Scan with Trivy (JSON)
141+ if : always()
142+ uses : aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
143+ with :
144+ image-ref : " ghcr.io/huntridge-labs/argus/${{ matrix.image }}:${{ github.sha }}"
145+ format : ' json'
146+ output : ' trivy-results.json'
147+
139148 - name : Generate scan report
140149 if : always()
141150 run : |
142151 mkdir -p scan-reports
143- python3 -c "
144- import json
152+ python3 << 'PYEOF'
153+ import json, os
154+
155+ image_name = os.environ["IMAGE_NAME"]
156+ report = {"image": image_name, "critical": 0, "high": 0, "medium": 0, "low": 0, "total": 0, "top_findings": []}
157+
158+ # Parse Trivy JSON for severity counts and details
145159 try:
146- sarif = json.load(open('trivy-results.sarif'))
147- results = sarif.get('runs', [{}])[0].get('results', [])
148- report = {
149- 'image': '${IMAGE_NAME}',
150- 'finding_count': len(results),
151- 'findings': [
152- {'id': r.get('ruleId', ''), 'message': r.get('message', {}).get('text', '')[:100], 'level': r.get('level', '')}
153- for r in results[:20]
154- ],
155- }
156- except Exception:
157- report = {'image': '${IMAGE_NAME}', 'finding_count': -1, 'findings': []}
158- json.dump(report, open('scan-reports/${IMAGE_NAME}.json', 'w'))
159- "
160+ data = json.load(open("trivy-results.json"))
161+ seen = set()
162+ for result in data.get("Results", []):
163+ for vuln in result.get("Vulnerabilities", []):
164+ vid = vuln.get("VulnerabilityID", "")
165+ if vid in seen:
166+ continue
167+ seen.add(vid)
168+ sev = vuln.get("Severity", "UNKNOWN").upper()
169+ if sev == "CRITICAL":
170+ report["critical"] += 1
171+ elif sev == "HIGH":
172+ report["high"] += 1
173+ elif sev == "MEDIUM":
174+ report["medium"] += 1
175+ elif sev == "LOW":
176+ report["low"] += 1
177+ report["total"] += 1
178+ if len(report["top_findings"]) < 15:
179+ report["top_findings"].append({
180+ "id": vid,
181+ "severity": sev,
182+ "pkg": vuln.get("PkgName", ""),
183+ "installed": vuln.get("InstalledVersion", ""),
184+ "fixed": vuln.get("FixedVersion", ""),
185+ "title": vuln.get("Title", "")[:80],
186+ })
187+ except Exception as e:
188+ report["error"] = str(e)
189+
190+ json.dump(report, open(f"scan-reports/{image_name}.json", "w"), indent=2)
191+ PYEOF
160192 env :
161193 IMAGE_NAME : ${{ matrix.image }}
162194
@@ -302,53 +334,97 @@ jobs:
302334
303335 - name : Generate PR comment
304336 run : |
305- python3 -c "
337+ python3 << 'PYEOF'
306338 import json, glob, os
307339
308- lines = ['## Container Image Scan Results', '']
309-
310- # Container vulnerability table
311- reports = sorted(glob.glob('scan-reports/*.json'))
312- if reports:
313- lines.append('### Image Vulnerability Scan (Trivy + Grype)')
314- lines.append('')
315- lines.append('| Image | CRITICAL+HIGH | Status |')
316- lines.append('|-------|:---:|--------|')
317- total_vulns = 0
318- for path in reports:
319- data = json.load(open(path))
320- name = data['image']
321- count = data['finding_count']
322- if count < 0:
323- emoji, status = ':x:', 'Scan failed'
324- elif count == 0:
325- emoji, status = ':white_check_mark:', 'Clean'
326- else:
327- emoji, status = ':warning:', f'{count} finding(s)'
328- total_vulns += count
329- lines.append(f'| \`{name}\` | {count if count >= 0 else \"N/A\"} | {emoji} {status} |')
330- lines.append('')
331-
332- # Argus CLI scan summary
333- cli_json = 'cli-results/argus-results.json'
340+ lines = []
341+ lines.append("## 🔒 Argus Container Security Scan")
342+ lines.append("")
343+
344+ # ── Overall summary table ──
345+ reports = sorted(glob.glob("scan-reports/*.json"))
346+ totals = {"critical": 0, "high": 0, "medium": 0, "low": 0}
347+ image_data = []
348+ for path in reports:
349+ data = json.load(open(path))
350+ image_data.append(data)
351+ for sev in totals:
352+ totals[sev] += data.get(sev, 0)
353+
354+ lines.append("### 📊 Findings Summary")
355+ lines.append("")
356+ lines.append("| 🚨 Critical | ⚠️ High | 🟡 Medium | 🔵 Low | 📦 Total |")
357+ lines.append("|:-----------:|:-------:|:---------:|:------:|:--------:|")
358+ total = sum(totals.values())
359+ lines.append(f"| **{totals['critical']}** | **{totals['high']}** | **{totals['medium']}** | **{totals['low']}** | **{total}** |")
360+ lines.append("")
361+
362+ # ── Per-image breakdown ──
363+ for data in image_data:
364+ name = data["image"]
365+ c, h, m, l = data.get("critical", 0), data.get("high", 0), data.get("medium", 0), data.get("low", 0)
366+ img_total = c + h + m + l
367+
368+ if img_total == 0:
369+ lines.append(f"<details>")
370+ lines.append(f"<summary>📦 {name} — ✅ Clean</summary>")
371+ lines.append("")
372+ lines.append("No vulnerabilities found at any severity level.")
373+ lines.append("")
374+ lines.append("</details>")
375+ else:
376+ lines.append(f"<details>")
377+ lines.append(f"<summary>📦 {name} — {img_total} finding(s) (🚨{c} ⚠️{h} 🟡{m} 🔵{l})</summary>")
378+ lines.append("")
379+ lines.append("| 🚨 Critical | ⚠️ High | 🟡 Medium | 🔵 Low | Total |")
380+ lines.append("|:-----------:|:-------:|:---------:|:------:|:-----:|")
381+ lines.append(f"| **{c}** | **{h}** | **{m}** | **{l}** | **{img_total}** |")
382+ lines.append("")
383+
384+ findings = data.get("top_findings", [])
385+ if findings:
386+ lines.append("| CVE | Severity | Package | Installed | Fixed | Title |")
387+ lines.append("|-----|----------|---------|-----------|-------|-------|")
388+ for f in findings:
389+ sev = f.get("severity", "")
390+ sev_icon = {"CRITICAL": "🚨", "HIGH": "⚠️", "MEDIUM": "🟡", "LOW": "🔵"}.get(sev, "")
391+ lines.append(
392+ f"| {f.get('id', '')} | {sev_icon} {sev} | {f.get('pkg', '')} | {f.get('installed', '')} | {f.get('fixed', 'N/A')} | {f.get('title', '')} |"
393+ )
394+ if img_total > len(findings):
395+ lines.append(f"| | | | | | *...and {img_total - len(findings)} more* |")
396+
397+ lines.append("")
398+ lines.append("</details>")
399+ lines.append("")
400+
401+ # ── Argus CLI scan summary ──
402+ cli_json = "cli-results/argus-results.json"
334403 if os.path.exists(cli_json):
335404 data = json.load(open(cli_json))
336- results = data.get('results', [])
337- scanners = [r['scanner'] for r in results]
338- total = sum(len(r.get('findings', [])) for r in results)
339- lines.append('### Argus CLI Scan')
340- lines.append('')
341- lines.append(f'Scanners executed: **{len(scanners)}** ({', '.join(scanners)})')
342- lines.append(f'Total findings: **{total}**')
343- lines.append('')
344-
345- lines.append('---')
346- lines.append('*Container scans: Trivy + Grype. Code scan: Argus CLI. SARIF uploaded to Security tab.*')
347-
348- with open('comment-body.md', 'w') as f:
349- f.write('\n'.join(lines))
350- print('\n'.join(lines))
351- "
405+ results = data.get("results", [])
406+ scanners = [r["scanner"] for r in results]
407+ total_findings = sum(len(r.get("findings", [])) for r in results)
408+ lines.append("<details>")
409+ lines.append(f"<summary>🔍 Argus CLI Scan — {len(scanners)} scanner(s), {total_findings} finding(s)</summary>")
410+ lines.append("")
411+ for r in results:
412+ scanner = r["scanner"]
413+ count = len(r.get("findings", []))
414+ icon = "✅" if count == 0 else "⚠️"
415+ lines.append(f"- {icon} **{scanner}**: {count} finding(s)")
416+ lines.append("")
417+ lines.append("</details>")
418+ lines.append("")
419+
420+ lines.append("---")
421+ lines.append("*Scanned with Trivy + Grype (containers) and Argus CLI (code). SARIF uploaded to Security tab.*")
422+
423+ body = "\n".join(lines)
424+ with open("comment-body.md", "w") as f:
425+ f.write(body)
426+ print(body)
427+ PYEOF
352428
353429 - name : Post PR comment
354430 env :
0 commit comments