85c500e45a
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---
222 lines
9.3 KiB
Bash
Executable File
222 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# scripts/run_postapply.sh — post-Terraform steps for the Nova platform pipeline.
|
||
#
|
||
# Performs steps 5–9 of run_platform.sh (after terraform apply/destroy):
|
||
# 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
|
||
# 9. Publish outputs to SSM + GitHub PR comment
|
||
# 9b. Uptime monitoring sub-deploy (sourced from run_uptime.sh)
|
||
#
|
||
# Expects the work dir ($NOVA_WORK_DIR) to already contain:
|
||
# - tf/*.tf (from run_codegen.sh)
|
||
# - stack.json (from run_codegen.sh)
|
||
# And terraform to have already run (init/validate/plan/apply) in $WORK/tf/.
|
||
#
|
||
# Usage:
|
||
# run_postapply.sh <contract.yml> [--environment <name>] [--quiet] [--deploy-uptime]
|
||
#
|
||
set -euo pipefail
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
CALLER_CWD="$(pwd)"
|
||
cd "$ROOT"
|
||
|
||
QUIET=0
|
||
DEPLOY_UPTIME=0
|
||
ENVIRONMENT_OVERRIDE=""
|
||
CONTRACT=""
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--quiet) QUIET=1; shift ;;
|
||
--deploy-uptime) DEPLOY_UPTIME=1; shift ;;
|
||
--environment) shift; ENVIRONMENT_OVERRIDE="$1"; shift ;;
|
||
--environment=*) ENVIRONMENT_OVERRIDE="${1#--environment=}"; shift ;;
|
||
-h|--help)
|
||
echo "Usage: run_postapply.sh <contract.yml> [--environment <name>] [--quiet] [--deploy-uptime]"
|
||
exit 0 ;;
|
||
*) CONTRACT="$1"; shift ;;
|
||
esac
|
||
done
|
||
|
||
[ -n "$CONTRACT" ] || { echo "FAIL: no contract file specified" >&2; exit 1; }
|
||
if ! [[ "$CONTRACT" = /* ]]; then
|
||
CONTRACT="$CALLER_CWD/$CONTRACT"
|
||
fi
|
||
[ -f "$CONTRACT" ] || { echo "FAIL: contract not found: $CONTRACT" >&2; exit 1; }
|
||
|
||
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
|
||
export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE"
|
||
fi
|
||
|
||
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}"
|
||
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
|
||
TF_DIR="$WORK/tf"
|
||
|
||
[ -d "$TF_DIR" ] || { echo "FAIL: work dir $TF_DIR not found (run run_codegen.sh first)" >&2; exit 1; }
|
||
|
||
stream() {
|
||
local log="$1"; shift
|
||
if [ "$QUIET" = "1" ]; then
|
||
"$@" > "$log" 2>&1
|
||
else
|
||
"$@" 2>&1 | tee "$log"
|
||
fi
|
||
}
|
||
|
||
run_hitl_gate() {
|
||
local _cid="$1" _env="$2" _ctx="$3"
|
||
if [ "$_env" = "dev" ]; then
|
||
echo "Environment is $_env — autonomous (no HITL gate)."
|
||
return 0
|
||
fi
|
||
echo "Environment is $_env — HITL attestation gate required$_ctx."
|
||
local _approver="${GITHUB_ACTOR:-${FORGE_ACTOR:-}}"
|
||
if [ -z "$_approver" ]; then
|
||
echo "WARNING: no approver identity (GITHUB_ACTOR/FORGE_ACTOR unset)" >&2
|
||
echo " the gate would block in a real CI run. Passing for local." >&2
|
||
fi
|
||
python3 -c "
|
||
import os, sys
|
||
sys.path.insert(0, '.')
|
||
from core.hitl_gates import attest
|
||
from core import env as _envhelper
|
||
ok, reason = attest('$_cid', '$_env', '$_approver')
|
||
if ok:
|
||
print(f'HITL: {reason}')
|
||
else:
|
||
print(f'HITL BLOCK: {reason}', file=sys.stderr)
|
||
sys.exit(1)
|
||
" || { echo "FAIL: HITL gate blocked" >&2; return 1; }
|
||
}
|
||
|
||
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
|
||
RUNTIME_SCAN_ENGINE="checkov-plan"
|
||
fi
|
||
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'))
|
||
print(f'PolicyCheckResult: {len(pcrs)} record(s)')
|
||
print()
|
||
for pcr in pcrs:
|
||
sev = pcr.get('severity', 'info')
|
||
res = pcr.get('result', 'unknown')
|
||
rule = pcr.get('ruleId', 'unknown')
|
||
msg = pcr.get('message', '')
|
||
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 ""
|
||
echo "=== Step 7: confidence signal compute ==="
|
||
python3 <<PY > "$WORK/signal.json" || { echo "FAIL: confidence signal failed" >&2; exit 1; }
|
||
import json
|
||
import core.confidence_signal as c
|
||
pcr = json.load(open("$WORK/pcr.json"))
|
||
inputs = {
|
||
"policy": pcr,
|
||
"validation": {"schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True},
|
||
"freshness": {"age_days": 0, "max_age_days": 7},
|
||
"source": {"submitter": "consumer", "commit_sha": "consumer-sha", "signed": False},
|
||
"history": {"prior_rollbacks": 0, "prior_policy_fails": 0},
|
||
"nfrs": {"conformance": None},
|
||
}
|
||
sig = c.compute("$CONTRACT_ID", "dev", inputs)
|
||
print(json.dumps({"score": sig.score, "band": sig.band, "perInput": sig.perInput, "reasonCodes": sig.reasonCodes}, indent=2))
|
||
PY
|
||
BAND=$(python3 -c "import json; print(json.load(open('$WORK/signal.json'))['band'])")
|
||
SCORE=$(python3 -c "import json; print(round(json.load(open('$WORK/signal.json'))['score'],3))")
|
||
echo "confidence: score=$SCORE band=$BAND"
|
||
[ "$BAND" = "pass" ] || { echo "FAIL: confidence band is $BAND, expected pass for dev" >&2; exit 1; }
|
||
|
||
echo ""
|
||
echo "=== Step 7b: HITL attestation gate (qa/prod/dr only) ==="
|
||
RESOLVED_ENV=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).get('environment','dev'))" 2>/dev/null || echo "dev")
|
||
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
|
||
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
|
||
fi
|
||
run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" "" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
|
||
|
||
echo ""
|
||
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
|
||
STACK_NAME=$(python3 -c "import json; print(json.load(open('$WORK/stack.json'))['stack']['name'])")
|
||
python3 <<PY > "$WORK/event.json" || { echo "FAIL: event build failed" >&2; exit 1; }
|
||
import json, datetime
|
||
sig = json.load(open("$WORK/signal.json"))
|
||
event = {
|
||
"contractId": "$CONTRACT_ID",
|
||
"eventType": "CONFIDENCE_COMPUTED",
|
||
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||
"environment": "dev",
|
||
"stack": "$STACK_NAME",
|
||
"score": sig["score"],
|
||
"band": sig["band"],
|
||
"prev_event_hash": "GENESIS",
|
||
}
|
||
print(json.dumps(event, indent=2))
|
||
PY
|
||
python3 core/outbox_writer.py "$WORK/event.json" > "$WORK/outbox_item.json" || { echo "FAIL: outbox write failed" >&2; exit 1; }
|
||
echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.json')); print('contractId=', d['contractId'], 'hash=', d['hash'][:16]+'...')")"
|
||
|
||
echo ""
|
||
echo "=== Step 9: publish outputs to SSM + GitHub PR comment ==="
|
||
cd "$TF_DIR"
|
||
TF_OUTPUTS=$(terraform output -json 2>/dev/null || echo "{}")
|
||
cd "$ROOT"
|
||
python3 <<PY > "$WORK/outputs_step.json" 2>/dev/null || true
|
||
import json, sys
|
||
sys.path.insert(0, "$ROOT")
|
||
from core.output_publisher import publish_to_ssm, format_comment, post_github_comment
|
||
tf_raw = json.loads('''$TF_OUTPUTS''')
|
||
outputs = {k: v.get("value") if isinstance(v, dict) else v for k, v in tf_raw.items()}
|
||
ssm_results = publish_to_ssm(outputs, "dev", "$CONTRACT_ID")
|
||
comment = format_comment(outputs, "dev", "$CONTRACT_ID", ssm_results)
|
||
posted = post_github_comment(comment)
|
||
print(json.dumps({"ssm": ssm_results, "posted": posted, "comment": comment}))
|
||
PY
|
||
if [ -f "$WORK/outputs_step.json" ]; then
|
||
echo "outputs published to SSM: $(python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(len([v for v in d.get('ssm',{}).values() if v]), 'parameters')" 2>/dev/null || echo "done")"
|
||
if [ "$QUIET" = "0" ]; then
|
||
python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(d.get('comment',''))" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
# Uptime monitoring: sourced from run_uptime.sh
|
||
source "$ROOT/scripts/run_uptime.sh"
|
||
|
||
echo ""
|
||
echo "=== POST-APPLY OK ==="
|
||
echo "Checkov(static, pre-plan) → Wiz-or-Checkov(plan) → confidence ($BAND) → outbox → outputs → uptime"
|