#!/bin/bash # # Governed Terraform Wrapper # ========================== # Enforces plan-before-apply and full audit trail. # Part of Phase 3: Execution Pipeline. # # Usage: # tf-governed.sh plan [--target=] # tf-governed.sh apply [--plan=] # tf-governed.sh destroy [--plan=] # # Rules: # 1. All applies MUST have a corresponding plan artifact # 2. Plans are stored with checksums for verification # 3. All operations are logged to the governance ledger # set -euo pipefail # Configuration GOVERNANCE_DIR="/opt/agent-governance" EVIDENCE_DIR="${GOVERNANCE_DIR}/evidence" PREFLIGHT_DIR="${GOVERNANCE_DIR}/preflight" ARTIFACT_DIR="${EVIDENCE_DIR}/terraform" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # Ensure directories exist mkdir -p "${ARTIFACT_DIR}" log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_ok() { echo -e "${GREEN}[OK]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_block() { echo -e "${RED}[BLOCKED]${NC} $1" } # Generate unique artifact ID generate_artifact_id() { echo "tf-$(date +%Y%m%d-%H%M%S)-$(openssl rand -hex 4)" } # Get agent info from environment or default get_agent_info() { AGENT_ID="${AGENT_ID:-cli-user}" AGENT_TIER="${AGENT_TIER:-1}" } # Run preflight checks run_preflight() { local targets="$1" log_info "Running preflight checks..." if [[ -f "${PREFLIGHT_DIR}/preflight.py" ]]; then cd "${PREFLIGHT_DIR}" python3 preflight.py ${targets} --action terraform --tier "${AGENT_TIER}" --agent-id "${AGENT_ID}" --quiet return $? else log_warn "Preflight system not available, skipping checks" return 0 fi } # Store plan artifact store_plan_artifact() { local plan_file="$1" local artifact_id="$2" local tf_dir="$3" local artifact_path="${ARTIFACT_DIR}/${artifact_id}" mkdir -p "${artifact_path}" # Copy plan binary cp "${plan_file}" "${artifact_path}/tfplan.binary" # Generate plan JSON terraform -chdir="${tf_dir}" show -json "${plan_file}" > "${artifact_path}/tfplan.json" 2>/dev/null || true # Generate human-readable plan terraform -chdir="${tf_dir}" show "${plan_file}" > "${artifact_path}/tfplan.txt" 2>/dev/null || true # Calculate checksums sha256sum "${plan_file}" > "${artifact_path}/tfplan.sha256" # Store metadata cat > "${artifact_path}/metadata.json" << EOF { "artifact_id": "${artifact_id}", "type": "terraform_plan", "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "agent_id": "${AGENT_ID}", "agent_tier": ${AGENT_TIER}, "tf_dir": "${tf_dir}", "checksum": "$(sha256sum ${plan_file} | cut -d' ' -f1)" } EOF echo "${artifact_path}" } # Verify plan artifact before apply verify_plan_artifact() { local plan_file="$1" local artifact_id="$2" local artifact_path="${ARTIFACT_DIR}/${artifact_id}" if [[ ! -d "${artifact_path}" ]]; then log_error "Plan artifact not found: ${artifact_id}" return 1 fi # Verify checksum local stored_checksum=$(cat "${artifact_path}/tfplan.sha256" | cut -d' ' -f1) local current_checksum=$(sha256sum "${plan_file}" | cut -d' ' -f1) if [[ "${stored_checksum}" != "${current_checksum}" ]]; then log_error "Plan checksum mismatch!" log_error " Stored: ${stored_checksum}" log_error " Current: ${current_checksum}" return 1 fi log_ok "Plan artifact verified: ${artifact_id}" return 0 } # Log to governance ledger log_to_ledger() { local action="$1" local status="$2" local details="$3" local ledger_entry=$(cat << EOF { "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "agent_id": "${AGENT_ID}", "agent_tier": ${AGENT_TIER}, "tool": "terraform", "action": "${action}", "status": "${status}", "details": "${details}" } EOF ) echo "${ledger_entry}" >> "${EVIDENCE_DIR}/terraform-ledger.jsonl" } # Main command handlers cmd_plan() { local tf_dir="$1" shift get_agent_info echo "" echo "==========================================" echo "GOVERNED TERRAFORM PLAN" echo "==========================================" echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})" echo "Directory: ${tf_dir}" echo "" # Extract targets for preflight (simplified) local targets="sandbox-vm-01" # Default target for preflight # Run preflight if ! run_preflight "${targets}"; then log_block "Preflight checks failed" log_to_ledger "plan" "BLOCKED" "Preflight failed" exit 1 fi log_ok "Preflight checks passed" # Generate artifact ID local artifact_id=$(generate_artifact_id) local plan_file="${tf_dir}/tfplan-${artifact_id}.binary" log_info "Running terraform plan..." log_info "Plan will be stored as artifact: ${artifact_id}" # Run terraform plan if terraform -chdir="${tf_dir}" plan -out="${plan_file}" "$@"; then log_ok "Terraform plan completed" # Store artifact local artifact_path=$(store_plan_artifact "${plan_file}" "${artifact_id}" "${tf_dir}") log_ok "Plan artifact stored: ${artifact_path}" log_to_ledger "plan" "SUCCESS" "artifact_id=${artifact_id}" echo "" echo "==========================================" echo "PLAN COMPLETE" echo "==========================================" echo "Artifact ID: ${artifact_id}" echo "To apply this plan:" echo " tf-governed.sh apply ${tf_dir} --plan=${artifact_id}" echo "==========================================" else log_error "Terraform plan failed" log_to_ledger "plan" "FAILED" "terraform error" exit 1 fi } cmd_apply() { local tf_dir="$1" shift get_agent_info echo "" echo "==========================================" echo "GOVERNED TERRAFORM APPLY" echo "==========================================" echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})" echo "Directory: ${tf_dir}" echo "" # Parse arguments local plan_artifact_id="" for arg in "$@"; do case $arg in --plan=*) plan_artifact_id="${arg#*=}" shift ;; esac done # Require plan artifact if [[ -z "${plan_artifact_id}" ]]; then log_block "Apply requires a plan artifact" echo "" echo "Usage: tf-governed.sh apply --plan=" echo "" echo "First run: tf-governed.sh plan " echo "Then use the artifact ID provided." log_to_ledger "apply" "BLOCKED" "no plan artifact" exit 1 fi local artifact_path="${ARTIFACT_DIR}/${plan_artifact_id}" local plan_file="${artifact_path}/tfplan.binary" # Verify plan exists if [[ ! -f "${plan_file}" ]]; then log_block "Plan artifact not found: ${plan_artifact_id}" log_to_ledger "apply" "BLOCKED" "artifact not found" exit 1 fi # Verify plan checksum if ! verify_plan_artifact "${plan_file}" "${plan_artifact_id}"; then log_block "Plan verification failed" log_to_ledger "apply" "BLOCKED" "checksum mismatch" exit 1 fi # Run preflight again before apply local targets="sandbox-vm-01" if ! run_preflight "${targets}"; then log_block "Preflight checks failed" log_to_ledger "apply" "BLOCKED" "Preflight failed" exit 1 fi log_ok "Preflight checks passed" log_info "Applying plan: ${plan_artifact_id}" # Copy plan to tf_dir for apply cp "${plan_file}" "${tf_dir}/tfplan.binary" # Run terraform apply if terraform -chdir="${tf_dir}" apply "${tf_dir}/tfplan.binary"; then log_ok "Terraform apply completed" # Store apply evidence cat > "${artifact_path}/apply-result.json" << EOF { "applied_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "agent_id": "${AGENT_ID}", "status": "SUCCESS" } EOF log_to_ledger "apply" "SUCCESS" "artifact_id=${plan_artifact_id}" echo "" echo "==========================================" echo "APPLY COMPLETE" echo "==========================================" echo "Plan artifact: ${plan_artifact_id}" echo "Evidence stored at: ${artifact_path}" echo "==========================================" else log_error "Terraform apply failed" log_to_ledger "apply" "FAILED" "terraform error" exit 1 fi # Cleanup rm -f "${tf_dir}/tfplan.binary" } cmd_destroy() { local tf_dir="$1" shift get_agent_info log_warn "DESTROY requires Tier 3+ access" if [[ "${AGENT_TIER}" -lt 3 ]]; then log_block "Destroy requires Tier 3+ (current: Tier ${AGENT_TIER})" log_to_ledger "destroy" "BLOCKED" "insufficient tier" exit 1 fi echo "" echo "==========================================" echo "GOVERNED TERRAFORM DESTROY" echo "==========================================" echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})" echo "Directory: ${tf_dir}" echo "" log_warn "This is a destructive operation!" # For destroy, we still require plan-first log_info "To destroy, first create a destroy plan:" echo " terraform -chdir=${tf_dir} plan -destroy -out=destroy.plan" echo " Then use: tf-governed.sh apply ${tf_dir} --plan=" log_to_ledger "destroy" "INFO" "destroy guidance provided" } # Show usage usage() { echo "Governed Terraform Wrapper" echo "" echo "Usage:" echo " tf-governed.sh plan [terraform options]" echo " tf-governed.sh apply --plan=" echo " tf-governed.sh destroy " echo "" echo "Environment Variables:" echo " AGENT_ID - Agent identifier (default: cli-user)" echo " AGENT_TIER - Agent trust tier 0-4 (default: 1)" echo "" echo "Examples:" echo " AGENT_ID=agent-001 tf-governed.sh plan ./infra" echo " tf-governed.sh apply ./infra --plan=tf-20260123-120000-abc123" } # Main case "${1:-}" in plan) shift cmd_plan "$@" ;; apply) shift cmd_apply "$@" ;; destroy) shift cmd_destroy "$@" ;; -h|--help|help) usage ;; *) usage exit 1 ;; esac