mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Answering "which deployed containers run a vulnerable PHP?" after CVE-2026-109618/109619 took a manual inventory of registry metadata, and nothing noticed a hardening ini that an image never loads. - New role sys-ctl-hlth-runtime, routed through sys-svc-compose next to sys-ctl-hlth-container. A daily timer runs `php` in every running container and fails, alerting through sys-ctl-alm-compose, when PHP is below the floor of its line or on a line older than every floor (7.4, 8.1). Every service that mounts php_hardening in meta/volumes.yml fails while expose_php or session.upload_progress.enabled is on; other PHP containers of such a stack are listed as info. Compose and swarm containers are matched by their project/stack and service labels. The check is not run at deploy time, so a finding never fails a deploy. - The floors (8.2.35, 8.3.36, 8.4.27, 8.5.12) live once, in HEALTH_RUNTIME_FLOORS of the role's vars/main.yml. - `make security-runtimes` lists the PHP of every pinned role image without pulling: Docker Hub tags through the Hub API, which no pull rate limit meters, other registries through the OCI config blob (new registry.config_blob, which the healthcheck probe now reuses). PHP_VERSION, distro PHP packages and a PHP_BASE build argument count. It exits 1 on an image below its floor or one the registry did not answer. Today it flags nfrastack/fusiondirectory:1.5 (line 7.4) and shopware/docker-base:8.3 (8.3.25). The role TODO.md keeps the follow-ups: further runtimes, the split web SAPI of web-app-lam, a scheduled CI run of the report and the CVE scan. Refs #1127 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.3 KiB
Python
72 lines
2.3 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import io
|
|
import unittest
|
|
|
|
import cli.contributing.security.runtimes.__main__ as runtimes_main
|
|
from utils.docker.image.discovery import ImageRef
|
|
|
|
FLOORS = {"8.2": "8.2.35", "8.3": "8.3.36", "8.4": "8.4.27", "8.5": "8.5.12"}
|
|
|
|
|
|
def _ref(role, service):
|
|
return ImageRef(
|
|
role=role,
|
|
service=service,
|
|
name=f"acme/{service}",
|
|
version="1.0",
|
|
source=f"docker.io/acme/{service}:1.0",
|
|
)
|
|
|
|
|
|
class TestReport(unittest.TestCase):
|
|
def _report(self, refs, probed):
|
|
with contextlib.redirect_stdout(io.StringIO()) as out:
|
|
code = runtimes_main.report(refs, probed, FLOORS)
|
|
return code, out.getvalue()
|
|
|
|
def test_an_eol_image_fails_the_report(self):
|
|
code, output = self._report(
|
|
[_ref("web-app-akaunting", "akaunting")], [["ENV PHP_VERSION=8.1.34"]]
|
|
)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn("runs the end-of-life line 8.1", output)
|
|
|
|
def test_patched_images_and_images_without_php_pass(self):
|
|
code, _output = self._report(
|
|
[
|
|
_ref("web-app-wordpress", "wordpress"),
|
|
_ref("svc-db-postgres", "postgres"),
|
|
],
|
|
[["ENV PHP_VERSION=8.4.27"], ["ENV PG_VERSION=17.2"]],
|
|
)
|
|
self.assertEqual(code, 0)
|
|
|
|
def test_an_unanswered_image_fails_the_report(self):
|
|
code, output = self._report([_ref("web-app-lam", "lam")], [None])
|
|
self.assertEqual(code, 1)
|
|
self.assertIn(
|
|
"web-app-lam:lam (docker.io/acme/lam:1.0): the registry gave no answer",
|
|
output,
|
|
)
|
|
|
|
def test_a_distro_line_without_patch_level_is_listed_but_passes(self):
|
|
code, output = self._report(
|
|
[_ref("web-app-lam", "lam")], [["RUN apt-get install -y php8.4-ldap"]]
|
|
)
|
|
self.assertEqual(code, 0)
|
|
self.assertIn("PHP line 8.4: patch level not in the registry metadata", output)
|
|
|
|
def test_an_eol_php_base_argument_fails_the_report(self):
|
|
code, output = self._report(
|
|
[_ref("web-app-fusiondirectory", "fusiondirectory")],
|
|
[["ARG PHP_BASE=7.4", "RUN |1 PHP_BASE=7.4 /bin/bash -c install"]],
|
|
)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn("PHP line 7.4: line 7.4 is end of life", output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|