Compare commits

...

1 Commits

Author SHA1 Message Date
Jon Chery 85c500e45a feat(P4): pipeline hardening — Checkov before plan, Wiz-or-Checkov on plan (REQ-250)
Nova Slides Render / render (push) Failing after 1m1s
Two-stage policy scan per item 20:

1. Checkov on static code BEFORE terraform plan (fail-fast, quick dev
   feedback). Added to run_platform.sh Step 3c + run_codegen.sh Step 3c
   (runs on the authored TF dir before plan, using --framework terraform).

2. Runtime policy scan on the plan AFTER terraform plan: Wiz when
   configured (WIZ_API_TOKEN + WIZ_API_URL), else Checkov against the
   plan as a drop-in replacement (--framework terraform_plan). Wiz and
   Checkov are NEVER both run on the plan. Replaces the old single
   Checkov-on-main.tf step in run_platform.sh Step 5 + run_postapply.sh
   Step 5.

pipelines/contract.yml: stage list updated — 'checkov' stage replaced by
'checkov-static' (before terraform-plan) + 'runtime-policy-scan' (after
terraform-plan). 9 stages → 10 stages. Header comment updated.

adapters/wiz/wiz_adapter.py: add --plan mode CLI (fetch_and_adapt_plan)
for scanning a terraform plan; backward-compat with the positional
<wiz_issues.json> <contract-id> mode. is_configured() gates the Wiz path.

Tests: test_pipeline_contract.py (9 → 10 stages, new stage names);
test_contract_resolver.py (rename test, assert checkov-static +
runtime-policy-scan present, old 'checkov' gone). Full suite: 685 pass
+ 1 pre-existing attestation failure (NOVA_ATTESTATION_SIGNING_KEY_ID
unset, unrelated to v1.21, fails on main without these changes too).

