diff --git a/scan.py b/scan.py index 5847259..7b4b6bc 100755 --- a/scan.py +++ b/scan.py @@ -1051,7 +1051,7 @@ def _chunk_list(items: list[object], size: int) -> list[list[object]]: def build_other_containers_bodies( - rows: list[tuple[str, str, int, int, int]], + rows: list, ) -> list[str]: """Split the summary by container groups so each message stays small.""" if not rows: @@ -1059,14 +1059,34 @@ def build_other_containers_bodies( bodies: list[str] = [] for chunk in _chunk_list(rows, 12): lines = ["Other containers with findings:", ""] - for image, container, total, fixable, unfixable in chunk: + for row in chunk: + if len(row) == 7: + image, container, total_all, fixable_all, unfixable_all, counts, fixable_counts = row + else: + image, container, total_all, fixable_all, unfixable_all = row + counts = {s: 0 for s in REPORTED_SEVERITIES} + fixable_counts = {s: 0 for s in REPORTED_SEVERITIES} label = f"{image} [{container}]" if container else image - lines.append(f"* {label}: {fixable}/{total} (fixable/total)") - lines += ["", f"Total: {sum(row[2] for row in chunk)} CVE(s) in {len(chunk)} container(s)"] + lines.append(label) + lines.append( + " ".join( + f"{name[0]} {fixable_counts.get(name, 0)}/{counts.get(name, 0)}" + for name in REPORTED_SEVERITIES + ) + ) + lines.append(f"Total: {fixable_all}/{total_all}") + lines.append("") + # Remove trailing blank line + if lines and lines[-1] == "": + lines.pop() + total_cves = sum(row[2] for row in chunk) + fixable_cves = sum(row[3] for row in chunk) + lines += ["", f"Total: {fixable_cves}/{total_cves} in {len(chunk)} container(s)"] bodies.append("\n".join(lines)) if len(bodies) > 1: - overall = f"Total: {sum(row[2] for row in rows)} CVE(s) in {len(rows)} container(s)" - bodies[-1] = bodies[-1] + "\n\n" + overall + overall_fixable = sum(row[3] for row in rows) + overall_total = sum(row[2] for row in rows) + bodies[-1] = bodies[-1] + f"\n\nOverall: {overall_fixable}/{overall_total} in {len(rows)} container(s)" return bodies @@ -1407,7 +1427,7 @@ def send_notifications( ) # One summary notification for every other container. - rows: list[tuple[str, str, int, int, int]] = [] + rows: list = [] for target in targets: result = aggregated.results.get(target.image) if result is None: @@ -1416,10 +1436,15 @@ def send_notifications( if not entries: continue total, fixable, unfixable = split_counts(entries) + counts = severity_counts(entries) + fixable_counts = {severity: 0 for severity in REPORTED_SEVERITIES} + for entry in entries: + if entry.fixable: + fixable_counts[entry.severity] += 1 for container in target.containers or [""]: if container and container in important: continue - rows.append((target.image, container, total, fixable, unfixable)) + rows.append((target.image, container, total, fixable, unfixable, counts, fixable_counts)) if rows: bodies = build_other_containers_bodies(rows) diff --git a/tests/test_scan.py b/tests/test_scan.py index 96ec5c7..612d8c9 100644 --- a/tests/test_scan.py +++ b/tests/test_scan.py @@ -498,15 +498,19 @@ class ReportTests(unittest.TestCase): def test_build_other_containers_body_has_counts_per_container(self) -> None: bodies = scan.build_other_containers_bodies( [ - ("nginx:1.27", "web-worker", 3, 2, 1), - ("redis:7", "cache", 4, 0, 4), + ("nginx:1.27", "web-worker", 3, 2, 1, {"CRITICAL":1,"HIGH":2,"MEDIUM":0,"LOW":0}, {"CRITICAL":1,"HIGH":1,"MEDIUM":0,"LOW":0}), + ("redis:7", "cache", 4, 0, 4, {"CRITICAL":0,"HIGH":4,"MEDIUM":0,"LOW":0}, {"CRITICAL":0,"HIGH":0,"MEDIUM":0,"LOW":0}), ] ) body = bodies[0] self.assertIn("Other containers with findings", body) - self.assertIn("nginx:1.27 [web-worker]: 2/3 (fixable/total)", body) - self.assertIn("redis:7 [cache]: 0/4 (fixable/total)", body) - self.assertIn("Total: 7 CVE(s) in 2 container(s)", body) + self.assertIn("nginx:1.27 [web-worker]", body) + self.assertIn("C 1/1 H 1/2 M 0/0 L 0/0", body) + self.assertIn("Total: 2/3", body) + self.assertIn("redis:7 [cache]", body) + self.assertIn("C 0/0 H 0/4 M 0/0 L 0/0", body) + self.assertIn("Total: 0/4", body) + self.assertIn("Total: 2/7 in 2 container(s)", body) def test_build_missing_body_separates_missing_and_unscanned(self) -> None: body = scan.build_missing_body(["gone"], ["web"]) @@ -670,7 +674,9 @@ printf '%s\\n' "$*" >> "$APPRISE_LOG" self.assertIn("Container: web", notification) self.assertIn("CVE-2026-0001", notification) self.assertIn("Vulnerability scan: other containers", notification) - self.assertIn("redis:7 [cache]: 1/1 (fixable/total)", notification) + self.assertIn("redis:7 [cache]", notification) + self.assertIn("C 0/0 H 0/1 M 0/0 L 0/0", notification) + self.assertIn("Total: 0/1", notification) self.assertIn("1/1 (fixable/total)", notification) # The important container must not also appear in the summary. self.assertNotIn("testphp-php [web]", notification)