#!/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): # 5. Checkov policy scan on the emitted main.tf # 6. Checkov 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 [--environment ] [--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 [--environment ] [--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: 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" 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" 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; } 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 < "$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 < "$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 < "$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 → confidence ($BAND) → outbox → outputs → uptime"