Skip to content

Commit a840c69

Browse files
committed
refactor(ci): use argus reporter and comment-pr action for container scans
Replaced custom Python report generator and custom shell PR commenter with the argus SDK and existing comment-pr composite action: - Scan step uses argus ContainerScanner.parse_trivy_results() to parse Trivy JSON into Finding objects, then MarkdownReporter to generate the summary markdown (severity table, collapsible per-scanner details) - Comment step uses .github/actions/comment-pr (deduplication via marker, branch/commit metadata, truncation handling) - Trivy scans ALL severities (removed CRITICAL,HIGH filter) — perception is protection - No more ad-hoc Python in the workflow for report formatting
1 parent 0ead1b3 commit a840c69

1 file changed

Lines changed: 77 additions & 163 deletions

File tree

.github/workflows/build-containers.yml

Lines changed: 77 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,26 @@ jobs:
113113
env:
114114
IMAGE_NAME: ${{ matrix.image }}
115115

116-
- name: Scan with Trivy
116+
- name: Checkout (for argus SDK)
117+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
118+
119+
- name: Set up Python
120+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
121+
with:
122+
python-version: '3.13'
123+
124+
- name: Install Argus SDK
125+
run: |
126+
pip install --quiet pyyaml
127+
echo "PYTHONPATH=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
128+
129+
# Scan with ALL severities — perception is protection
130+
- name: Scan with Trivy (SARIF)
117131
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
118132
with:
119133
image-ref: "ghcr.io/huntridge-labs/argus/${{ matrix.image }}:${{ github.sha }}"
120134
format: 'sarif'
121135
output: 'trivy-results.sarif'
122-
severity: 'CRITICAL,HIGH'
123136

124137
- name: Upload Trivy SARIF
125138
if: always()
@@ -136,7 +149,6 @@ jobs:
136149
fail-build: false
137150
severity-cutoff: critical
138151

139-
# Run Trivy again with JSON for severity breakdown
140152
- name: Scan with Trivy (JSON)
141153
if: always()
142154
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
@@ -145,59 +157,51 @@ jobs:
145157
format: 'json'
146158
output: 'trivy-results.json'
147159

148-
- name: Generate scan report
160+
# Use argus container scanner parser + markdown reporter
161+
- name: Generate report with Argus
149162
if: always()
150163
run: |
151-
mkdir -p scan-reports
164+
mkdir -p scanner-summaries
152165
python3 << 'PYEOF'
153-
import json, os
166+
import sys, os
167+
sys.path.insert(0, os.environ.get("PYTHONPATH", "."))
168+
169+
from pathlib import Path
170+
from argus.scanners.container import ContainerScanner
171+
from argus.core.models import ScanResult, ScanSummary, Severity
172+
from argus.reporters.markdown import MarkdownReporter
154173
155174
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
159-
try:
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)
175+
trivy_json = Path("trivy-results.json")
176+
177+
scanner = ContainerScanner()
178+
findings = scanner.parse_trivy_results(trivy_json) if trivy_json.exists() else []
179+
180+
result = ScanResult(
181+
scanner=f"container/{image_name}",
182+
findings=findings,
183+
metadata={"image": image_name},
184+
)
185+
summary = ScanSummary(results=[result])
186+
187+
reporter = MarkdownReporter()
188+
reporter.report(summary, "scanner-summaries")
189+
190+
# Rename to per-image filename for aggregation
191+
src = Path("scanner-summaries/argus-summary.md")
192+
dst = Path(f"scanner-summaries/{image_name}.md")
193+
if src.exists():
194+
dst.write_text(src.read_text())
191195
PYEOF
192196
env:
193197
IMAGE_NAME: ${{ matrix.image }}
194198

195-
- name: Upload scan report
199+
- name: Upload scan artifacts
196200
if: always()
197201
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
198202
with:
199-
name: scan-report-${{ matrix.image }}
200-
path: scan-reports/
203+
name: scanner-summary-container-${{ matrix.image }}
204+
path: scanner-summaries/
201205
retention-days: 7
202206

203207
# ── Step 3: Test argus CLI using built images ───────────────────────
@@ -310,20 +314,24 @@ jobs:
310314
path: cli-test-results/
311315
retention-days: 7
312316

313-
# ── Step 4: Post PR comment with results ───────────────────────────
317+
# ── Step 4: Post PR comment using argus markdown + comment-pr action
314318
comment-pr:
315319
name: Container Scan Summary
316320
if: always() && github.event_name == 'pull_request'
317321
needs: [scan, test-cli]
318322
runs-on: ubuntu-latest
319323

320324
steps:
321-
- name: Download scan reports
325+
- name: Checkout (for comment-pr action)
326+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
327+
328+
# Download the argus-generated markdown from each scan matrix job
329+
- name: Download scanner summaries
322330
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
323331
with:
324-
pattern: scan-report-*
332+
pattern: scanner-summary-container-*
325333
merge-multiple: true
326-
path: scan-reports
334+
path: scanner-summaries
327335

328336
- name: Download CLI test results
329337
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
@@ -332,120 +340,26 @@ jobs:
332340
path: cli-results
333341
continue-on-error: true
334342

335-
- name: Generate PR comment
343+
# Combine per-image argus markdown reports into one file
344+
- name: Combine scanner summaries
336345
run: |
337-
python3 << 'PYEOF'
338-
import json, glob, os
339-
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"
403-
if os.path.exists(cli_json):
404-
data = json.load(open(cli_json))
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
346+
mkdir -p scanner-summaries
347+
{
348+
for md in scanner-summaries/*.md; do
349+
[ -f "$md" ] && cat "$md" && echo ""
350+
done
351+
if [ -f "cli-results/argus-summary.md" ]; then
352+
echo "---"
353+
echo ""
354+
cat cli-results/argus-summary.md
355+
fi
356+
} > scanner-summaries/combined-container-scan.md
428357
429-
- name: Post PR comment
430-
env:
431-
GH_TOKEN: ${{ github.token }}
432-
PR_NUMBER: ${{ github.event.pull_request.number }}
433-
REPO: ${{ github.repository }}
434-
run: |
435-
BODY=$(cat comment-body.md)
436-
MARKER="<!-- argus-container-scan -->"
437-
438-
# Find existing comment by marker
439-
COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
440-
--jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" 2>/dev/null | head -1)
441-
442-
FULL_BODY="${MARKER}
443-
${BODY}"
444-
445-
if [ -n "$COMMENT_ID" ]; then
446-
gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" \
447-
-X PATCH -f body="$FULL_BODY"
448-
else
449-
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
450-
-f body="$FULL_BODY"
451-
fi
358+
# Post using the existing comment-pr composite action
359+
- name: Comment PR with scan results
360+
uses: ./.github/actions/comment-pr
361+
with:
362+
summary_file: scanner-summaries/combined-container-scan.md
363+
comment_marker: argus-container-scan
364+
title: '🔒 Argus Container Security Scan'
365+
fallback_message: 'No container scan results available'

0 commit comments

Comments
 (0)