more changes!
This commit is contained in:
1 parent
beb2f544d9
commit
66808d77f0
3 files changed
+63
-29
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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,19 +631,19 @@ 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)
|
||||
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn(
|
||||
"Scanning testphp-php (used by container testphp)", stderr.getvalue()
|
||||
code = self.run_main(
|
||||
[], docker.socket_path, IMPORTANT_CONTAINERS="web", FAKE_TRIVY_SEVERITY="LOW"
|
||||
)
|
||||
|
||||
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")]
|
||||
with FakeDocker(containers) as docker:
|
||||
|
||||
Reference in new issue
Block a user