The v1.25 kyverno-json engine adapter and policies were authored but never
validated against the real `kj` binary — the test suite
`pytest.skip("kj not installed")` when `kj` was absent, masking the bug.
With `kj` v0.0.3 now installed, the 3 failing-fixture tests
(stack-ir/plan-json/regression) showed 0 fails (all passed falsely). Root
causes (3 substrate bugs) and fixes:
1. ENGINE — bare-list output format. `kj scan --output json` emits a bare
JSON LIST at the top level (NOT `{"results": [...]}`); each entry has
`resource` + `results[].rules[]` with `violations[]` (fail) / `error`
string (eval error) / neither (pass). The v1.25 `_translate` did
`out.get("results", [])` on a dict → `out` is a list → returned `[]` →
emitted a single KJ_NO_RESULTS pass PCR. Rewrote `_translate` to parse
the real v0.0.3 nested shape (policy.metadata.name, rule.name,
violations[].errors[].field/detail/value). Future-proofs to also accept
the legacy dict shape. Preserves RESULT_MAP, severity-from-annotation,
is_configured(), _skipped_not_configured, _error_pcr, the temp-file
payload write, and the subprocess invocation.
2. ENGINE — `.json` policies not loaded by `kj`. The upstream loader
(pkg/policy/load.go) uses fileinfo.IsYaml() which only matches
`.yaml`/`.yml` — `.json` files are silently skipped (0 policies).
Nova policies are authored as `.json` (TestPolicyFilesExist asserts the
filenames). Added `_materialize_yaml_policy_dir`: mirrors the source
tree to a temp dir, copying every `.json` policy to a `.yaml` twin
(JSON is a valid YAML subset, verified against kj v0.0.3). Source
`.json` files remain untouched.
3. POLICIES — `validate` wrapper + check syntax. Removed the `validate`
wrapper from all 16 policies (kj v0.0.3 ignores `validate`-wrapped
rules — `assert` goes directly under the rule). Fixed the check syntax:
a check entry is `expression: expected_value` (e.g.
`(regex_match(..., @)): true`), not `field: (expression)` (which
compared a bool to nothing → "types not comparable"). For per-resource
checks over stack-IR/plan-JSON, `~.resources` (descendant anchor) is
required for per-element iteration; a plain path applies to the whole
array. For type-scoped rules (s3/ebs encryption, iam/db/kms), the type
guard is folded into the expression (`type == '...' && !<has-prop>`)
so non-matching resources short-circuit to false. cap-013 dedup uses
`max(map(&length(@), values(group_by(adapters, &@)))) == `1`` (no
`duplicates` JMESPath fn exists). Preserved all policy metadata
(apiVersion, kind, metadata.name, severity + title annotations) —
TestPolicyValidity/TestPolicyFilesExist still pass.
INSTALL SCRIPT — the v1.25 `go install .../cmd/kj@latest` failed: the
`cmd/kj` path does not exist in v0.0.3 (upstream produces a binary named
`kyverno-json`). Fixed to `go install github.com/kyverno/kyverno-json@latest`
+ symlink `kyverno-json` → `kj` (GOBIN and /usr/local/bin fallbacks).
Idempotent: short-circuits when `kj` is already on PATH and working.
Verification: `which kj` → /usr/local/bin/kj; `kj version` → v0.0.3.
test_kyverno_json_engine + test_stack_ir_policies + test_plan_json_policies
+ test_meta_policies + test_regression_policies: 36 passed, 0 skips
(_require_kj no longer skips). Full suite (excluding pre-existing hang in
test_verify_regression_mode.py): 776 passed, 6 failed — all 6 failures are
pre-existing (confirmed by stashing this commit's diff and re-running);
the only in-scope-acceptable failure is
test_module_standards.py::test_all_l1_have_required_files (dynamodb
extension drift, data-engineer's later wave).
---ci---
project: acdl
phase: 3
milestone: v1.26
status: execute
wave: W0.5
---
kyverno-json Engine Adapter (v1.25)
The kyverno-json engine is Nova's primary compliance/policy tool
(v1.25), implemented behind the swappable PolicyEngine protocol so
OPA (or any other engine) can replace it one day.
What kyverno-json is
kyverno-json is a standalone
Go binary from the Kyverno project — a separate runtime from the
K8s Kyverno admission controller. It applies Kyverno ValidatingPolicy
resources to any JSON or YAML payload file via the kj scan CLI.
Unlike the K8s Kyverno adapter (adapters/kyverno/), which only
speaks to K8s manifests, kyverno-json evaluates consumer contracts,
resolved Stack IR, terraform plan JSON, and even the merged PCR list
itself (meta-policies).
Install
bash scripts/install-kyverno-json.sh
# or directly:
go install github.com/kyverno/kyverno-json/cmd/kj@latest
kj version
The platform functions without the binary — is_configured() returns
False when which kj is absent → evaluate() returns a single
SKIPPED PCR (KJ_ENGINE_NOT_CONFIGURED). The confidence signal
proceeds with a neutral policy input (D-120 graceful degradation).
Policy directory layout
adapters/kyverno-json/policies/
├── _smoke.json # round-trip smoke test
├── contract/ # consumer contract JSON policies
│ ├── require-id-pattern.json
│ ├── require-env-in-enum.json
│ ├── require-infrastructure-min-1.json
│ └── forbid-unknown-fields.json
├── stack-ir/ # resolved Stack IR policies
│ ├── require-tagging-standard.json
│ ├── forbid-public-ingress.json
│ └── require-encryption-by-default.json
├── plan-json/ # terraform show -json policies
│ ├── forbid-plaintext-secrets.json
│ ├── forbid-iam-wildcard.json
│ └── require-kms-reference.json
├── meta/ # policies over the merged PCR list
│ ├── block-on-any-critical.json
│ └── tagging-rules-agree.json
└── regression/ # capability-inventory policies
├── cap-013-adapter-dedup.json
├── cap-023-metrics-collector.json
└── cap-024-deck-structure.json
The four policy categories
- contract/ — over the consumer contract JSON (pre-resolve).
- stack-ir/ — over the resolved Target Stack IR (post-resolve).
- plan-json/ — over
terraform show -jsonoutput (pipeline Step 5b). - meta/ — over the merged
list[PolicyCheckResult](meta-policies). - regression/ — over the capability-inventory JSON (declarative
mirrors of
core/regression_verify.py).
Severity convention
kyverno-json does not natively assign severities. Each Nova policy
declares its severity via a metadata.annotations field:
metadata:
annotations:
nova.cloudinit.dev/severity: high
Valid values: critical, high, medium, low, info (default
when absent).
Engine enum reuse (D-116)
kyverno-json PCR records carry engine: "kyverno" (no new enum value).
The engine field records the policy-engine family, not the specific
binary. The K8s Kyverno adapter and the kyverno-json engine are
distinguished by ruleId prefix (KYVERNO_ vs KJ_) and evidence
payload shape (namespace/kind vs assertion/jmespath).
Schema path
The output records validate against
schemas/policy_check_result.schema.json
(engine: "kyverno" is in the enum). The confidence signal consumes
the merged PCR list engine-agnostically.
Swap boundary
The PolicyEngine protocol (core/policy_engine.py) is the swap
boundary. The OPA-equivalent surface is documented in
.ciagent/RESEARCH.md §4.2 — a future OpaEngine implements the same
protocol without touching the confidence signal, the PCR schema, or
the pipeline.