more changes!

This commit is contained in:
David Duran Rosich committed 2026-10-25 00:06:13 +02:00
1 parent beb2f544d9
commit 66808d77f0
3 files changed
+64 -30

No files matched your search

+9 -6
View File
@@ -27,9 +27,10 @@ host only needs Docker. JSON reports are kept on disk for auditing.
(useful for CI).
3. Notifications are sent. When `IMPORTANT_CONTAINERS` is set, each named
container gets its own complete CVE list (showing `C X/Y ...` where X is
fixable and Y is total per severity, plus `Total: fixable/total`), missing
important containers are reported, and every other container is summarised as
`fixable/total`. Without `IMPORTANT_CONTAINERS`, a single aggregated
fixable and Y is total per severity, plus `Total: fixable/total`), even if
there are no CVEs at or above `NOTIFY_MIN_SEVERITY`. Missing important
containers are reported, and every other container is summarised in chunks
as `fixable/total`. Without `IMPORTANT_CONTAINERS`, a single aggregated
notification lists each CVE with the affected packages, containers, and
images.
@@ -258,11 +259,13 @@ When the list is set, notifications are split into three groups:
1. **One notification per important container**, titled
`Vulnerability scan: <container> (<image>)`. It lists every CVE at or above
`NOTIFY_MIN_SEVERITY`, with a total split into fixable and unfixable.
`NOTIFY_MIN_SEVERITY` with `C X/Y H X/Y M X/Y L X/Y` and `Total: fixable/total`.
A notification is still sent if there are none, showing totals at 0/0.
2. **One notification for important containers that are missing** – either not
running, or running an image that was not part of the scan.
3. **One summary notification for every other container**, listing the image,
container, total CVE count, and the fixable/unfixable split.
3. **One summary notification for every other container**, split every ~12
containers (with per-part and overall totals), listing image/container and
`fixable/total`.
A container can appear in only the important list or the summary, never both.
If an important container shares its image with other containers, the others
+41 -10
View File
@@ -973,21 +973,22 @@ def build_important_body(
) -> str:
"""Full CVE list for one important container, with fixable/unfixable counts."""
entries = cve_entries(result, severity_rank(min_severity))
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
total_fixable = sum(1 for entry in entries if entry.fixable)
total_all = len(entries)
lines = [
f"Image: {target.image}",
f"Container: {container}",
" | ".join(
" ".join(
f"{name[0]} {fixable_counts[name]}/{counts[name]}"
for name in REPORTED_SEVERITIES
),
f"Total: {fixable}/{total} (fixable/total)",
f"Total: {total_fixable}/{total_all}",
"",
"CVEs:",
]
@@ -1001,6 +1002,33 @@ def build_important_body(
return "\n".join(lines)
def build_important_summary_body(
container: str, target: ScanTarget, result: ScanResult, min_severity: str
) -> str:
"""Summary for an important container with no findings above threshold."""
entries = cve_entries(result, severity_rank(min_severity))
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
total_fixable = sum(1 for entry in entries if entry.fixable)
total_all = len(entries)
lines = [
f"Image: {target.image}",
f"Container: {container}",
" ".join(
f"{name[0]} {fixable_counts[name]}/{counts[name]}"
for name in REPORTED_SEVERITIES
),
f"Total: {total_fixable}/{total_all}",
"",
"No CVEs at or above the notification threshold.",
]
return "\n".join(lines)
def build_other_containers_body(
rows: list[tuple[str, str, int, int, int]],
) -> str:
@@ -1348,22 +1376,25 @@ def send_notifications(
continue # scan failure; already reported by scan_image
entries = cve_entries(result, settings.notify_rank)
if not entries:
report_path = os.path.join(settings.report_dir, f"{report_slug(image)}.json")
if entries:
body = build_important_body(
name, target, result, report_path, settings.notify_min_severity
)
else:
log(
"DEBUG",
f"Important container {name} ({image}) has no findings at or above "
f"{settings.notify_min_severity}",
)
continue
report_path = os.path.join(settings.report_dir, f"{report_slug(image)}.json")
body = build_important_body(
name, target, result, report_path, settings.notify_min_severity
)
body = build_important_summary_body(
name, target, result, settings.notify_min_severity
)
title = f"{settings.apprise_title_prefix}: {name} ({image})"
log(
"DEBUG",
f"Preparing important-container notification for {name}: "
f"{len(entries)} CVE(s)",
f"{len(entries)} CVE(s) at or above threshold",
)
send_notification_parts(settings, stats, title, body)
+14 -14
View File
@@ -475,8 +475,8 @@ class ReportTests(unittest.TestCase):
)
self.assertIn("Image: nginx:1.27", body)
self.assertIn("Container: web", body)
self.assertIn("C 1/1 | H 1/2 | M 0/0 | L 0/0", body)
self.assertIn("Total: 2/3 (fixable/total)", body)
self.assertIn("C 1/1 H 1/2 M 0/0 L 0/0", body)
self.assertIn("Total: 2/3", body)
self.assertIn("CVE-1", body)
self.assertIn("CVE-2", body)
self.assertIn("CVE-5", body)
@@ -492,8 +492,8 @@ class ReportTests(unittest.TestCase):
self.assertIn("CVE-1", body)
self.assertNotIn("CVE-2", body)
self.assertNotIn("CVE-5", body)
self.assertIn("C 1/1 | H 0/0 | M 0/0 | L 0/0", body)
self.assertIn("Total: 1/1 (fixable/total)", body)
self.assertIn("C 1/1 H 0/0 M 0/0 L 0/0", body)
self.assertIn("Total: 1/1", body)
def test_build_other_containers_body_has_counts_per_container(self) -> None:
bodies = scan.build_other_containers_bodies(
@@ -631,18 +631,18 @@ printf '%s\\n' "$*" >> "$APPRISE_LOG"
self.assertTrue(os.path.exists(os.path.join(self.env["REPORT_DIR"], "testphp-php.json")))
self.assertIn("testphp-php", Path(self.apprise_log).read_text())
def test_scan_logs_containers_using_the_image(self) -> None:
containers = [container("a" * 64, "testphp-php", name="testphp")]
def test_important_container_always_notified_when_clean(self) -> None:
containers = [container("a" * 64, "alpine:3.20", name="web")]
with FakeDocker(containers) as docker:
stderr = io.StringIO()
with mock.patch.object(scan, "MIN_LOG_LEVEL", "INFO"):
with redirect_stderr(stderr):
code = self.run_main([], docker.socket_path)
code = self.run_main(
[], docker.socket_path, IMPORTANT_CONTAINERS="web", FAKE_TRIVY_SEVERITY="LOW"
)
self.assertEqual(code, 1)
self.assertIn(
"Scanning testphp-php (used by container testphp)", stderr.getvalue()
)
self.assertEqual(code, 0)
notification = Path(self.apprise_log).read_text()
self.assertIn("Vulnerability scan: web (alpine:3.20)", notification)
self.assertIn("Total: 0/0", notification)
self.assertIn("No CVEs at or above", notification)
def test_notification_includes_container(self) -> None:
containers = [container("a" * 64, "testphp-php", name="testphp")]