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",
|
"stage": "complete",
|
||||||
"milestone": "v1.25",
|
"milestone": "v1.25",
|
||||||
"phase_role": "pre_execution",
|
"phase_role": "execution",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-08-12T16:50:00Z",
|
"updated_at": "2026-08-12T17:15:00Z",
|
||||||
"project": "acdl",
|
"project": "acdl",
|
||||||
"milestone_complete": false,
|
"milestone_complete": false,
|
||||||
"tag_line": "v1.24.x",
|
"tag_line": "v1.24.x",
|
||||||
"tag": "v1.24.0",
|
"tag": "v1.24.2",
|
||||||
"next_tag": "v1.24.1",
|
"next_tag": "v1.24.3",
|
||||||
"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"],
|
|
||||||
"release": {
|
"release": {
|
||||||
"forge": "gitea",
|
"forge": "gitea",
|
||||||
"releases_created": true,
|
"releases_created": true,
|
||||||
"release_id": 640,
|
"release_ids": {"v1.24.0": 640, "v1.24.1": 641, "v1.24.2": 642},
|
||||||
"release_tag": "v1.24.0"
|
"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
|
# Validate contract against schema
|
||||||
jsonschema.validate(contract, contract_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>}
|
# Interpolation (D-081): expand ${env.<field>} + ${contract.<field>}
|
||||||
# tokens AFTER schema validation (the schema sees raw tokens, which are
|
# tokens AFTER schema validation (the schema sees raw tokens, which are
|
||||||
# valid strings) and BEFORE IR resolution (the resolver sees concrete
|
# 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,
|
"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
|
# Add the human-readable title
|
||||||
if contract.get("name"):
|
if contract.get("name"):
|
||||||
stack_instance["stack"]["title"] = contract["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"))
|
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||||
jsonschema.validate(stack_instance, stack_schema)
|
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
|
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()
|
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}')
|
print(f' [{marker}] {sev:8s} {rule:30s} {msg}')
|
||||||
"
|
"
|
||||||
|
|
||||||
echo ""
|
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 ==="
|
echo "=== Step 7: confidence signal compute ==="
|
||||||
python3 <<PY > "$WORK/signal.json" || fail "confidence signal failed"
|
python3 <<PY > "$WORK/signal.json" || fail "confidence signal failed"
|
||||||
import json
|
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