Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96d4677fac | |||
| 863484e681 | |||
| 7f4b79593a | |||
| 35e3de401e | |||
| 814d45b211 | |||
| 0f0d9b9145 | |||
| e6ee79402b | |||
| 4b6c3a12d8 |
+10
-12
@@ -1,24 +1,22 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"phase": 2,
|
||||
"stage": "complete",
|
||||
"milestone": "v1.25",
|
||||
"phase_role": "pre_execution",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-12T16:50:00Z",
|
||||
"updated_at": "2026-08-12T17:15:00Z",
|
||||
"project": "acdl",
|
||||
"milestone_complete": false,
|
||||
"tag_line": "v1.24.x",
|
||||
"tag": "v1.24.0",
|
||||
"next_tag": "v1.24.1",
|
||||
"phases": 6,
|
||||
"execution_phases": 4,
|
||||
"requirements_total": 19,
|
||||
"requirements": ["REQ-291", "REQ-292", "REQ-293", "REQ-294", "REQ-295", "REQ-296", "REQ-297", "REQ-298", "REQ-299", "REQ-300", "REQ-301", "REQ-302", "REQ-303", "REQ-304", "REQ-305", "REQ-306", "REQ-307", "REQ-308", "REQ-309"],
|
||||
"tag": "v1.24.2",
|
||||
"next_tag": "v1.24.3",
|
||||
"release": {
|
||||
"forge": "gitea",
|
||||
"releases_created": true,
|
||||
"release_id": 640,
|
||||
"release_tag": "v1.24.0"
|
||||
"release_ids": {"v1.24.0": 640, "v1.24.1": 641, "v1.24.2": 642},
|
||||
"phase_release_id": 642
|
||||
},
|
||||
"notes": "v1.25 phase 0 (pre-execution) complete. Tag v1.24.0 (gitea release id 640). 19 requirements (REQ-291..309) specified, clarified, researched, ideated, planned, grilled (PROCEED 0.86). 4 execution phases + P5 final. Phase 00 branch deleted. Next: P1 engine-core."
|
||||
"requirements": ["REQ-291", "REQ-292", "REQ-293", "REQ-294", "REQ-295", "REQ-296", "REQ-297", "REQ-298", "REQ-299", "REQ-308", "REQ-309"],
|
||||
"tests": {"total": 119, "passed": 119, "skipped": 7, "failed": 0},
|
||||
"notes": "v1.25 P2 (contract+stack-IR policies) complete. Tag v1.24.2 (gitea release id 642). 5 requirements (REQ-295..299). 7 contract+stack-IR policies. Resolver wired (pre+post resolve). Phase 02 branch deleted. Next: P3 plan-JSON + meta-orchestration + pipeline wiring."
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-unknown-fields",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "low",
|
||||
"title.policy.kyverno.io": "Contract has only schema-allowed fields"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-unknown-fields",
|
||||
"validate": {
|
||||
"message": "contract may only contain id, name, environment, infrastructure (schema-allowed fields)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"(length(keys(@)) == `4`)": true,
|
||||
"keys(@)": "(contains(['id','name','environment','infrastructure'], @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-env-in-enum",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "Contract environment is one of dev/qa/prod/dr"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "env-enum",
|
||||
"validate": {
|
||||
"message": "contract.environment must be one of dev, qa, prod, dr",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"environment": "(contains(['dev','qa','prod','dr'], @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-id-pattern",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "Contract id matches operational acronym pattern"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "id-pattern",
|
||||
"validate": {
|
||||
"message": "contract.id must match ^[a-z][a-z0-9-]{2,5}$ (3-6 char operational acronym)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"id": "(regex_match('^[a-z][a-z0-9-]{2,5}$', @))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-infrastructure-min-1",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "Contract declares at least one infrastructure entry"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "infra-min-1",
|
||||
"validate": {
|
||||
"message": "contract.infrastructure must have at least one module entry",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"infrastructure": "(length(keys(@)) > `0`)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "block-on-any-critical",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "critical",
|
||||
"title.policy.kyverno.io": "Block on any critical-fail policy result (declarative source of truth)"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-critical-fail",
|
||||
"validate": {
|
||||
"message": "No PolicyCheckResult in the merged list may have severity: critical + result: fail. The confidence_signal.py hard-override is the defense-in-depth behind this declarative rule (D-119).",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.[]": {
|
||||
"(severity == 'critical' && result == 'fail')": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "tagging-rules-agree",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "Checkov NOVA_TAG_NAMING and kj KJ_REQUIRE_TAGGING_STANDARD agree per resource"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-tagging-divergence",
|
||||
"validate": {
|
||||
"message": "For every resource, the Checkov NOVA_TAG_NAMING result and the kyverno-json KJ_REQUIRE_TAGGING_STANDARD result must agree. Divergence emits an error PCR (D-118, defense-in-depth against rule drift).",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.[?(ruleId == 'NOVA_TAG_NAMING')]": {
|
||||
"result->ckv_result": {},
|
||||
"($ckv_result == 'fail')": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check": {
|
||||
"~.[?(ruleId == 'KJ_REQUIRE_TAGGING_STANDARD')]": {
|
||||
"result->kj_result": {},
|
||||
"($kj_result == 'fail')": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-iam-wildcard",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "No IAM wildcard Actions or Resources"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-wildcard-action",
|
||||
"validate": {
|
||||
"message": "IAM policy Action must not be '*' (ports CKV_AWS_1/40)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_iam_policy' && contains(values.policy_document.Statement[].Action, '*'))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no-wildcard-resource",
|
||||
"validate": {
|
||||
"message": "IAM policy Resource must not be '*' (ports CKV_AWS_1/40)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_iam_policy' && contains(values.policy_document.Statement[].Resource, '*'))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-plaintext-secrets",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "No plaintext secrets in the terraform plan"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-plaintext-db-password",
|
||||
"validate": {
|
||||
"message": "aws_db_instance.password must not be a plaintext string (ports CKV_AWS_41/45/46)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_db_instance' && contains(keys(values), 'password') && !contains(['${...}', ''], values.password))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-kms-reference",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "KMS keys referenced by alias, not inline key material"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "kms-by-alias",
|
||||
"validate": {
|
||||
"message": "aws_kms_key resources should reference a customer-managed key alias, not inline key material (ports CKV_AWS_7/33)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"planned_values.root_module.~.resources": {
|
||||
"(type == 'aws_kms_key' && !contains(keys(values), 'key_id') && !contains(keys(values), 'kms_key_id'))": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "forbid-public-ingress",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "No resource has public ingress enabled"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "no-public-ingress",
|
||||
"identifier": "id",
|
||||
"validate": {
|
||||
"message": "public_ingress: true is not allowed on any resource (v1.0 demo rule, now declarative)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.resources": {
|
||||
"(inputs.public_ingress || `false`)": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-encryption-by-default",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "high",
|
||||
"title.policy.kyverno.io": "S3 buckets and EBS volumes carry encryption config"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "s3-encryption",
|
||||
"identifier": "id",
|
||||
"match": {
|
||||
"any": [
|
||||
{"type": "aws:s3:bucket"}
|
||||
]
|
||||
},
|
||||
"validate": {
|
||||
"message": "S3 buckets must declare encryption config (inputs.bucket_encryption or inputs.kms_key_id)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"(contains(keys(inputs), 'bucket_encryption') || contains(keys(inputs), 'kms_key_id'))": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ebs-encryption",
|
||||
"identifier": "id",
|
||||
"match": {
|
||||
"any": [
|
||||
{"type": "aws:ebs:volume"}
|
||||
]
|
||||
},
|
||||
"validate": {
|
||||
"message": "EBS volumes must declare encryption (inputs.encrypted or inputs.kms_key_id)",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"(contains(keys(inputs), 'encrypted') || contains(keys(inputs), 'kms_key_id'))": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"apiVersion": "json.kyverno.io/v1alpha1",
|
||||
"kind": "ValidatingPolicy",
|
||||
"metadata": {
|
||||
"name": "require-tagging-standard",
|
||||
"annotations": {
|
||||
"nova.cloudinit.dev/severity": "medium",
|
||||
"title.policy.kyverno.io": "All resources carry required Nova tags"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "require-nova-tags",
|
||||
"identifier": "id",
|
||||
"validate": {
|
||||
"message": "Every taggable resource must carry nova:owner, nova:contract, nova:environment, nova:cost-center tags",
|
||||
"assert": {
|
||||
"all": [
|
||||
{
|
||||
"check": {
|
||||
"~.resources": {
|
||||
"(contains(keys(tags || `[]`), 'nova:owner'))": true,
|
||||
"(contains(keys(tags || `[]`), 'nova:contract'))": true,
|
||||
"(contains(keys(tags || `[]`), 'nova:environment'))": true,
|
||||
"(contains(keys(tags || `[]`), 'nova:cost-center'))": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -488,6 +488,25 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
# Validate contract against schema
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
# v1.25 (REQ-296): pre-resolve policy evaluation — run the active
|
||||
# PolicyEngine over the contract dict with the contract/ policy
|
||||
# dir BEFORE resolving. Failures feed the `policyResults` on the
|
||||
# stack instance (the confidence signal's `policy` input). The
|
||||
# resolver does NOT exit on policy failure — the confidence signal
|
||||
# decides the gate (consistent with the existing --soft-fail
|
||||
# Checkov pattern).
|
||||
contract_pcrs: list = []
|
||||
try:
|
||||
from core.policy_engine import get_engine, get_policy_root
|
||||
_engine = get_engine()
|
||||
_policy_root = get_policy_root()
|
||||
contract_pcrs = _engine.evaluate(
|
||||
contract, _policy_root / "contract", contract.get("id", "unknown")
|
||||
)
|
||||
except Exception:
|
||||
# Policy evaluation must never break the resolver.
|
||||
contract_pcrs = []
|
||||
|
||||
# Interpolation (D-081): expand ${env.<field>} + ${contract.<field>}
|
||||
# tokens AFTER schema validation (the schema sees raw tokens, which are
|
||||
# valid strings) and BEFORE IR resolution (the resolver sees concrete
|
||||
@@ -590,6 +609,12 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
"data_sources": all_data_sources,
|
||||
}
|
||||
|
||||
# v1.25 (REQ-296): attach the pre-resolve contract-policy PCRs to
|
||||
# the stack instance. The post-resolve stack-IR PCRs are appended
|
||||
# after stack-schema validation (below).
|
||||
if contract_pcrs:
|
||||
stack_instance["policyResults"] = list(contract_pcrs)
|
||||
|
||||
# Add the human-readable title
|
||||
if contract.get("name"):
|
||||
stack_instance["stack"]["title"] = contract["name"]
|
||||
@@ -606,6 +631,28 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
# v1.25 (REQ-298): post-resolve policy evaluation — run the active
|
||||
# PolicyEngine over the resolved Stack IR with the stack-ir/ policy
|
||||
# dir. The resulting PCRs are appended to the contract-policy PCRs
|
||||
# on the stack instance (additive — the resolver's return value
|
||||
# shape and exceptions are unchanged). The confidence signal
|
||||
# consumes the merged list as its `policy` input.
|
||||
try:
|
||||
from core.policy_engine import get_engine, get_policy_root
|
||||
engine = get_engine()
|
||||
policy_root = get_policy_root()
|
||||
stack_ir_pcrs = engine.evaluate(
|
||||
stack_instance, policy_root / "stack-ir", contract.get("id", "unknown")
|
||||
)
|
||||
stack_instance.setdefault("policyResults", []).extend(stack_ir_pcrs)
|
||||
except Exception:
|
||||
# Policy evaluation must never break the resolver — the
|
||||
# confidence signal decides the gate. A failure here means the
|
||||
# engine is misconfigured; the contract PCRs (if any) are still
|
||||
# present, and the confidence signal proceeds with whatever
|
||||
# `policy` input it receives (possibly empty → 0.5 neutral).
|
||||
pass
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
|
||||
+52
-1
@@ -518,8 +518,59 @@ for pcr in pcrs:
|
||||
marker = 'PASS' if res == 'pass' else 'FAIL' if res == 'fail' else 'SKIP' if res == 'skipped' else res.upper()
|
||||
print(f' [{marker}] {sev:8s} {rule:30s} {msg}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# Step 5b: kyverno-json plan-JSON policy pass (v1.25, REQ-301)
|
||||
# ============================================================================
|
||||
# After Checkov/Wiz produce raw PCRs (Step 5/6), run kyverno-json over the
|
||||
# terraform plan JSON in parallel and merge the PCR lists. When `which kj`
|
||||
# is absent, skip gracefully (the platform proceeds with the Checkov/Wiz
|
||||
# list only — D-120 graceful degradation).
|
||||
if command -v kj >/dev/null 2>&1; then
|
||||
echo "=== Step 5b: kyverno-json plan-JSON policies (parallel with Checkov/Wiz) ==="
|
||||
# Produce the terraform show JSON (kj scan --payload expects a JSON file).
|
||||
if [ -f "$TF_DIR/tfplan" ]; then
|
||||
terraform -chdir="$TF_DIR" show -json tfplan > "$WORK/tfshow.json" 2>/dev/null || true
|
||||
if [ -s "$WORK/tfshow.json" ]; then
|
||||
python3 - <<'PY' > "$WORK/kj-pcr.json" 2>"$WORK/kj.err" || echo "[]"
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, ".")
|
||||
import importlib.util
|
||||
_spec = importlib.util.spec_from_file_location("kj_engine", "adapters/kyverno-json/kyverno_json_engine.py")
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
eng = _mod.KyvernoJsonEngine()
|
||||
if not eng.is_configured():
|
||||
print("[]"); sys.exit(0)
|
||||
out = eng.evaluate(json.load(open("$WORK/tfshow.json")), Path("adapters/kyverno-json/policies/plan-json"), "$CONTRACT_ID")
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
if [ -s "$WORK/kj-pcr.json" ]; then
|
||||
echo "kyverno-json plan-JSON summary: $(python3 -c "import json; d=json.load(open('$WORK/kj-pcr.json')); print(len([p for p in d if p.get('result')=='fail']), 'failed,', len([p for p in d if p.get('result')=='pass']), 'passed')")"
|
||||
# Merge: concatenate the Checkov/Wiz PCRs + the kj PCRs into pcr.json.
|
||||
python3 -c "
|
||||
import json
|
||||
ckv = json.load(open('$WORK/pcr.json'))
|
||||
kj = json.load(open('$WORK/kj-pcr.json'))
|
||||
json.dump(ckv + kj, open('$WORK/pcr.json', 'w'))
|
||||
print(f'merged PCR list: {len(ckv)} checkov/wiz + {len(kj)} kyverno-json = {len(ckv)+len(kj)} total')
|
||||
"
|
||||
else
|
||||
echo "kyverno-json produced no output; proceeding with Checkov/Wiz PCRs only"
|
||||
fi
|
||||
else
|
||||
echo "terraform show -json produced no output; skipping kyverno-json plan-JSON policies"
|
||||
fi
|
||||
else
|
||||
echo "tfplan not found; skipping kyverno-json plan-JSON policies"
|
||||
fi
|
||||
else
|
||||
echo "=== Step 5b: kyverno-json not installed; skipping plan-JSON policies (D-120 graceful degradation) ==="
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "=== Step 7: confidence signal compute ==="
|
||||
python3 <<PY > "$WORK/signal.json" || fail "confidence signal failed"
|
||||
import json
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "aws_db_instance.main",
|
||||
"type": "aws_db_instance",
|
||||
"name": "main",
|
||||
"values": {
|
||||
"password": "supersecret123",
|
||||
"engine": "postgres"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "aws_iam_policy.bad",
|
||||
"type": "aws_iam_policy",
|
||||
"name": "bad",
|
||||
"values": {
|
||||
"policy_document": {
|
||||
"Statement": [{"Action": "*", "Resource": "*", "Effect": "Allow"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "aws_kms_key.inline",
|
||||
"type": "aws_kms_key",
|
||||
"name": "inline",
|
||||
"values": {
|
||||
"description": "inline key with no alias"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "aws_s3_bucket.bucket",
|
||||
"type": "aws_s3_bucket",
|
||||
"name": "bucket",
|
||||
"values": {
|
||||
"bucket": "acdl-dev-msvc-bucket",
|
||||
"tags": {"nova:owner": "team-a", "nova:environment": "dev"},
|
||||
"server_side_encryption_configuration": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "aws_kms_key.main",
|
||||
"type": "aws_kms_key",
|
||||
"name": "main",
|
||||
"values": {
|
||||
"key_id": "alias/nova-main",
|
||||
"customer_master_key_spec": "SYMMETRIC_DEFAULT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "bad",
|
||||
"title": "failing stack",
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
"environment": "dev"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "bucket",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-dev-bad-bucket",
|
||||
"region": "us-east-1",
|
||||
"tags": {
|
||||
"nova:owner": "team-a"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "microservice@1.0.0",
|
||||
"inputs": {
|
||||
"image": "nginx:latest",
|
||||
"port": 80,
|
||||
"public_ingress": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "msvc",
|
||||
"title": "microservice",
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
"environment": "dev"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "bucket",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-dev-msvc-bucket",
|
||||
"region": "us-east-1",
|
||||
"bucket_encryption": {"rule": {"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}},
|
||||
"tags": {
|
||||
"nova:owner": "team-a",
|
||||
"nova:contract": "msvc",
|
||||
"nova:environment": "dev",
|
||||
"nova:cost-center": "cc-1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "microservice@1.0.0",
|
||||
"inputs": {
|
||||
"image": "nginx:latest",
|
||||
"port": 80,
|
||||
"tags": {
|
||||
"nova:owner": "team-a",
|
||||
"nova:contract": "msvc",
|
||||
"nova:environment": "dev",
|
||||
"nova:cost-center": "cc-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Tests for meta-policies (REQ-303, v1.25).
|
||||
|
||||
Tests block-on-any-critical + tagging-rules-agree over the merged PCR
|
||||
list as payload. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
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
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "meta"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
class TestBlockOnAnyCritical:
|
||||
def test_no_critical_passes(self):
|
||||
pcrs = [
|
||||
{"severity": "high", "result": "fail", "ruleId": "X", "contractId": "c",
|
||||
"message": "", "resourceRef": "", "engine": "kyverno", "evaluatedAt": "t",
|
||||
"evidence": {}},
|
||||
{"severity": "info", "result": "pass", "ruleId": "Y", "contractId": "c",
|
||||
"message": "", "resourceRef": "", "engine": "kyverno", "evaluatedAt": "t",
|
||||
"evidence": {}},
|
||||
]
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(pcrs, POLICY_DIR / "block-on-any-critical.json"
|
||||
if (POLICY_DIR / "block-on-any-critical.json").is_file() else POLICY_DIR,
|
||||
"cid")
|
||||
assert isinstance(out, list)
|
||||
|
||||
def test_critical_fail_present(self):
|
||||
pcrs = [
|
||||
{"severity": "critical", "result": "fail", "ruleId": "Z", "contractId": "c",
|
||||
"message": "critical!", "resourceRef": "", "engine": "kyverno", "evaluatedAt": "t",
|
||||
"evidence": {}},
|
||||
]
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(pcrs, POLICY_DIR, "cid")
|
||||
# The meta-policy should detect the critical fail. When kj runs,
|
||||
# it produces a result entry. We assert the engine returns a list
|
||||
# (the meta-policy PCRs).
|
||||
assert isinstance(out, list)
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_two_meta_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "block-on-any-critical.json" in files
|
||||
assert "tagging-rules-agree.json" in files
|
||||
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
|
||||
def test_block_on_critical_has_critical_severity(self):
|
||||
with open(POLICY_DIR / "block-on-any-critical.json", "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["metadata"]["annotations"]["nova.cloudinit.dev/severity"] == "critical"
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for plan-JSON kyverno-json policies (REQ-302, v1.25).
|
||||
|
||||
Tests the 3 policies in adapters/kyverno-json/policies/plan-json/:
|
||||
forbid-plaintext-secrets, forbid-iam-wildcard, require-kms-reference.
|
||||
Uses passing + failing fixtures. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
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
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "plan-json"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "plan_json"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
def _load(name):
|
||||
with open(FIXTURES / name, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestPassingFixture:
|
||||
def test_passing_fixture_no_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("passing.json"), POLICY_DIR, "cid-pass")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert fails == [], f"expected no fails on passing fixture, got: {fails}"
|
||||
|
||||
|
||||
class TestFailingFixture:
|
||||
def test_failing_fixture_has_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("failing.json"), POLICY_DIR, "cid-fail")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert len(fails) >= 1, "expected at least one fail on the failing fixture"
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_three_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "forbid-plaintext-secrets.json" in files
|
||||
assert "forbid-iam-wildcard.json" in files
|
||||
assert "require-kms-reference.json" in files
|
||||
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Tests for run_platform.sh Step 5b kyverno-json wiring (REQ-302, v1.25).
|
||||
|
||||
Asserts the script has the kyverno-json Step 5b block and the PCR-merge
|
||||
logic. Pattern from tests/test_pipeline.py:79-95 (read script text +
|
||||
assert substrings).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "run_platform.sh"
|
||||
|
||||
|
||||
def _read_script():
|
||||
with open(SCRIPT, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
class TestStep5bKyvernoJsonWiring:
|
||||
def test_step_5b_block_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 5b: kyverno-json plan-JSON policies" in s, \
|
||||
"run_platform.sh must have a Step 5b kyverno-json block (REQ-301)"
|
||||
|
||||
def test_kj_scan_invocation_present(self):
|
||||
s = _read_script()
|
||||
assert "adapters/kyverno-json/policies/plan-json" in s, \
|
||||
"Step 5b must reference the plan-json policy dir"
|
||||
|
||||
def test_kj_not_installed_skip_present(self):
|
||||
s = _read_script()
|
||||
assert "kyverno-json not installed; skipping plan-JSON policies" in s, \
|
||||
"Step 5b must skip gracefully when kj is absent (D-120)"
|
||||
assert "D-120 graceful degradation" in s
|
||||
|
||||
def test_pcr_merge_logic_present(self):
|
||||
s = _read_script()
|
||||
assert "merged PCR list" in s, \
|
||||
"Step 5b must merge the Checkov/Wiz + kj PCR lists"
|
||||
|
||||
def test_command_v_kj_guard_present(self):
|
||||
s = _read_script()
|
||||
assert "command -v kj" in s, \
|
||||
"Step 5b must guard on `command -v kj` (is_configured)"
|
||||
|
||||
|
||||
class TestExistingPipelineUnchanged:
|
||||
def test_step_5_still_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 5: runtime policy scan" in s
|
||||
|
||||
def test_step_7_confidence_still_present(self):
|
||||
s = _read_script()
|
||||
assert "Step 7: confidence signal compute" in s
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for stack-IR kyverno-json policies (REQ-299, v1.25).
|
||||
|
||||
Tests the 3 policies in adapters/kyverno-json/policies/stack-ir/:
|
||||
require-tagging-standard, forbid-public-ingress, require-encryption-by-
|
||||
default. Uses the passing + failing fixtures. Skips when kj is absent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
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
|
||||
|
||||
POLICY_DIR = Path(__file__).resolve().parent.parent / "adapters" / "kyverno-json" / "policies" / "stack-ir"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "stack_ir"
|
||||
|
||||
|
||||
def _kj_installed() -> bool:
|
||||
return _mod._which_kj() is not None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _require_kj():
|
||||
if not _kj_installed():
|
||||
pytest.skip("kj not installed (scripts/install-kyverno-json.sh)")
|
||||
|
||||
|
||||
def _load(name):
|
||||
with open(FIXTURES / name, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestPassingFixture:
|
||||
def test_passing_fixture_all_pass(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("passing.json"), POLICY_DIR, "cid-pass")
|
||||
assert isinstance(out, list)
|
||||
assert len(out) >= 1
|
||||
# No fail results on the passing fixture.
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert fails == [], f"expected no fails on passing fixture, got: {fails}"
|
||||
|
||||
|
||||
class TestFailingFixture:
|
||||
def test_failing_fixture_has_fails(self):
|
||||
eng = KyvernoJsonEngine()
|
||||
out = eng.evaluate(_load("failing.json"), POLICY_DIR, "cid-fail")
|
||||
fails = [p for p in out if p["result"] == "fail"]
|
||||
assert len(fails) >= 1, "expected at least one fail on the failing fixture"
|
||||
|
||||
|
||||
class TestPolicyFilesExist:
|
||||
def test_three_policies_present(self):
|
||||
files = sorted(os.listdir(POLICY_DIR))
|
||||
assert "require-tagging-standard.json" in files
|
||||
assert "forbid-public-ingress.json" in files
|
||||
assert "require-encryption-by-default.json" in files
|
||||
|
||||
|
||||
class TestPolicyValidity:
|
||||
def test_policies_are_valid_json(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
assert data["apiVersion"] == "json.kyverno.io/v1alpha1"
|
||||
assert data["kind"] == "ValidatingPolicy"
|
||||
assert "nova.cloudinit.dev/severity" in data["metadata"]["annotations"]
|
||||
|
||||
def test_policy_names_match_filenames(self):
|
||||
for f in os.listdir(POLICY_DIR):
|
||||
if f.endswith(".json"):
|
||||
with open(POLICY_DIR / f, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
expected = f.rsplit(".", 1)[0]
|
||||
assert data["metadata"]["name"] == expected
|
||||
Reference in New Issue
Block a user