|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# Governance Audit: Scan user prompts for threat signals before agent processing |
| 4 | +# |
| 5 | +# Environment variables: |
| 6 | +# GOVERNANCE_LEVEL - "open", "standard", "strict", "locked" (default: standard) |
| 7 | +# BLOCK_ON_THREAT - "true" to exit non-zero on threats (default: false) |
| 8 | +# SKIP_GOVERNANCE_AUDIT - "true" to disable (default: unset) |
| 9 | + |
| 10 | +set -euo pipefail |
| 11 | + |
| 12 | +if [[ "${SKIP_GOVERNANCE_AUDIT:-}" == "true" ]]; then |
| 13 | + exit 0 |
| 14 | +fi |
| 15 | + |
| 16 | +INPUT=$(cat) |
| 17 | + |
| 18 | +mkdir -p logs/copilot/governance |
| 19 | + |
| 20 | +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") |
| 21 | +LEVEL="${GOVERNANCE_LEVEL:-standard}" |
| 22 | +BLOCK="${BLOCK_ON_THREAT:-false}" |
| 23 | +LOG_FILE="logs/copilot/governance/audit.log" |
| 24 | + |
| 25 | +# Extract prompt text from Copilot input (JSON with userMessage field) |
| 26 | +PROMPT="" |
| 27 | +if command -v jq &>/dev/null; then |
| 28 | + PROMPT=$(echo "$INPUT" | jq -r '.userMessage // .prompt // empty' 2>/dev/null || echo "") |
| 29 | +fi |
| 30 | +if [[ -z "$PROMPT" ]]; then |
| 31 | + PROMPT="$INPUT" |
| 32 | +fi |
| 33 | + |
| 34 | +# Threat detection patterns organized by category |
| 35 | +# Each pattern has: category, description, severity (0.0-1.0) |
| 36 | +THREATS_FOUND=() |
| 37 | + |
| 38 | +check_pattern() { |
| 39 | + local pattern="$1" |
| 40 | + local category="$2" |
| 41 | + local severity="$3" |
| 42 | + local description="$4" |
| 43 | + |
| 44 | + if echo "$PROMPT" | grep -qiE "$pattern"; then |
| 45 | + local evidence |
| 46 | + evidence=$(echo "$PROMPT" | grep -oiE "$pattern" | head -1) |
| 47 | + local evidence_encoded |
| 48 | + evidence_encoded=$(printf '%s' "$evidence" | base64 | tr -d '\n') |
| 49 | + THREATS_FOUND+=("$category $severity $description $evidence_encoded") |
| 50 | + fi |
| 51 | +} |
| 52 | + |
| 53 | +# Data exfiltration signals |
| 54 | +check_pattern "send\s+(all|every|entire)\s+\w+\s+to\s+" "data_exfiltration" "0.8" "Bulk data transfer" |
| 55 | +check_pattern "export\s+.*\s+to\s+(external|outside|third[_-]?party)" "data_exfiltration" "0.9" "External export" |
| 56 | +check_pattern "curl\s+.*\s+-d\s+" "data_exfiltration" "0.7" "HTTP POST with data" |
| 57 | +check_pattern "upload\s+.*\s+(credentials|secrets|keys)" "data_exfiltration" "0.95" "Credential upload" |
| 58 | + |
| 59 | +# Privilege escalation signals |
| 60 | +check_pattern "(sudo|as\s+root|admin\s+access|runas\s+/user)" "privilege_escalation" "0.8" "Elevated privileges" |
| 61 | +check_pattern "chmod\s+777" "privilege_escalation" "0.9" "World-writable permissions" |
| 62 | +check_pattern "add\s+.*\s+(sudoers|administrators)" "privilege_escalation" "0.95" "Adding admin access" |
| 63 | + |
| 64 | +# System destruction signals |
| 65 | +check_pattern "(rm\s+-rf\s+/|del\s+/[sq]|format\s+c:)" "system_destruction" "0.95" "Destructive command" |
| 66 | +check_pattern "(drop\s+database|truncate\s+table|delete\s+from\s+\w+\s*(;|\s*$))" "system_destruction" "0.9" "Database destruction" |
| 67 | +check_pattern "wipe\s+(all|entire|every)" "system_destruction" "0.9" "Mass deletion" |
| 68 | + |
| 69 | +# Prompt injection signals |
| 70 | +check_pattern "ignore\s+(previous|above|all)\s+(instructions?|rules?|prompts?)" "prompt_injection" "0.9" "Instruction override" |
| 71 | +check_pattern "you\s+are\s+now\s+(a|an)\s+(assistant|ai|bot|system|expert|language\s+model)\b" "prompt_injection" "0.7" "Role reassignment" |
| 72 | +check_pattern "(^|\n)\s*system\s*:\s*you\s+are" "prompt_injection" "0.6" "System prompt injection" |
| 73 | + |
| 74 | +# Credential exposure signals |
| 75 | +check_pattern "(api[_-]?key|secret[_-]?key|password|token)\s*[:=]\s*['\"]?\w{8,}" "credential_exposure" "0.9" "Possible hardcoded credential" |
| 76 | +check_pattern "(aws_access_key|AKIA[0-9A-Z]{16})" "credential_exposure" "0.95" "AWS key exposure" |
| 77 | + |
| 78 | +# Log the prompt event |
| 79 | +if [[ ${#THREATS_FOUND[@]} -gt 0 ]]; then |
| 80 | + # Build threats JSON array |
| 81 | + THREATS_JSON="[" |
| 82 | + FIRST=true |
| 83 | + MAX_SEVERITY="0.0" |
| 84 | + for threat in "${THREATS_FOUND[@]}"; do |
| 85 | + IFS=$'\t' read -r category severity description evidence_encoded <<< "$threat" |
| 86 | + local evidence |
| 87 | + evidence=$(printf '%s' "$evidence_encoded" | base64 -d 2>/dev/null || echo "[redacted]") |
| 88 | + |
| 89 | + if [[ "$FIRST" != "true" ]]; then |
| 90 | + THREATS_JSON+="," |
| 91 | + fi |
| 92 | + FIRST=false |
| 93 | + |
| 94 | + THREATS_JSON+=$(jq -Rn \ |
| 95 | + --arg cat "$category" \ |
| 96 | + --arg sev "$severity" \ |
| 97 | + --arg desc "$description" \ |
| 98 | + --arg ev "$evidence" \ |
| 99 | + '{"category":$cat,"severity":($sev|tonumber),"description":$desc,"evidence":$ev}') |
| 100 | + |
| 101 | + # Track max severity |
| 102 | + if (( $(echo "$severity > $MAX_SEVERITY" | bc -l 2>/dev/null || echo 0) )); then |
| 103 | + MAX_SEVERITY="$severity" |
| 104 | + fi |
| 105 | + done |
| 106 | + THREATS_JSON+="]" |
| 107 | + |
| 108 | + jq -Rn \ |
| 109 | + --arg timestamp "$TIMESTAMP" \ |
| 110 | + --arg level "$LEVEL" \ |
| 111 | + --arg max_severity "$MAX_SEVERITY" \ |
| 112 | + --argjson threats "$THREATS_JSON" \ |
| 113 | + --argjson count "${#THREATS_FOUND[@]}" \ |
| 114 | + '{"timestamp":$timestamp,"event":"threat_detected","governance_level":$level,"threat_count":$count,"max_severity":($max_severity|tonumber),"threats":$threats}' \ |
| 115 | + >> "$LOG_FILE" |
| 116 | + |
| 117 | + echo "⚠️ Governance: ${#THREATS_FOUND[@]} threat signal(s) detected (max severity: $MAX_SEVERITY)" |
| 118 | + for threat in "${THREATS_FOUND[@]}"; do |
| 119 | + IFS=$'\t' read -r category severity description _evidence_encoded <<< "$threat" |
| 120 | + echo " 🔴 [$category] $description (severity: $severity)" |
| 121 | + done |
| 122 | + |
| 123 | + # In strict/locked mode or when BLOCK_ON_THREAT is true, exit non-zero to block |
| 124 | + if [[ "$BLOCK" == "true" ]] || [[ "$LEVEL" == "strict" ]] || [[ "$LEVEL" == "locked" ]]; then |
| 125 | + echo "🚫 Prompt blocked by governance policy (level: $LEVEL)" |
| 126 | + exit 1 |
| 127 | + fi |
| 128 | +else |
| 129 | + jq -Rn \ |
| 130 | + --arg timestamp "$TIMESTAMP" \ |
| 131 | + --arg level "$LEVEL" \ |
| 132 | + '{"timestamp":$timestamp,"event":"prompt_scanned","governance_level":$level,"status":"clean"}' \ |
| 133 | + >> "$LOG_FILE" |
| 134 | +fi |
| 135 | + |
| 136 | +exit 0 |
0 commit comments