Files
computer-playbook/tests/unit/python/cli/contributing/security/test_issues.py
T
kevinveenbirkenbachandClaude Opus 5.5 4fa63dd1fb feat(security): keep one vulnerability issue per image and comment its unresolved CVEs
The dry-run mirror scan (run 41286420589) reported 10,179 CVE/image pairs
across 135 red digests; one issue per pair would flood the tracker. The
sync now keeps exactly one open issue per pinned image:

- The body lists every CVE row (id, severity, engine, role, service,
  package, installed, fixed), CRITICAL first, and stops below 60,000
  characters with the number of rows left out and a pointer to the
  vulnerability-findings artifact. On the dry-run data: 135 issues, 8
  capped, the largest body 59,872 characters.
- A further open issue with the same title closes as a duplicate of the
  oldest one.
- When an image's CVE table changed, the issue gets a "Still unresolved"
  comment with the table and an updated body; an unchanged table leaves the
  issue alone, so a daily run does not add 135 identical comments.

Part of #1324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:48:08 +01:00

146 lines
5.4 KiB
Python

from __future__ import annotations
import json
import subprocess
from unittest import TestCase, main
import cli.contributing.security.issues.__main__ as issues_main
ROW = {
"engine": "grype",
"role": "web-app-roundcube",
"service": "roundcube",
"image": "docker.io/roundcube/roundcubemail:1.7.5-apache",
"mirror": "ghcr.io/o/r/mirror/docker.io/roundcube/roundcubemail",
"digest": "sha256:abc",
"id": "CVE-2026-109618",
"ids": ["CVE-2026-109618"],
"severity": "CRITICAL",
"package": "php-cli",
"installed": "8.4.26",
"fixed": "8.4.27",
}
TITLE = "🛡️ docker.io/roundcube/roundcubemail:1.7.5-apache"
OTHER_IMAGE = "docker.io/library/nginx:1.31.8-alpine"
class FakeGh:
def __init__(self, open_issues, hang=()):
self.open_issues = open_issues
self.hang = [list(verb) for verb in hang]
self.calls = []
def __call__(self, args):
if list(args[:2]) in self.hang:
raise subprocess.TimeoutExpired(["gh", *args], 120)
self.calls.append(list(args))
if args[:2] == ["issue", "list"]:
return json.dumps(self.open_issues)
return ""
def verbs(self):
return [
call[:2] for call in self.calls if call[0] == "issue" and call[1] != "list"
]
class TestSync(TestCase):
def test_a_new_finding_opens_one_issue(self):
gh = FakeGh([])
issues_main.sync(
{"findings": [ROW, {**ROW, "engine": "trivy"}], "errors": []}, "o/r", gh
)
self.assertEqual(gh.verbs(), [["issue", "create"]])
def test_a_changed_image_gets_a_comment_and_an_updated_body(self):
gh = FakeGh([{"number": 7, "title": TITLE, "body": "old table"}])
issues_main.sync({"findings": [ROW], "errors": []}, "o/r", gh)
self.assertEqual(gh.verbs(), [["issue", "comment"], ["issue", "edit"]])
comment = gh.calls[-2]
self.assertIn("Still unresolved:", comment[comment.index("--body") + 1])
self.assertIn(ROW["id"], comment[comment.index("--body") + 1])
def test_an_unchanged_image_leaves_its_issue_alone(self):
current = issues_main.body([ROW])
gh = FakeGh([{"number": 7, "title": TITLE, "body": current + "\r\n"}])
actions = issues_main.sync({"findings": [ROW], "errors": []}, "o/r", gh)
self.assertEqual((gh.verbs(), actions), ([], [f"unchanged #7 {TITLE}"]))
def test_a_second_open_issue_of_one_image_closes_as_duplicate(self):
current = issues_main.body([ROW])
gh = FakeGh(
[
{"number": 9, "title": TITLE, "body": current},
{"number": 7, "title": TITLE, "body": current},
]
)
actions = issues_main.sync({"findings": [ROW], "errors": []}, "o/r", gh)
self.assertEqual(gh.verbs(), [["issue", "close"]])
self.assertEqual(gh.calls[-1][2], "9")
self.assertIn(f"unchanged #7 {TITLE}", actions)
def test_a_fixed_finding_closes_its_issue(self):
gh = FakeGh([{"number": 7, "title": TITLE}])
actions = issues_main.sync({"findings": [], "errors": []}, "o/r", gh)
self.assertEqual(actions, [f"closed #7 {TITLE}"])
def test_a_scan_error_closes_nothing(self):
gh = FakeGh([{"number": 7, "title": TITLE}])
issues_main.sync(
{"findings": [], "errors": ["x: the mirror served no digest"]}, "o/r", gh
)
self.assertEqual(gh.verbs(), [])
def test_a_failed_issue_call_is_reported_and_the_others_still_run(self):
other = {**ROW, "image": OTHER_IMAGE}
gh = FakeGh([{"number": 7, "title": TITLE}], hang=[("issue", "edit")])
actions = issues_main.sync({"findings": [ROW, other], "errors": []}, "o/r", gh)
failed = [action for action in actions if action.startswith("failed ")]
self.assertEqual(len(failed), 1)
self.assertTrue(failed[0].startswith(f"failed updated #7 {TITLE}"))
self.assertEqual(gh.verbs(), [["issue", "create"], ["issue", "comment"]])
def test_every_cve_of_one_image_shares_one_issue(self):
gh = FakeGh([])
actions = issues_main.sync(
{
"findings": [ROW, {**ROW, "id": "CVE-2026-109619"}],
"errors": [],
},
"o/r",
gh,
)
self.assertEqual(actions, [f"opened {TITLE}"])
def test_the_issue_is_labelled(self):
gh = FakeGh([])
issues_main.sync({"findings": [ROW], "errors": []}, "o/r", gh)
create = gh.calls[-1]
self.assertEqual(create[create.index("--label") + 1], issues_main.LABEL)
class TestBody(TestCase):
def _table(self, text):
return [line for line in text.splitlines() if line.startswith("| CVE-")]
def test_every_engine_gets_a_row(self):
text = issues_main.body([ROW, {**ROW, "engine": "trivy"}])
self.assertEqual(len(self._table(text)), 2)
def test_critical_rows_come_first(self):
high = {**ROW, "id": "CVE-2026-000001", "severity": "HIGH"}
table = self._table(issues_main.body([high, ROW]))
self.assertIn("| CRITICAL |", table[0])
def test_a_large_image_stays_under_the_body_limit(self):
rows = [{**ROW, "id": f"CVE-2026-{index:06d}"} for index in range(2000)]
text = issues_main.body(rows)
self.assertLessEqual(len(text), issues_main.BODY_LIMIT)
hidden = 2000 - len(self._table(text))
self.assertGreater(hidden, 0)
self.assertIn(f"{hidden} more row(s)", text)
if __name__ == "__main__":
main()