Skip to content

Commit be3ff25

Browse files
committed
feat(ci): rich container scan PR comment with severity breakdown
PR comment now matches the classic Argus scanner summary style: - Overall findings summary table (critical/high/medium/low/total) - Per-image collapsible sections with severity counts - Top 15 CVEs per image with package, version, and fix info - Argus CLI scan results in collapsible section - Emoji severity indicators matching scanner-summary format Scan report now captures full Trivy JSON (not just SARIF) to extract severity breakdown and vulnerability details per image.
1 parent 0417453 commit be3ff25

1 file changed

Lines changed: 135 additions & 59 deletions

File tree

.github/workflows/build-containers.yml

Lines changed: 135 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)