Skip to content

Commit 1c170da

Browse files
committed
fix(ci): fix container scan PR comment showing identical collapsed titles
Each matrix job now writes just its per-container detail section via report_single() (named <details><summary>📦 scanner-bandit — N vulns), plus a JSON summary with severity counts. The combine step reads all JSON summaries to build: 1. Combined findings summary table (aggregated counts) 2. Container breakdown table (one row per image with severity columns) 3. Per-container detail sections (each named, distinguishable collapsed) Replaces the old approach where each matrix job produced a full report wrapped in an identical 🐳 Container Security Scan header.
1 parent 3ee0123 commit 1c170da

2 files changed

Lines changed: 146 additions & 20 deletions

File tree

.github/workflows/build-containers.yml

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,24 @@ jobs:
185185
trivy_findings=trivy_findings,
186186
combined_findings=trivy_findings,
187187
)
188-
summary = ContainerScanSummary(results=[result])
189188
189+
# Write just this container's detail section (not the full report)
190190
reporter = ContainerMarkdownReporter()
191-
reporter.report(summary, "scanner-summaries")
191+
reporter.report_single(result, "scanner-summaries")
192192
193-
# Rename for per-image aggregation
194-
src = Path("scanner-summaries/container-scan.md")
195-
dst = Path(f"scanner-summaries/{image_name}.md")
196-
if src.exists():
197-
dst.write_text(src.read_text())
193+
# Also write a JSON summary for the combine step
194+
import json
195+
json.dump({
196+
"name": image_name,
197+
"image_ref": image_ref,
198+
"critical": result.critical_count,
199+
"high": result.high_count,
200+
"medium": result.medium_count,
201+
"low": result.low_count,
202+
"total": result.total_count,
203+
"unique": result.unique_count,
204+
"build_success": result.build_success,
205+
}, open(f"scanner-summaries/{image_name}.json", "w"))
198206
PYEOF
199207
env:
200208
IMAGE_NAME: ${{ matrix.image }}
@@ -344,20 +352,70 @@ jobs:
344352
path: cli-results
345353
continue-on-error: true
346354

