diff --git a/scan.py b/scan.py index 5774e64..a068273 100755 --- a/scan.py +++ b/scan.py @@ -1008,14 +1008,40 @@ def build_other_containers_body( Each row is ``(image, container, total, fixable, unfixable)``. """ - lines = ["Other containers:"] + lines = ["Other containers with findings:", ""] for image, container, total, fixable, unfixable in rows: - label = container or image - lines.append(f"- {label}: {fixable}/{total}") - lines.append(f"Total: {sum(row[2] for row in rows)} in {len(rows)}") + 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 rows)} CVE(s) in {len(rows)} container(s)"] return "\n".join(lines) +def _chunk_list(items: list[object], size: int) -> list[list[object]]: + if size <= 0: + return [items] + return [items[i : i + size] for i in range(0, len(items), size)] + + +def build_other_containers_bodies( + rows: list[tuple[str, str, int, int, int]], +) -> list[str]: + """Split the summary by container groups so each message stays small.""" + if not rows: + return [] + bodies: list[str] = [] + for chunk in _chunk_list(rows, 12): + lines = ["Other containers with findings:", ""] + for image, container, total, fixable, unfixable in chunk: + 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)"] + 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 + return bodies + + def build_missing_body( missing: list[str], not_scanned: list[str] | None = None ) -> str: @@ -1365,12 +1391,23 @@ def send_notifications( rows.append((target.image, container, total, fixable, unfixable)) if rows: - send_notification_parts( - settings, - stats, - f"{settings.apprise_title_prefix}: other containers", - build_other_containers_body(rows), - ) + bodies = build_other_containers_bodies(rows) + for index, body in enumerate(bodies, 1): + if len(bodies) == 1: + title = f"{settings.apprise_title_prefix}: other containers" + else: + title = f"{settings.apprise_title_prefix}: other containers [{index}/{len(bodies)}]" + if len(title) > 200: + original = f"{settings.apprise_title_prefix}: other containers" + suffix = f" [{index}/{len(bodies)}]" + title = original[: 200 - len(suffix) - 3] + "..." + suffix + body_truncated = _truncate_text(body, notification_limit(settings)) + if notify(settings, title, body_truncated): + continue + body_truncated = _truncate_text(body, min(notification_limit(settings), 1100)) + if notify(settings, title, body_truncated): + continue + stats.notify_errors += 1 def resolve_container_images( diff --git a/tests/test_scan.py b/tests/test_scan.py index 05ca8cb..d52bc5a 100644 --- a/tests/test_scan.py +++ b/tests/test_scan.py @@ -496,15 +496,17 @@ class ReportTests(unittest.TestCase): self.assertIn("Total: 1/1 (fixable/total)", body) def test_build_other_containers_body_has_counts_per_container(self) -> None: - body = scan.build_other_containers_body( + bodies = scan.build_other_containers_bodies( [ ("nginx:1.27", "web-worker", 3, 2, 1), ("redis:7", "cache", 4, 0, 4), ] ) - self.assertIn("- web-worker: 2/3", body) - self.assertIn("- cache: 0/4", body) - self.assertIn("Total: 7 in 2", body) + 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) def test_build_missing_body_separates_missing_and_unscanned(self) -> None: body = scan.build_missing_body(["gone"], ["web"]) @@ -668,7 +670,7 @@ 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("cache: 1/1", notification) + self.assertIn("redis:7 [cache]: 1/1 (fixable/total)", notification) self.assertIn("1/1 (fixable/total)", notification) # The important container must not also appear in the summary. self.assertNotIn("testphp-php [web]", notification)