Files
computer-playbook/tests/unit/python/utils/security/test_engine.py
T
kevinveenbirkenbachandClaude Opus 5.5 6b36c8579a fix(sys-ctl-hlth-runtime): scan with Trivy, Grype and OSV instead of PHP floors
The runtime check judged PHP by its upstream version and failed every
container on an end-of-life line. Distribution builds backport fixes without
raising that number, so main run 41154895895 failed Checkmk#0 on
`PHP 8.1.2-1ubuntu2.28 runs the end-of-life line 8.1`, a build Ubuntu lists
as fixed for CVE-2026-109618 and CVE-2026-109619.

- The role now runs Trivy, Grype and OSV-Scanner, with a Syft SBOM per
  target, against the host and the images of the running containers, in
  bounded parallel lanes. Targets, engines and the failing severities are
  lists in meta/services.yml; the default is every target and engine at
  CRITICAL and HIGH with a fix available.
- Grype always scans the binary-classified packages of the SBOM, so a PHP
  built from source in an official php image (8.4.26) still fails on the
  CVEs fixed in 8.4.27, which Trivy and OSV do not see.
- OSV reads an SBOM copy whose distro purls carry the source package name.
- A CVE is suppressed only in the role owning the image, under
  services.<service>.security.suppressions with id, until and a
  `# nocheck: cve <reason>` marker; a lint rejects bare, malformed and
  expired entries.
- The deploy-time run fails on its first finding: sys-service gains
  system_service_run_once and sys-service-terminator skips the retries
  only for those units. The timer runs daily at 00:00 UTC.
- The PHP version floors are removed; the php_hardening check stays.
- make test-security replaces make security-runtimes: Syft builds an SBOM
  of every pinned image straight from the GHCR mirror, cached by digest,
  and the three engines scan it with the role's threshold and suppressions.

Part of #1324

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

73 lines
2.4 KiB
Python

from __future__ import annotations
import random
from unittest import TestCase, main
from utils.security import engine
from utils.security.pipeline import pipeline
class TestEnginesFor(TestCase):
def test_every_pin_runs_behind_syft(self):
for name in engine.SCANNERS:
with self.subTest(engine=name):
self.assertEqual(engine.engines_for(name), ["syft", name])
def test_an_unknown_engine_is_rejected(self):
with self.assertRaises(ValueError):
engine.engines_for("clair")
def test_every_pin_reads_the_grype_database(self):
self.assertEqual(engine.databases_for("trivy"), ["trivy", "grype"])
self.assertEqual(engine.databases_for("grype"), ["grype"])
def test_the_ci_pool_matches_the_role_pipeline(self):
self.assertEqual(engine.SCANNERS, pipeline().SCANNERS)
def test_every_pin_is_a_valid_role_config(self):
p = pipeline()
for name in engine.SCANNERS:
with self.subTest(engine=name):
config = p.scan_config(
{
"targets": ["host", "container"],
"engines": engine.engines_for(name),
"severity": ["CRITICAL", "HIGH"],
"fixed_only": True,
"workers": 2,
"db_update": False,
}
)
self.assertEqual(list(config.databases()), engine.databases_for(name))
class TestDrawEngine(TestCase):
def test_a_draw_is_one_scanner(self):
rng = random.Random(1) # noqa: S311 - seeded draw, not crypto
self.assertIn(engine.draw_engine(rng), engine.SCANNERS)
def test_draws_cover_every_scanner(self):
rng = random.Random(7) # noqa: S311 - seeded draw, not crypto
self.assertEqual(
{engine.draw_engine(rng) for _ in range(60)}, set(engine.SCANNERS)
)
class TestDeployOverrides(TestCase):
def test_a_deploy_reads_provided_databases(self):
runtime = engine.deploy_overrides("osv", "/opt/vulnerability-db")[engine.ROLE][
"services"
]["runtime"]
self.assertEqual(
runtime,
{
"engines": ["syft", "osv"],
"database_dir": "/opt/vulnerability-db",
"db_update": False,
},
)
if __name__ == "__main__":
main()