347-
# Combine per-image argus markdown reports into one file
355+
- name: Set up Python
356+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
357+
with:
358+
python-version: '3.13'
359+
360+
- name: Install Argus SDK
361+
run: |
362+
pip install --quiet pyyaml
363+
echo "PYTHONPATH=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
364+
365+
# Build combined report with summary table + per-container sections
348366
- name: Combine scanner summaries
349367
run: |
350-
mkdir -p scanner-summaries
351-
{
352-
for md in scanner-summaries/*.md; do
353-
[ -f "$md" ] && cat "$md" && echo ""
354-
done
355-
if [ -f "cli-results/argus-summary.md" ]; then
356-
echo "---"
357-
echo ""
358-
cat cli-results/argus-summary.md
359-
fi
360-
} > scanner-summaries/combined-container-scan.md
368+
python3 << 'PYEOF'
369+
import sys, os, glob, json
370+
sys.path.insert(0, os.environ.get("PYTHONPATH", "."))
371+
372+
from pathlib import Path
373+
from argus.container.scanner import ContainerScanResult, ContainerScanSummary
374+
from argus.reporters.container_markdown import ContainerMarkdownReporter
375+
376+
# Load per-image JSON summaries for accurate severity counts
377+
results = []
378+
for json_file in sorted(Path("scanner-summaries").glob("*.json")):
379+
data = json.load(open(json_file))
380+
# Create a minimal result with counts (findings list empty —
381+
# the per-image markdown sections have the detail)
382+
r = ContainerScanResult(
383+
name=data["name"],
384+
image_ref=data.get("image_ref", data["name"]),
385+
build_success=data.get("build_success", True),
386+
)
387+
# Inject counts via the combined_findings proxy
388+
# (ContainerScanResult computes counts from combined_findings)
389+
from argus.core.models import Finding, Severity
390+
for sev, count in [
391+
(Severity.CRITICAL, data.get("critical", 0)),
392+
(Severity.HIGH, data.get("high", 0)),
393+
(Severity.MEDIUM, data.get("medium", 0)),
394+
(Severity.LOW, data.get("low", 0)),
395+
]:
396+
for _ in range(count):
397+
r.combined_findings.append(
398+
Finding(id="count", severity=sev, title="")
399+
)
400+
results.append(r)
401+
402+
summary = ContainerScanSummary(results=results)
403+
section_files = sorted(Path("scanner-summaries").glob("*.md"))
404+
405+
# Build combined report: summary table + breakdown + per-container sections
406+
content = ContainerMarkdownReporter.build_combined_report(
407+
section_files=section_files,
408+
summary=summary,
409+
)
410+
411+
# Append CLI scan results if available
412+
cli_md = Path("cli-results/argus-summary.md")
413+
if cli_md.exists():
414+
content += "\n---\n\n" + cli_md.read_text()
415+
416+
Path("scanner-summaries/combined-container-scan.md").write_text(content)
417+
print(f"Combined report: {len(section_files)} container sections")
418+
PYEOF
361419
362420
# Post using the existing comment-pr composite action
363421
- name: Comment PR with scan results

argus/reporters/container_markdown.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def report(
6464
return filepath
6565

6666
# ------------------------------------------------------------------
67-
# Top-level builder
67+
# Top-level builder (full multi-container report)
6868
# ------------------------------------------------------------------
6969

7070
def _build(self, summary: Any, artifacts_url: str = "") -> str:
@@ -95,6 +95,74 @@ def _build(self, summary: Any, artifacts_url: str = "") -> str:
9595
lines.append("</details>")
9696
return "\n".join(lines)
9797

98+
# ------------------------------------------------------------------
99+
# Single-container section (for CI matrix jobs)
100+
# ------------------------------------------------------------------
101+
102+
def report_single(
103+
self,
104+
result: Any,
105+
output_dir: Optional[Path] = None,
106+
) -> Path:
107+
"""Write a single container's detail section to a named file.
108+
109+
Produces just the per-container ``<details>`` block (no outer
110+
wrapper, no combined summary). Designed for CI matrix jobs
111+
where each container is scanned in a separate job and the
112+
results are combined later by ``build_combined_report``.
113+
"""
114+
dest = Path(output_dir) if output_dir else _DEFAULT_OUTPUT_DIR
115+
dest.mkdir(parents=True, exist_ok=True)
116+
117+
name = getattr(result, "name", "unknown")
118+
filepath = dest / f"{name}.md"
119+
content = "\n".join(self._build_container_detail(result))
120+
filepath.write_text(content, encoding="utf-8")
121+
return filepath
122+
123+
@classmethod
124+
def build_combined_report(
125+
cls,
126+
section_files: list[Path],
127+
summary: Any,
128+
artifacts_url: str = "",
129+
) -> str:
130+
"""Combine per-container sections into a full report.
131+
132+
Call this from the CI combine step after downloading all
133+
per-container markdown files from matrix job artifacts.
134+
135+
Parameters
136+
----------
137+
section_files:
138+
Paths to per-container markdown files (from ``report_single``).
139+
summary:
140+
A ``ContainerScanSummary`` with all results for the
141+
combined header and breakdown table.
142+
"""
143+
reporter = cls()
144+
lines: list[str] = []
145+
146+
lines.extend(reporter._build_combined_summary(summary))
147+
lines.extend(reporter._build_breakdown_table(summary))
148+
149+
lines.append("### \U0001f50d Detailed Findings by Container")
150+
lines.append("")
151+
152+
for path in sorted(section_files):
153+
if path.exists():
154+
lines.append(path.read_text(encoding="utf-8"))
155+
lines.append("")
156+
157+
if artifacts_url:
158+
lines.append(
159+
f"**\U0001f4c1 Artifacts:** "
160+
f"[Container Scan Reports]({artifacts_url})"
161+
)
162+
lines.append("")
163+
164+
return "\n".join(lines)
165+
98166
# ------------------------------------------------------------------
99167
# Combined findings summary
100168
# ------------------------------------------------------------------

0 commit comments

Comments
 (0)