mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-14 06:37:25 +00:00
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>
89 lines
2.7 KiB
Python
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()
|