padding
This commit is contained in:
1 parent
77d064627a
commit
0fe06f2ba5
3 files changed
+68
-22
No files matched your search
@@ -261,7 +261,7 @@ When the list is set, notifications are split into four groups:
|
|||||||
1. **One notification per important container**, titled
|
1. **One notification per important container**, titled
|
||||||
`Vulnerability scan: <container> (<image>)`. It lists every CVE at or above
|
`Vulnerability scan: <container> (<image>)`. It lists every CVE at or above
|
||||||
`NOTIFY_MIN_SEVERITY`, with the image/container and severity counts shown in
|
`NOTIFY_MIN_SEVERITY`, with the image/container and severity counts shown in
|
||||||
a fenced monospaced table (`Image`, `Container`, `C`, `H`, `M`, `L`).
|
a padded fenced monospaced table (`Image`, `Container`, `C`, `H`, `M`, `L`).
|
||||||
A notification is still sent if there are none, showing totals at 0/0.
|
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
|
2. **One notification for important containers that are missing** – either not
|
||||||
running, or running an image that was not part of the scan.
|
running, or running an image that was not part of the scan.
|
||||||
|
|||||||
@@ -999,18 +999,31 @@ def summary_table(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Render image/container/severity counts as a monospaced markdown table."""
|
"""Render image/container/severity counts as a monospaced markdown table."""
|
||||||
headers = ["Image", "Container"] + [name[0] for name in severities]
|
headers = ["Image", "Container"] + [name[0] for name in severities]
|
||||||
lines = [
|
table_rows: list[list[str]] = []
|
||||||
"```",
|
|
||||||
"| " + " | ".join(headers) + " |",
|
|
||||||
"| " + " | ".join("---" for _ in headers) + " |",
|
|
||||||
]
|
|
||||||
for image, container, counts, fixable_counts in rows:
|
for image, container, counts, fixable_counts in rows:
|
||||||
cells = [short_image(image), container or "-"]
|
cells = [short_image(image), container or "-"]
|
||||||
cells += [
|
cells += [
|
||||||
f"{fixable_counts.get(name, 0)}/{counts.get(name, 0)}"
|
f"{fixable_counts.get(name, 0)}/{counts.get(name, 0)}"
|
||||||
for name in severities
|
for name in severities
|
||||||
]
|
]
|
||||||
lines.append("| " + " | ".join(cells) + " |")
|
table_rows.append(cells)
|
||||||
|
|
||||||
|
widths = [max(3, len(header)) for header in headers]
|
||||||
|
for cells in table_rows:
|
||||||
|
for index, cell in enumerate(cells):
|
||||||
|
widths[index] = max(widths[index], len(cell))
|
||||||
|
|
||||||
|
def render(cells: list[str]) -> str:
|
||||||
|
return "| " + " | ".join(
|
||||||
|
cell.ljust(widths[index]) for index, cell in enumerate(cells)
|
||||||
|
) + " |"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"```",
|
||||||
|
render(headers),
|
||||||
|
"| " + " | ".join("-" * width for width in widths) + " |",
|
||||||
|
]
|
||||||
|
lines += [render(cells) for cells in table_rows]
|
||||||
lines.append("```")
|
lines.append("```")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|||||||
+48
-15
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import socketserver
|
import socketserver
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -85,6 +86,11 @@ def container(
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def table_row_pattern(cells: list[str]) -> str:
|
||||||
|
"""Regex matching a table row regardless of column padding."""
|
||||||
|
return r"\|\s*" + r"\s*\|\s*".join(re.escape(cell) for cell in cells) + r"\s*\|"
|
||||||
|
|
||||||
|
|
||||||
class DockerDiscoveryTests(unittest.TestCase):
|
class DockerDiscoveryTests(unittest.TestCase):
|
||||||
def test_images_in_use_returns_running_images(self) -> None:
|
def test_images_in_use_returns_running_images(self) -> None:
|
||||||
with FakeDocker([container("a" * 64, "testphp-php"), container("b" * 64, "redis:7")]) as docker:
|
with FakeDocker([container("a" * 64, "testphp-php"), container("b" * 64, "redis:7")]) as docker:
|
||||||
@@ -489,9 +495,9 @@ class ReportTests(unittest.TestCase):
|
|||||||
body = scan.build_important_body(
|
body = scan.build_important_body(
|
||||||
"web", target, result, "/reports/x.json", "HIGH"
|
"web", target, result, "/reports/x.json", "HIGH"
|
||||||
)
|
)
|
||||||
self.assertIn("| Image | Container | C | H |", body)
|
self.assertRegex(body, table_row_pattern(["Image", "Container", "C", "H"]))
|
||||||
self.assertIn("| nginx:1.27 | web | 1/1 | 1/2 |", body)
|
self.assertRegex(body, table_row_pattern(["nginx:1.27", "web", "1/1", "1/2"]))
|
||||||
self.assertNotIn("| M |", body)
|
self.assertNotRegex(body, r"\|\s*M\s*\|")
|
||||||
self.assertIn("CVE-1", body)
|
self.assertIn("CVE-1", body)
|
||||||
self.assertIn("CVE-2", body)
|
self.assertIn("CVE-2", body)
|
||||||
self.assertIn("CVE-5", body)
|
self.assertIn("CVE-5", body)
|
||||||
@@ -507,9 +513,9 @@ class ReportTests(unittest.TestCase):
|
|||||||
self.assertIn("CVE-1", body)
|
self.assertIn("CVE-1", body)
|
||||||
self.assertNotIn("CVE-2", body)
|
self.assertNotIn("CVE-2", body)
|
||||||
self.assertNotIn("CVE-5", body)
|
self.assertNotIn("CVE-5", body)
|
||||||
self.assertIn("| Image | Container | C |", body)
|
self.assertRegex(body, table_row_pattern(["Image", "Container", "C"]))
|
||||||
self.assertIn("| nginx:1.27 | web | 1/1 |", body)
|
self.assertRegex(body, table_row_pattern(["nginx:1.27", "web", "1/1"]))
|
||||||
self.assertNotIn("| H |", body)
|
self.assertNotRegex(body, r"\|\s*H\s*\|")
|
||||||
self.assertNotIn("Total:", body)
|
self.assertNotIn("Total:", body)
|
||||||
|
|
||||||
def test_build_other_containers_body_has_counts_per_container(self) -> None:
|
def test_build_other_containers_body_has_counts_per_container(self) -> None:
|
||||||
@@ -522,10 +528,12 @@ class ReportTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
body = bodies[0]
|
body = bodies[0]
|
||||||
self.assertIn("Other containers with findings", body)
|
self.assertIn("Other containers with findings", body)
|
||||||
self.assertIn("| Image | Container | C | H |", body)
|
self.assertRegex(body, table_row_pattern(["Image", "Container", "C", "H"]))
|
||||||
self.assertIn("| nginx:1.27 | web-worker | 1/1 | 1/2 |", body)
|
self.assertRegex(
|
||||||
self.assertIn("| redis:7 | cache | 0/0 | 0/4 |", body)
|
body, table_row_pattern(["nginx:1.27", "web-worker", "1/1", "1/2"])
|
||||||
self.assertNotIn("| M |", body)
|
)
|
||||||
|
self.assertRegex(body, table_row_pattern(["redis:7", "cache", "0/0", "0/4"]))
|
||||||
|
self.assertNotRegex(body, r"\|\s*M\s*\|")
|
||||||
self.assertNotIn("Total:", body)
|
self.assertNotIn("Total:", body)
|
||||||
self.assertNotIn("Overall:", body)
|
self.assertNotIn("Overall:", body)
|
||||||
|
|
||||||
@@ -544,8 +552,8 @@ class ReportTests(unittest.TestCase):
|
|||||||
[("web", target, result)], all_rows, "HIGH"
|
[("web", target, result)], all_rows, "HIGH"
|
||||||
)
|
)
|
||||||
self.assertIn("Important containers:", body)
|
self.assertIn("Important containers:", body)
|
||||||
self.assertIn("| Image | Container | C | H |", body)
|
self.assertRegex(body, table_row_pattern(["Image", "Container", "C", "H"]))
|
||||||
self.assertIn("| nginx:1.27 | web | 1/1 | 1/2 |", body)
|
self.assertRegex(body, table_row_pattern(["nginx:1.27", "web", "1/1", "1/2"]))
|
||||||
self.assertIn("Packages:", body)
|
self.assertIn("Packages:", body)
|
||||||
self.assertIn("libc 1.0 -> 1.1", body)
|
self.assertIn("libc 1.0 -> 1.1", body)
|
||||||
self.assertIn("openssl 3.0.1 -> 3.0.2", body)
|
self.assertIn("openssl 3.0.1 -> 3.0.2", body)
|
||||||
@@ -554,6 +562,31 @@ class ReportTests(unittest.TestCase):
|
|||||||
self.assertIn("Overall: 2/3 fixable/total in 1 container(s)", body)
|
self.assertIn("Overall: 2/3 fixable/total in 1 container(s)", body)
|
||||||
self.assertNotIn("Total:", body)
|
self.assertNotIn("Total:", body)
|
||||||
|
|
||||||
|
def test_summary_table_pads_every_column(self) -> None:
|
||||||
|
table = scan.summary_table(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"nginx:1.27",
|
||||||
|
"web",
|
||||||
|
{"CRITICAL": 1, "HIGH": 2, "MEDIUM": 0, "LOW": 0},
|
||||||
|
{"CRITICAL": 1, "HIGH": 1, "MEDIUM": 0, "LOW": 0},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
["CRITICAL", "HIGH"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
table,
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"```",
|
||||||
|
"| Image | Container | C | H |",
|
||||||
|
"| ---------- | --------- | --- | --- |",
|
||||||
|
"| nginx:1.27 | web | 1/1 | 1/2 |",
|
||||||
|
"```",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_build_missing_body_separates_missing_and_unscanned(self) -> None:
|
def test_build_missing_body_separates_missing_and_unscanned(self) -> None:
|
||||||
body = scan.build_missing_body(["gone"], ["web"])
|
body = scan.build_missing_body(["gone"], ["web"])
|
||||||
self.assertIn("not running", body)
|
self.assertIn("not running", body)
|
||||||
@@ -690,7 +723,7 @@ printf '%s\\n' "$*" >> "$APPRISE_LOG"
|
|||||||
self.assertNotIn("Total:", notification)
|
self.assertNotIn("Total:", notification)
|
||||||
self.assertIn("No CVEs at or above", notification)
|
self.assertIn("No CVEs at or above", notification)
|
||||||
self.assertIn("Vulnerability scan: summary", notification)
|
self.assertIn("Vulnerability scan: summary", notification)
|
||||||
self.assertIn("| alpine:3.20 | web | 0/0 | 0/0 |", notification)
|
self.assertRegex(notification, table_row_pattern(["alpine:3.20", "web", "0/0", "0/0"]))
|
||||||
self.assertIn("Overall: 0/0 fixable/total in 1 container(s)", notification)
|
self.assertIn("Overall: 0/0 fixable/total in 1 container(s)", notification)
|
||||||
|
|
||||||
def test_notification_includes_container(self) -> None:
|
def test_notification_includes_container(self) -> None:
|
||||||
@@ -716,10 +749,10 @@ printf '%s\\n' "$*" >> "$APPRISE_LOG"
|
|||||||
self.assertEqual(code, 1)
|
self.assertEqual(code, 1)
|
||||||
notification = Path(self.apprise_log).read_text()
|
notification = Path(self.apprise_log).read_text()
|
||||||
self.assertIn("Vulnerability scan: web (testphp-php)", notification)
|
self.assertIn("Vulnerability scan: web (testphp-php)", notification)
|
||||||
self.assertIn("| testphp-php | web | 1/1 | 0/0 |", notification)
|
self.assertRegex(notification, table_row_pattern(["testphp-php", "web", "1/1", "0/0"]))
|
||||||
self.assertIn("CVE-2026-0001", notification)
|
self.assertIn("CVE-2026-0001", notification)
|
||||||
self.assertIn("Vulnerability scan: other containers", notification)
|
self.assertIn("Vulnerability scan: other containers", notification)
|
||||||
self.assertIn("| redis:7 | cache | 1/1 | 0/0 |", notification)
|
self.assertRegex(notification, table_row_pattern(["redis:7", "cache", "1/1", "0/0"]))
|
||||||
self.assertNotIn("Total:", notification)
|
self.assertNotIn("Total:", notification)
|
||||||
self.assertIn("Vulnerability scan: summary", notification)
|
self.assertIn("Vulnerability scan: summary", notification)
|
||||||
self.assertIn("Overall: 2/2 fixable/total in 2 container(s)", notification)
|
self.assertIn("Overall: 2/2 fixable/total in 2 container(s)", notification)
|
||||||
|
|||||||
Reference in new issue
Block a user