e74a8c2f5d
---ci--- project: acdl phase: 42 milestone: v1.9 status: execute ---/ci--- Phase 42 — stub-implementation (REQ-107..111, D-084): route_halt_artifact (REQ-107): - core/separation_of_duties.py: real SNS publish (ACDL_SOD_HALT_TOPIC_ARN) + outbox fallback (SEPARATION_OF_DUTIES_VIOLATION event via outbox_writer) + stderr emission. No silent print-only stub. - terraform/platform/main.tf: aws_sns_topic.acdl-sod-halt + output. HITL attestation gates (REQ-108): - core/hitl_gates.py: attest(contract_id, env, approver, evidence, outbox_client) records approver_qa/approver_prod/approver_dr to outbox, runs SoD check on prod, invokes attestation matrix, returns (ok, reason). Dev skips (autonomous). approver_from_env() reads GITHUB_ACTOR/GITEA_ACTOR. - scripts/run_platform.sh: Step 7b HITL gate before apply for qa/prod/dr. 8-concern attestation matrix (REQ-109, D-084): - core/attestation_matrix.py: check(env, evidence) runs the 8 concerns from hitl_matrix_design.md §10.4. Offline-testable (contract_nfrs, schema_validity, policy_pass) run for real. Operator-supplied accept signed artifacts validated for freshness (FRESHNESS_DAYS table) + schema. Signature skip when ACDL_ATTESTATION_SIGNING_KEY_ID unset (D-089). Fail loud if missing/expired for prod/dr. Wiz real client (REQ-110): - adapters/wiz/wiz_adapter.py: WizClient (GraphQL API, Bearer auth, pagination via pageInfo.hasNextPage + endCursor). fetch_and_adapt translates issues → PolicyCheckResult; graceful degrade when WIZ_API_TOKEN/WIZ_API_URL unset. Kyverno fleshed out (REQ-111): - adapters/kyverno/kyverno_adapter.py: full PolicyReport → PolicyCheckResult mapping (pass/fail/skip/warn + severity + skip-with- reason + resource ref construction from kind/name/namespace). adapt_inactive() emits KYVERNO_INACTIVE_TF_STACK guard. --kube-version stub parsed for future GitOps. Tests: +47 (test_route_halt_artifact.py, test_hitl_gates.py, test_attestation_matrix.py, test_wiz_adapter_real_client.py, expanded test_kyverno_adapter.py). Existing wiz_adapter tests updated for the real client's control.name ruleId. 493 passed; run_ci.sh green; run_platform.sh --check-only green.
136 lines
4.7 KiB
Python
136 lines
4.7 KiB
Python
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
|
|
|
|
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
|
|
and produces PolicyReport resources. This adapter translates those results
|
|
to the normalized PolicyCheckResult schema (engine: "kyverno").
|
|
|
|
v1.9 (REQ-111): the translator is fleshed out — full PolicyReport →
|
|
PolicyCheckResult mapping with severity + skip-with-reason handling. It
|
|
remains inactive for Terraform-only stacks (guard preserved — emits a
|
|
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
|
|
A `--kube-version` stub is parsed but not yet used (for future GitOps).
|
|
|
|
D-053: the platform emits Terraform, not K8s manifests. This adapter
|
|
activates when the GitOps reconciler (roadmap) emits K8s manifests.
|
|
Sample policies are included as documentation at adapters/kyverno/policies/.
|
|
|
|
CLI: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]
|
|
"""
|
|
|
|
import datetime
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
SEVERITY_MAP = {
|
|
"critical": "critical",
|
|
"high": "high",
|
|
"medium": "medium",
|
|
"low": "low",
|
|
"info": "info",
|
|
"informational": "info",
|
|
}
|
|
|
|
RESULT_MAP = {
|
|
"pass": "pass",
|
|
"fail": "fail",
|
|
"warn": "skipped",
|
|
"warning": "skipped",
|
|
"error": "error",
|
|
"skip": "skipped",
|
|
"skipped": "skipped",
|
|
}
|
|
|
|
|
|
def _iso8601_now():
|
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def _to_pcr(entry, contract_id):
|
|
severity_raw = entry.get("severity", "info")
|
|
severity = SEVERITY_MAP.get(str(severity_raw).lower(), "info")
|
|
result_raw = entry.get("result", "skip")
|
|
result = RESULT_MAP.get(str(result_raw).lower(), "error")
|
|
# Skip-with-reason: a skipped result carries a message that explains why.
|
|
message = entry.get("message", "")
|
|
if result == "skipped" and not message:
|
|
message = entry.get("skipReason", entry.get("skippedMessage", "skipped (no reason)"))
|
|
policy = entry.get("policy", "")
|
|
rule = entry.get("rule", "")
|
|
rule_id = f"{policy}/{rule}" if rule else (policy or "KYVERNO_UNKNOWN")
|
|
resource = entry.get("resource", "")
|
|
if not resource and entry.get("name"):
|
|
# Construct a resource ref from kind/name/namespace when present.
|
|
kind = entry.get("kind", "")
|
|
ns = entry.get("namespace", "")
|
|
resource = f"{kind}/{ns}/{entry.get('name')}" if kind else entry.get("name", "")
|
|
return {
|
|
"contractId": contract_id,
|
|
"evaluatedAt": _iso8601_now(),
|
|
"engine": "kyverno",
|
|
"ruleId": rule_id,
|
|
"severity": severity,
|
|
"result": result,
|
|
"message": message,
|
|
"evidence": {
|
|
"resource": resource,
|
|
"namespace": entry.get("namespace", ""),
|
|
"kind": entry.get("kind", ""),
|
|
"name": entry.get("name", ""),
|
|
"policy": policy,
|
|
"rule": rule,
|
|
},
|
|
"resourceRef": resource,
|
|
}
|
|
|
|
|
|
def _emit_inactive_tf(contract_id):
|
|
"""Emit a SKIPPED record when the platform emits Terraform, not K8s manifests."""
|
|
return {
|
|
"contractId": contract_id,
|
|
"evaluatedAt": _iso8601_now(),
|
|
"engine": "kyverno",
|
|
"ruleId": "KYVERNO_INACTIVE_TF_STACK",
|
|
"severity": "info",
|
|
"result": "skipped",
|
|
"message": "Kyverno inactive — the platform emits Terraform, not K8s manifests. Activates when the GitOps reconciler emits K8s manifests (D-053).",
|
|
"evidence": {},
|
|
"resourceRef": "",
|
|
}
|
|
|
|
|
|
def adapt(policyreport_json_path, contract_id, kube_version=None):
|
|
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
out = []
|
|
# Kyverno PolicyReport has a .results[] array.
|
|
results = data.get("results", [])
|
|
if not isinstance(results, list):
|
|
results = []
|
|
for entry in results:
|
|
out.append(_to_pcr(entry, contract_id))
|
|
if not out:
|
|
out.append(_emit_inactive_tf(contract_id))
|
|
# kube_version is parsed but not yet used (future GitOps reconciler).
|
|
_ = kube_version
|
|
return out
|
|
|
|
|
|
def adapt_inactive(contract_id):
|
|
"""Convenience: emit the inactive-for-TF record directly (no report file)."""
|
|
return [_emit_inactive_tf(contract_id)]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
kube_ver = None
|
|
args = sys.argv[1:]
|
|
if "--kube-version" in args:
|
|
idx = args.index("--kube-version")
|
|
if idx + 1 < len(args):
|
|
kube_ver = args[idx + 1]
|
|
args = args[:idx] + args[idx + 2:]
|
|
if len(args) != 2:
|
|
print("usage: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]", file=sys.stderr)
|
|
sys.exit(2)
|
|
print(json.dumps(adapt(args[0], args[1], kube_version=kube_ver), indent=2)) |