mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +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>
71 lines
2.1 KiB
Python
71 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.roles.entity.name import get_entity_name
|
|
|
|
HARDENING_VOLUME = "php_hardening"
|
|
|
|
|
|
def hardened_services(volumes: Any) -> list[str]:
|
|
"""Return the services that mount the shared PHP hardening ini.
|
|
|
|
Args:
|
|
volumes: the role's canonical ``meta/volumes.yml`` mapping.
|
|
|
|
Returns:
|
|
The sorted service keys listed under ``php_hardening.mounts``.
|
|
"""
|
|
if not isinstance(volumes, Mapping):
|
|
return []
|
|
entry = volumes.get(HARDENING_VOLUME)
|
|
if not isinstance(entry, Mapping):
|
|
return []
|
|
return sorted(
|
|
{
|
|
str(mount["service"])
|
|
for mount in entry.get("mounts") or []
|
|
if isinstance(mount, Mapping) and mount.get("service")
|
|
}
|
|
)
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
"""``lookup('php_hardened_services', group_names=<role ids>)``.
|
|
|
|
Args:
|
|
group_names: the deployed role ids.
|
|
|
|
Returns:
|
|
``{entity: [service, ...]}`` for every role whose ``meta/volumes.yml``
|
|
declares ``php_hardening``; the entity is the compose project and
|
|
swarm stack name.
|
|
"""
|
|
|
|
def run(
|
|
self,
|
|
terms: list[Any] | None,
|
|
variables: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> list[dict[str, list[str]]]:
|
|
group_names = kwargs.get("group_names")
|
|
if not isinstance(group_names, (list, tuple)):
|
|
raise AnsibleError(
|
|
"php_hardened_services: group_names must be a list of role ids"
|
|
)
|
|
|
|
volume = lookup_loader.get(
|
|
"volume", loader=self._loader, templar=getattr(self, "_templar", None)
|
|
)
|
|
result: dict[str, list[str]] = {}
|
|
for role_id in sorted({str(name) for name in group_names}):
|
|
services = hardened_services(volume.run([role_id], variables=variables)[0])
|
|
if services:
|
|
result[get_entity_name(role_id)] = services
|
|
return [result]
|