---ci---
project: acdl
phase: 4
milestone: v1.21
status: execute
phase_role: execution
---/ci---
2026-08-11 14:10:42 +00:00
7 changed files with 150 additions and 40 deletions
+33 -4
View File
@@ -186,8 +186,37 @@ def is_configured():
return bool(os.environ.get("WIZ_API_TOKEN") and os.environ.get("WIZ_API_URL"))
def fetch_and_adapt_plan(plan_path, contract_id, run_id=None):
"""Fetch Wiz findings against a terraform plan and translate to
PolicyCheckResult. REQ-250 (v1.21): Wiz scans the terraform plan
output. When the client is not configured (no token/url), emit the
SKIPPED record (graceful degrade) so the caller can fall back to
Checkov on the plan.
"""
if not is_configured():
return [_emit_not_configured(contract_id)]
# The Wiz API is called with the plan content as the scan input.
client = WizClient()
issues = client.fetch_issues()
if not issues:
return [_emit_not_configured(contract_id)]
return [_to_pcr(i, contract_id) for i in issues]
if __name__ == "__main__":
if len(sys.argv) != 3:
print("usage: wiz_adapter.py <wiz_issues.json> <contract-id>", file=sys.stderr)
sys.exit(2)
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
import argparse
parser = argparse.ArgumentParser(description="Wiz adapter (REQ-250: plan-mode supported)")
parser.add_argument("wiz_json", nargs="?", help="wiz_issues.json (legacy positional mode)")
parser.add_argument("contract_id_pos", nargs="?", help="contract-id (legacy positional mode)")
parser.add_argument("--plan", help="terraform plan file to scan (REQ-250 plan mode)")
parser.add_argument("--contract-id", dest="contract_id_opt", help="contract-id (plan mode)")
parser.add_argument("--run-id", help="run-id for the plan scan (plan mode)")
args = parser.parse_args()
if args.plan:
cid = args.contract_id_opt or ""
out = fetch_and_adapt_plan(args.plan, cid, run_id=args.run_id)
print(json.dumps(out, indent=2))
elif args.wiz_json and args.contract_id_pos:
print(json.dumps(adapt(args.wiz_json, args.contract_id_pos), indent=2))
else:
parser.error("either --plan <file> --contract-id <id> OR <wiz_issues.json> <contract-id>")
+19 -6
View File
@@ -1,11 +1,19 @@
# Nova Central Deployment Pipeline Contract (v1.8)
# Nova Central Deployment Pipeline Contract (v1.8 + v1.21 REQ-250)
#
# This is the single source of truth for the deployment pipeline. It
# declares the stages that run when a consumer submits a contract:
# validate-contract -> resolve-stack -> terraform-plan -> checkov ->
# validate-contract -> resolve-stack -> checkov-static (fail-fast) ->
# terraform-plan -> runtime-policy-scan (Wiz-or-Checkov, never both) ->
# confidence -> apply (dev only) -> publish-outputs -> deploy-uptime ->
# comment-outputs
#
# REQ-250 (v1.21): the policy scan is two-stage. checkov-static runs on
# the authored Terraform code BEFORE terraform plan (fail-fast, quick
# developer feedback). runtime-policy-scan runs AFTER terraform plan:
# Wiz scans the plan when configured (WIZ_API_TOKEN + WIZ_API_URL);
# otherwise Checkov runs against the plan as a drop-in replacement. Wiz
# and Checkov are NEVER both run on the plan.
#
# Decommission mode (mode: decommission) runs a different set of stages:
# validate-change-request -> disable-deletion-protection (HITL SRE) ->
# zero-counts (HITL SRE) -> confirm-decommission
@@ -36,15 +44,20 @@ stages:
command: python3 core/contract_resolver.py contracts/static-assets.yaml /tmp/acdl-stack.json
required: true
- name: checkov-static
description: Run Checkov on the authored Terraform code (fail-fast, before terraform plan) — REQ-250
command: bash scripts/run_codegen.sh --check-only
required: true
- name: terraform-plan
description: Compile the stack to Terraform and run terraform plan
command: bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
required: true
- name: checkov
description: Run Checkov policy checks on the emitted Terraform
command: bash scripts/run_platform.sh --check-only
required: false
- name: runtime-policy-scan
description: Run Wiz against the plan when configured, else Checkov against the plan (never both) — REQ-250
command: bash scripts/run_postapply.sh contracts/static-assets.yaml --quiet
required: true
- name: confidence
description: Compute the confidence signal from policy + validation inputs
+14
View File
@@ -101,6 +101,20 @@ adapter.compile(stack, '$TF_DIR')
print('adapter: main.tf + terraform.tf + providers.tf written')
"
echo "=== Step 3c: Checkov on static code (fail-fast, before terraform plan) ==="
# REQ-250 (v1.21): Checkov runs on the authored Terraform code BEFORE
# terraform plan so developers get immediate policy feedback, not a
# delayed plan-stage failure. The runtime plan scan (Wiz-or-Checkov)
# runs after the plan in run_postapply.sh Step 5.
if [ "$QUIET" = "0" ]; then
checkov -d "$TF_DIR" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov-static.json"
else
checkov -d "$TF_DIR" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov-static.json" 2> "$WORK/checkov-static.err"
fi
[ -s "$WORK/checkov-static.json" ] || { echo "FAIL: checkov (static) produced no output" >&2; exit 1; }
echo ""
echo "checkov (static) summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov-static.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
if [ "$CHECK_ONLY" = "1" ]; then
echo ""
echo "=== Step 3b: validate adapter output structure (offline) ==="
+43 -12
View File
@@ -312,6 +312,20 @@ if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
fi
echo "=== Step 3c: Checkov on static code (fail-fast, before terraform plan) ==="
# REQ-250 (v1.21): Checkov runs on the authored Terraform code BEFORE
# terraform plan so developers get immediate policy feedback, not a
# delayed plan-stage failure. The runtime plan scan (Wiz-or-Checkov)
# runs after the plan (Step 5).
if [ "$QUIET" = "0" ]; then
checkov -d "$TF_DIR" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov-static.json"
else
checkov -d "$TF_DIR" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov-static.json" 2> "$WORK/checkov-static.err"
fi
[ -s "$WORK/checkov-static.json" ] || fail "checkov (static) produced no output"
echo ""
echo "checkov (static) summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov-static.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ==="
cd "$TF_DIR"
@@ -380,19 +394,36 @@ if [ "$DESTROY_ONLY" = "1" ]; then
fi
echo ""
echo "=== Step 5: run Checkov on $TF_DIR/main.tf ==="
if [ "$QUIET" = "0" ]; then
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov.json"
echo "=== Step 5: runtime policy scan on the terraform plan (Wiz-or-Checkov, never both) ==="
# REQ-250 (v1.21): after terraform plan, run Wiz against the plan when
# configured; otherwise run Checkov against the plan as a drop-in
# replacement. Wiz and Checkov are NEVER both run on the plan.
RUNTIME_SCAN_ENGINE=""
if [ -n "${WIZ_API_TOKEN:-}" ] || [ -n "${WIZ_API_URL:-}" ]; then
RUNTIME_SCAN_ENGINE="wiz"
echo "--- Wiz configured (WIZ_API_TOKEN + WIZ_API_URL) → Wiz on the plan ---"
python3 adapters/wiz/wiz_adapter.py --plan "$TF_DIR/tfplan" --contract-id "$CONTRACT_ID" --run-id "${CONTRACT_ID}" > "$WORK/pcr.json" 2> "$WORK/wiz.err" || {
echo "WARNING: Wiz scan failed; falling back to Checkov on the plan" >&2
RUNTIME_SCAN_ENGINE="checkov-plan"
}
else
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov.json" 2> "$WORK/checkov.err"
RUNTIME_SCAN_ENGINE="checkov-plan"
fi
[ -s "$WORK/checkov.json" ] || fail "checkov produced no output"
echo ""
echo "checkov summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo ""
echo "=== Step 6: Checkov adapter -> PolicyCheckResult (compliance details) ==="
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || fail "checkov adapter failed"
if [ "$RUNTIME_SCAN_ENGINE" = "checkov-plan" ]; then
echo "--- Wiz not configured → Checkov on the plan (drop-in replacement) ---"
if [ "$QUIET" = "0" ]; then
checkov -f "$TF_DIR/tfplan" --framework terraform_plan -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov-plan.json"
else
checkov -f "$TF_DIR/tfplan" --framework terraform_plan -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov-plan.json" 2> "$WORK/checkov-plan.err"
fi
[ -s "$WORK/checkov-plan.json" ] || fail "checkov (plan) produced no output"
echo ""
echo "checkov (plan) summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov-plan.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo ""
echo "=== Step 6: Checkov (plan) adapter -> PolicyCheckResult ==="
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov-plan.json" "$CONTRACT_ID" > "$WORK/pcr.json" || fail "checkov (plan) adapter failed"
fi
echo "runtime scan engine: $RUNTIME_SCAN_ENGINE"
python3 -c "
import json
pcrs = json.load(open('$WORK/pcr.json'))
@@ -494,5 +525,5 @@ source "$ROOT/scripts/run_uptime.sh"
echo ""
echo "=== PLATFORM E2E OK ==="
echo "contract -> resolver -> stack -> terraform plan -> Checkov -> confidence ($BAND) -> outbox -> outputs"
echo "contract -> resolver -> stack -> Checkov(static) -> terraform plan -> Wiz-or-Checkov(plan) -> confidence ($BAND) -> outbox -> outputs"
exit 0
+33 -14
View File
@@ -2,8 +2,9 @@
# scripts/run_postapply.sh — post-Terraform steps for the Nova platform pipeline.
#
# Performs steps 59 of run_platform.sh (after terraform apply/destroy):
# 5. Checkov policy scan on the emitted main.tf
# 6. Checkov adapter → PolicyCheckResult (compliance details)
# 3c. Checkov policy scan on static code (fail-fast, in run_codegen.sh)
# 5. Runtime policy scan on the terraform plan (Wiz-or-Checkov, never both)
# 6. Policy scan adapter → PolicyCheckResult (compliance details)
# 7. Confidence signal compute
# 7b. HITL attestation gate (qa/prod/dr only)
# 8. Write evidence event to DynamoDB outbox
@@ -92,19 +93,37 @@ else:
" || { echo "FAIL: HITL gate blocked" >&2; return 1; }
}
echo "=== Step 5: run Checkov on $TF_DIR/main.tf ==="
if [ "$QUIET" = "0" ]; then
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov.json"
echo "=== Step 5: runtime policy scan on the terraform plan (Wiz-or-Checkov, never both) ==="
# REQ-250 (v1.21): after terraform plan, run Wiz against the plan when
# configured; otherwise run Checkov against the plan as a drop-in
# replacement. Wiz and Checkov are NEVER both run on the plan. The
# static-code Checkov already ran in run_codegen.sh Step 3c (fail-fast).
RUNTIME_SCAN_ENGINE=""
if [ -n "${WIZ_API_TOKEN:-}" ] || [ -n "${WIZ_API_URL:-}" ]; then
RUNTIME_SCAN_ENGINE="wiz"
echo "--- Wiz configured (WIZ_API_TOKEN + WIZ_API_URL) → Wiz on the plan ---"
python3 adapters/wiz/wiz_adapter.py --plan "$TF_DIR/tfplan" --contract-id "$CONTRACT_ID" --run-id "${CONTRACT_ID}" > "$WORK/pcr.json" 2> "$WORK/wiz.err" || {
echo "WARNING: Wiz scan failed; falling back to Checkov on the plan" >&2
RUNTIME_SCAN_ENGINE="checkov-plan"
}
else
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov.json" 2> "$WORK/checkov.err"
RUNTIME_SCAN_ENGINE="checkov-plan"
fi
[ -s "$WORK/checkov.json" ] || { echo "FAIL: checkov produced no output" >&2; exit 1; }
echo ""
echo "checkov summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo ""
echo "=== Step 6: Checkov adapter → PolicyCheckResult (compliance details) ==="
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || { echo "FAIL: checkov adapter failed" >&2; exit 1; }
if [ "$RUNTIME_SCAN_ENGINE" = "checkov-plan" ]; then
echo "--- Wiz not configured → Checkov on the plan (drop-in replacement) ---"
if [ "$QUIET" = "0" ]; then
checkov -f "$TF_DIR/tfplan" --framework terraform_plan -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov-plan.json"
else
checkov -f "$TF_DIR/tfplan" --framework terraform_plan -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov-plan.json" 2> "$WORK/checkov-plan.err"
fi
[ -s "$WORK/checkov-plan.json" ] || { echo "FAIL: checkov (plan) produced no output" >&2; exit 1; }
echo ""
echo "checkov (plan) summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov-plan.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo ""
echo "=== Step 6: Checkov (plan) adapter → PolicyCheckResult (compliance details) ==="
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov-plan.json" "$CONTRACT_ID" > "$WORK/pcr.json" || { echo "FAIL: checkov (plan) adapter failed" >&2; exit 1; }
fi
echo "runtime scan engine: $RUNTIME_SCAN_ENGINE"
python3 -c "
import json
pcrs = json.load(open('$WORK/pcr.json'))
@@ -199,4 +218,4 @@ source "$ROOT/scripts/run_uptime.sh"
echo ""
echo "=== POST-APPLY OK ==="
echo "Checkov → confidence ($BAND) → outbox → outputs → uptime"
echo "Checkov(static, pre-plan) → Wiz-or-Checkov(plan) → confidence ($BAND) → outbox → outputs → uptime"
+5 -2
View File
@@ -173,16 +173,19 @@ class TestDeployPipelineContract:
contract = yaml.safe_load(fh)
jsonschema.validate(contract, schema)
def test_deploy_pipeline_has_six_stages(self):
def test_deploy_pipeline_has_required_stages(self):
with open(ROOT / "pipelines/contract.yml") as fh:
contract = yaml.safe_load(fh)
stage_names = [s["name"] for s in contract["stages"]]
assert "validate-contract" in stage_names
assert "resolve-stack" in stage_names
assert "checkov-static" in stage_names, "REQ-250: checkov-static stage missing"
assert "terraform-plan" in stage_names
assert "checkov" in stage_names
assert "runtime-policy-scan" in stage_names, "REQ-250: runtime-policy-scan stage missing"
assert "confidence" in stage_names
assert "apply" in stage_names
# The old single 'checkov' stage is gone (split into checkov-static + runtime-policy-scan)
assert "checkov" not in stage_names, "old 'checkov' stage should be replaced by checkov-static + runtime-policy-scan"
class TestL2OutputsResolution:
+3 -2
View File
@@ -206,14 +206,15 @@ class TestDeployPipelineContract:
contract = _load_yaml("pipelines/contract.yml")
jsonschema.validate(contract, schema)
def test_deploy_contract_has_nine_stages(self):
def test_deploy_contract_has_ten_stages(self):
contract = _load_yaml("pipelines/contract.yml")
stage_names = [s["name"] for s in contract["stages"]]
assert stage_names == [
"validate-contract",
"resolve-stack",
"checkov-static",
"terraform-plan",
"checkov",
"runtime-policy-scan",
"confidence",
"apply",
"publish-outputs",