Files
computer-playbook/tests/unit/python/cli/contributing/security/test_issues.py
T
kevinveenbirkenbachandClaude Opus 5.5 5e5e61d255 feat(ci): scan the mirrored images daily and track their CVEs as issues
The daily mirror workflow gains a scan job after the sync. It runs
make test-security against the GHCR mirror, writes the failing findings per
image and per CVE into the job summary and uploads the report as the
vulnerability-findings artifact.

- One issue per CVE and image, labelled vulnerability, is opened, updated
  while the finding stays and closed once no engine reports it. Issues are
  only closed when the scan reported no error, so an image that failed to
  scan keeps its issues.
- workflow_dispatch and workflow_call take an issues switch; false is a dry
  run that only produces the summary and the artifact. Scheduled runs always
  sync the issues.
- Only the scan job holds issues: write.

Part of #1324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:44:59 +01:00

89 lines
2.7 KiB
Python

from __future__ import annotations
import json
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 = "🛡️ CVE-2026-109618 in docker.io/roundcube/roundcubemail:1.7.5-apache"
class FakeGh:
def __init__(self, open_issues):
self.open_issues = open_issues
self.calls = []
def __call__(self, args):
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_known_finding_updates_its_issue(self):
gh = FakeGh([{"number": 7, "title": TITLE}])
issues_main.sync({"findings": [ROW], "errors": []}, "o/r", gh)
self.assertEqual(gh.calls[-1][:3], ["issue", "edit", "7"])
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_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 test_every_engine_gets_a_row(self):
text = issues_main.body([ROW, {**ROW, "engine": "trivy"}])
self.assertEqual(
sum(
1
for line in text.splitlines()
if line.startswith(("| grype", "| trivy"))
),
2,
)
if __name__ == "__main__":
main()