Files
acdl/tests/test_kyverno_json_engine.py
T
Jon Chery ac18c98385 feat(P1): kyverno-json engine core + PolicyEngine protocol (REQ-291..294, 308, 309)
core/policy_engine.py: PolicyEngine Protocol (PEP 544, runtime_checkable)
+ PolicyEngineRegistry (selects from config.json.policy.engine) + NullEngine
fallback (NULL_ENGINE_INACTIVE when policy key absent).

adapters/kyverno-json/: KyvernoJsonEngine — shells to , translates
native output → list[dict] PCR records (engine: "kyverno", ruleId KJ_ prefix,
severity via nova.cloudinit.dev/severity annotation, default info).
is_configured() guards on  → KJ_ENGINE_NOT_CONFIGURED SKIPPED PCR
(distinct from NullEngine). Defensive parsing (malformed → error PCR).

config.json: new  object {engine: kyverno-json, policy_root}.

scripts/install-kyverno-json.sh: go install kj@latest (D-115).
CI (.gitea + .github): install Go + kj for policy-engine tests (best-effort;
tests skip when kj absent).

tests: 24 pass, 2 skip (kj not installed). 132 existing tests unchanged.
NullEngine satisfies PolicyEngine Protocol (G-Q8a — proves swap boundary).

---ci---
project: acdl
phase: 1
milestone: v1.25
status: execute
phase_role: execution
requirements:
  covered: [REQ-291, REQ-292, REQ-293, REQ-294, REQ-308, REQ-309]
  partial: []
---/ci---
2026-08-12 18:19:16 +00:00

213 lines
7.8 KiB
Python

