Files
computer-playbook/tests/unit/python/cli/contributing/security/test_runtimes.py
T
kevinveenbirkenbachandClaude Opus 5.5 3c743736a8 feat(sys-ctl-hlth-runtime): alert on EOL or unpatched PHP and missing PHP hardening
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>
2026-10-29 00:12:25 +01:00

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()