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
|
||||
`Vulnerability scan: <container> (<image>)`. It lists every CVE at or above
|
||||
`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.
|
||||
2. **One notification for important containers that are missing** – either not
|
||||
running, or running an image that was not part of the scan.
|
||||
|
||||
@@ -999,18 +999,31 @@ def summary_table(
|
||||
) -> str:
|
||||
"""Render image/container/severity counts as a monospaced markdown table."""
|
||||
headers = ["Image", "Container"] + [name[0] for name in severities]
|
||||
lines = [
|
||||
"```",
|
||||
"| " + " | ".join(headers) + " |",
|
||||
"| " + " | ".join("---" for _ in headers) + " |",
|
||||
]
|
||||
table_rows: list[list[str]] = []
|
||||
for image, container, counts, fixable_counts in rows:
|
||||
cells = [short_image(image), container or "-"]
|
||||
cells += [
|
||||
f"{fixable_counts.get(name, 0)}/{counts.get(name, 0)}"
|
||||
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("```")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
+48
-15
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socketserver
|
||||
import subprocess
|
||||
@@ -85,6 +86,11 @@ def container(
|
||||
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):
|
||||
def test_images_in_use_returns_running_images(self) -> None:
|
||||
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(
|
||||
"web", target, result, "/reports/x.json", "HIGH"
|
||||
)
|
||||
self.assertIn("| Image | Container | C | H |", body)
|
||||
self.assertIn("| nginx:1.27 | web | 1/1 | 1/2 |", body)
|
||||
self.assertNotIn("| M |", body)
|
||||
self.assertRegex(body, table_row_pattern(["Image", "Container", "C", "H"]))
|
||||
self.assertRegex(body, table_row_pattern(["nginx:1.27", "web", "1/1", "1/2"]))
|
||||
self.assertNotRegex(body, r"\|\s*M\s*\|")
|
||||
self.assertIn("CVE-1", body)
|
||||
self.assertIn("CVE-2", body)
|
||||
self.assertIn("CVE-5", body)
|
||||
@@ -507,9 +513,9 @@ class ReportTests(unittest.TestCase):
|
||||
self.assertIn("CVE-1", body)
|
||||
self.assertNotIn("CVE-2", body)
|
||||
self.assertNotIn("CVE-5", body)
|
||||
self.assertIn("| Image | Container | C |", body)
|
||||
self.assertIn("| nginx:1.27 | web | 1/1 |", body)
|
||||
self.assertNotIn("| H |", body)
|
||||
self.assertRegex(body, table_row_pattern(["Image", "Container", "C"]))
|
||||
self.assertRegex(body, table_row_pattern(["nginx:1.27", "web", "1/1"]))
|
||||
self.assertNotRegex(body, r"\|\s*H\s*\|")
|
||||
self.assertNotIn("Total:", body)
|
||||
|
||||
def test_build_other_containers_body_has_counts_per_container(self) -> None:
|
||||
@@ -522,10 +528,12 @@ class ReportTests(unittest.TestCase):
|
||||
)
|
||||
body = bodies[0]
|
||||
self.assertIn("Other containers with findings", body)
|
||||
self.assertIn("| Image | Container | C | H |", body)
|
||||
self.assertIn("| nginx:1.27 | web-worker | 1/1 | 1/2 |", body)
|
||||
self.assertIn("| redis:7 | cache | 0/0 | 0/4 |", body)
|
||||
self.assertNotIn("| M |", body)
|
||||
self.assertRegex(body, table_row_pattern(["Image", "Container", "C", "H"]))
|
||||
self.assertRegex(
|
||||
body, table_row_pattern(["nginx:1.27", "web-worker", "1/1", "1/2"])
|
||||
)
|
||||
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("Overall:", body)
|
||||
|
||||
@@ -544,8 +552,8 @@ class ReportTests(unittest.TestCase):
|
||||
[("web", target, result)], all_rows, "HIGH"
|
||||
)
|
||||
self.assertIn("Important containers:", body)
|
||||
self.assertIn("| Image | Container | C | H |", body)
|
||||
self.assertIn("| nginx:1.27 | web | 1/1 | 1/2 |", body)
|
||||
self.assertRegex(body, table_row_pattern(["Image", "Container", "C", "H"]))
|
||||
self.assertRegex(body, table_row_pattern(["nginx:1.27", "web", "1/1", "1/2"]))
|
||||
self.assertIn("Packages:", body)
|
||||
self.assertIn("libc 1.0 -> 1.1", 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.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:
|
||||
body = scan.build_missing_body(["gone"], ["web"])
|
||||
self.assertIn("not running", body)
|
||||
@@ -690,7 +723,7 @@ printf '%s\\n' "$*" >> "$APPRISE_LOG"
|
||||
self.assertNotIn("Total:", notification)
|
||||
self.assertIn("No CVEs at or above", 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)
|
||||
|
||||
def test_notification_includes_container(self) -> None:
|
||||
@@ -716,10 +749,10 @@ printf '%s\\n' "$*" >> "$APPRISE_LOG"
|
||||
self.assertEqual(code, 1)
|
||||
notification = Path(self.apprise_log).read_text()
|
||||
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("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.assertIn("Vulnerability scan: summary", notification)
|
||||
self.assertIn("Overall: 2/2 fixable/total in 2 container(s)", notification)
|
||||
|
||||
Reference in new issue
Block a user