"""Tests for adapters/kyverno-json/kyverno_json_engine.py (REQ-309, v1.25).
PCR schema validity (jsonschema validation), defensive parsing
(malformed output → error PCR, never exception), is_configured()
guard, severity annotation reading (G-Q10a), and pytest.skip when
kj is absent.
"""
import json
import os
import sys
from pathlib import Path
from unittest import mock
import jsonschema
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
# Load the engine module by file path (the dir has a hyphen).
import importlib.util
_ENGINE_PATH = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "kyverno_json_engine.py"
_spec = importlib.util.spec_from_file_location("kyverno_json_engine", _ENGINE_PATH)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
KyvernoJsonEngine = _mod.KyvernoJsonEngine
_to_pcr = _mod._to_pcr
_load_policy_severities = _mod._load_policy_severities
PCR_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schemas" / "policy_check_result.schema.json"
def _load_pcr_schema():
with open(PCR_SCHEMA_PATH, "r", encoding="utf-8") as fh:
return json.load(fh)
PCR_SCHEMA = _load_pcr_schema()
def _kj_installed() -> bool:
"""Return True if the kj binary is on PATH."""
return _mod._which_kj() is not None
def _smoke_policy_dir() -> Path:
return Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies"
class TestToPcr:
def test_pass_entry(self):
entry = {"policy": "require-contract-id", "rule": "require-id",
"result": "pass", "message": "ok", "resource": "res-1"}
pcr = _to_pcr(entry, "cid", "high")
assert pcr["contractId"] == "cid"
assert pcr["engine"] == "kyverno"
assert pcr["ruleId"] == "KJ_require-contract-id/require-id"
assert pcr["result"] == "pass"
assert pcr["severity"] == "high"
assert pcr["resourceRef"] == "res-1"
def test_fail_entry(self):
entry = {"policy": "forbid-public-ingress", "rule": "no-public",
"result": "fail", "message": "public ingress not allowed",
"resource": "s3/x"}
pcr = _to_pcr(entry, "cid", "critical")
assert pcr["result"] == "fail"
assert pcr["severity"] == "critical"
assert pcr["message"] == "public ingress not allowed"
def test_skip_entry(self):
entry = {"policy": "p", "rule": "r", "result": "skip"}
pcr = _to_pcr(entry, "cid", "info")
assert pcr["result"] == "skipped"
def test_unknown_result_becomes_error(self):
entry = {"policy": "p", "rule": "r", "result": "garbled"}
pcr = _to_pcr(entry, "cid", "info")
assert pcr["result"] == "error"
def test_pcr_validates_against_schema(self):
entry = {"policy": "p", "rule": "r", "result": "pass",
"message": "ok", "resource": "r"}
pcr = _to_pcr(entry, "cid-uuid", "medium")
jsonschema.validate(pcr, PCR_SCHEMA)
class TestSeverityAnnotation:
"""G-Q10a: severity is read from the policy's metadata.annotation."""
def test_policy_with_severity_annotation(self, tmp_path):
policy = {
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {
"name": "test-sev",
"annotations": {"nova.cloudinit.dev/severity": "high"},
},
"spec": {"rules": [{"name": "r", "validate": {"assert": {"all": []}}}]},
}
p = tmp_path / "test-sev.json"
p.write_text(json.dumps(policy))
sevs = _load_policy_severities(tmp_path)
assert sevs.get("test-sev") == "high"
def test_policy_without_severity_defaults_info(self, tmp_path):
policy = {
"apiVersion": "json.kyverno.io/v1alpha1",
"kind": "ValidatingPolicy",
"metadata": {"name": "no-sev"},
"spec": {"rules": [{"name": "r", "validate": {"assert": {"all": []}}}]},
}
p = tmp_path / "no-sev.json"
p.write_text(json.dumps(policy))
sevs = _load_policy_severities(tmp_path)
assert sevs.get("no-sev") == "info"
def test_underscore_files_skipped(self, tmp_path):
# _smoke.json starts with _ — should be skipped.
(tmp_path / "_smoke.json").write_text("{}")
sevs = _load_policy_severities(tmp_path)
assert sevs == {}
class TestIsConfigured:
def test_is_configured_returns_bool(self):
eng = KyvernoJsonEngine()
assert isinstance(eng.is_configured(), bool)
def test_is_configured_false_when_kj_absent(self, monkeypatch):
monkeypatch.setattr(_mod, "_which_kj", lambda: None)
eng = KyvernoJsonEngine()
assert eng.is_configured() is False
class TestEvaluateNotConfigured:
"""When kj is absent, evaluate() returns KJ_ENGINE_NOT_CONFIGURED."""
def test_evaluate_returns_skipped_when_not_configured(self, monkeypatch):
monkeypatch.setattr(_mod, "_which_kj", lambda: None)
eng = KyvernoJsonEngine()
out = eng.evaluate({"id": "x"}, Path("/tmp/policies"), "cid-1")
assert len(out) == 1
assert out[0]["ruleId"] == "KJ_ENGINE_NOT_CONFIGURED"
assert out[0]["result"] == "skipped"
jsonschema.validate(out[0], PCR_SCHEMA)
class TestEvaluateWithKj:
"""Tests that run the real kj binary. Skip when kj is not installed."""
@pytest.fixture(autouse=True)
def _require_kj(self):
if not _kj_installed():
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
def test_smoke_policy_round_trip(self, tmp_path):
eng = KyvernoJsonEngine()
if not eng.is_configured():
pytest.skip("kj not configured")
# Use the real smoke policy dir.
out = eng.evaluate({"id": "msvc"}, _smoke_policy_dir(), "cid-smoke")
assert isinstance(out, list)
assert len(out) >= 1
for pcr in out:
jsonschema.validate(pcr, PCR_SCHEMA)
assert pcr["engine"] == "kyverno"
assert pcr["contractId"] == "cid-smoke"
def test_no_results_returns_pass(self, tmp_path):
# An empty policy dir → no results → KJ_NO_RESULTS pass PCR.
eng = KyvernoJsonEngine()
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
out = eng.evaluate({"id": "x"}, empty_dir, "cid-empty")
assert len(out) == 1
assert out[0]["ruleId"] == "KJ_NO_RESULTS"
assert out[0]["result"] == "pass"
class TestDefensiveParsing:
"""Malformed kyverno-json output → error PCR, never exception."""
def test_malformed_output_produces_error_pcr(self, monkeypatch):
eng = KyvernoJsonEngine()
# Mock is_configured → True, then mock subprocess to return
# garbage output.
monkeypatch.setattr(_mod, "_which_kj", lambda: "/fake/kj")
monkeypatch.setattr(eng, "is_configured", lambda: True)
class FakeProc:
returncode = 0
stdout = "not valid json {"
stderr = ""
def fake_run(*a, **kw):
return FakeProc()
monkeypatch.setattr(_mod.subprocess, "run", fake_run)
out = eng.evaluate({"id": "x"}, _smoke_policy_dir(), "cid-bad")
assert len(out) == 1
assert out[0]["result"] == "error"
assert out[0]["ruleId"] == "KJ_ENGINE_ERROR"
jsonschema.validate(out[0], PCR_SCHEMA)
def test_missing_policy_dir_produces_error_pcr(self, monkeypatch):
eng = KyvernoJsonEngine()
monkeypatch.setattr(_mod, "_which_kj", lambda: "/fake/kj")
monkeypatch.setattr(eng, "is_configured", lambda: True)
out = eng.evaluate({"id": "x"}, Path("/nonexistent/dir"), "cid-miss")
assert len(out) == 1
assert out[0]["result"] == "error"
assert "not found" in out[0]["message"]