Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ Hooks enable automated workflows triggered by specific events during GitHub Copi

| Name | Description | Events | Bundled Assets |
| ---- | ----------- | ------ | -------------- |
| [Governance Audit](../hooks/governance-audit/README.md) | Scans Copilot agent prompts for threat signals and logs governance events | sessionStart, sessionEnd, userPromptSubmitted | `audit-prompt.sh`<br />`audit-session-end.sh`<br />`audit-session-start.sh`<br />`hooks.json` |
| [Session Auto-Commit](../hooks/session-auto-commit/README.md) | Automatically commits and pushes changes when a Copilot coding agent session ends | sessionEnd | `auto-commit.sh`<br />`hooks.json` |
| [Session Logger](../hooks/session-logger/README.md) | Logs all Copilot coding agent session activity for audit and analysis | sessionStart, sessionEnd, userPromptSubmitted | `hooks.json`<br />`log-prompt.sh`<br />`log-session-end.sh`<br />`log-session-start.sh` |
99 changes: 99 additions & 0 deletions hooks/governance-audit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
name: 'Governance Audit'
description: 'Scans Copilot agent prompts for threat signals and logs governance events'
tags: ['security', 'governance', 'audit', 'safety']
---

# Governance Audit Hook

Real-time threat detection and audit logging for GitHub Copilot coding agent sessions. Scans user prompts for dangerous patterns before the agent processes them.

## Overview

This hook provides governance controls for Copilot coding agent sessions:
- **Threat detection**: Scans prompts for data exfiltration, privilege escalation, system destruction, prompt injection, and credential exposure
- **Governance levels**: Open, standard, strict, locked — from audit-only to full blocking
- **Audit trail**: Append-only JSON log of all governance events
- **Session summary**: Reports threat counts at session end

## Threat Categories

| Category | Examples | Severity |
|----------|----------|----------|
| `data_exfiltration` | "send all records to external API" | 0.7 - 0.95 |
| `privilege_escalation` | "sudo", "chmod 777", "add to sudoers" | 0.8 - 0.95 |
| `system_destruction` | "rm -rf /", "drop database" | 0.9 - 0.95 |
| `prompt_injection` | "ignore previous instructions" | 0.6 - 0.9 |
| `credential_exposure` | Hardcoded API keys, AWS access keys | 0.9 - 0.95 |

## Governance Levels

| Level | Behavior |
|-------|----------|
| `open` | Log threats only, never block |
| `standard` | Log threats, block only if `BLOCK_ON_THREAT=true` |
| `strict` | Log and block all detected threats |
| `locked` | Log and block all detected threats |

## Installation

1. Copy the hook folder to your repository:
```bash
cp -r hooks/governance-audit .github/hooks/
```

2. Ensure scripts are executable:
```bash
chmod +x .github/hooks/governance-audit/*.sh
```

3. Create the logs directory and add to `.gitignore`:
```bash
mkdir -p logs/copilot/governance
echo "logs/" >> .gitignore
```

4. Commit to your repository's default branch.

## Configuration

Set environment variables in `hooks.json`:

```json
{
"env": {
"GOVERNANCE_LEVEL": "strict",
"BLOCK_ON_THREAT": "true"
}
}
```

| Variable | Values | Default | Description |
|----------|--------|---------|-------------|
| `GOVERNANCE_LEVEL` | `open`, `standard`, `strict`, `locked` | `standard` | Controls blocking behavior |
| `BLOCK_ON_THREAT` | `true`, `false` | `false` | Block prompts with threats (standard level) |
| `SKIP_GOVERNANCE_AUDIT` | `true` | unset | Disable governance audit entirely |

## Log Format

Events are written to `logs/copilot/governance/audit.log` in JSON Lines format:

```json
{"timestamp":"2026-01-15T10:30:00Z","event":"session_start","governance_level":"standard","cwd":"/workspace/project"}
{"timestamp":"2026-01-15T10:31:00Z","event":"prompt_scanned","governance_level":"standard","status":"clean"}
{"timestamp":"2026-01-15T10:32:00Z","event":"threat_detected","governance_level":"standard","threat_count":1,"threats":[{"category":"privilege_escalation","severity":0.8,"description":"Elevated privileges","evidence":"sudo"}]}
{"timestamp":"2026-01-15T10:45:00Z","event":"session_end","total_events":12,"threats_detected":1}
```

## Requirements

- `jq` for JSON processing (pre-installed on most CI environments and macOS)
- `grep` with `-E` (extended regex) support
- `bc` for floating-point comparison (optional, gracefully degrades)

## Privacy & Security

- Full prompts are **never** logged — only matched threat patterns (minimal evidence snippets) and metadata are recorded
- Add `logs/` to `.gitignore` to keep audit data local
- Set `SKIP_GOVERNANCE_AUDIT=true` to disable entirely
- All data stays local — no external network calls
136 changes: 136 additions & 0 deletions hooks/governance-audit/audit-prompt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/bin/bash

# Governance Audit: Scan user prompts for threat signals before agent processing
#
# Environment variables:
# GOVERNANCE_LEVEL - "open", "standard", "strict", "locked" (default: standard)
# BLOCK_ON_THREAT - "true" to exit non-zero on threats (default: false)
# SKIP_GOVERNANCE_AUDIT - "true" to disable (default: unset)

set -euo pipefail

if [[ "${SKIP_GOVERNANCE_AUDIT:-}" == "true" ]]; then
exit 0
fi

INPUT=$(cat)

mkdir -p logs/copilot/governance

TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LEVEL="${GOVERNANCE_LEVEL:-standard}"
BLOCK="${BLOCK_ON_THREAT:-false}"
LOG_FILE="logs/copilot/governance/audit.log"

# Extract prompt text from Copilot input (JSON with userMessage field)
PROMPT=""
if command -v jq &>/dev/null; then
PROMPT=$(echo "$INPUT" | jq -r '.userMessage // .prompt // empty' 2>/dev/null || echo "")
fi
if [[ -z "$PROMPT" ]]; then
PROMPT="$INPUT"
fi

# Threat detection patterns organized by category
# Each pattern has: category, description, severity (0.0-1.0)
THREATS_FOUND=()

check_pattern() {
local pattern="$1"
local category="$2"
local severity="$3"
local description="$4"

if echo "$PROMPT" | grep -qiE "$pattern"; then
local evidence
evidence=$(echo "$PROMPT" | grep -oiE "$pattern" | head -1)
local evidence_encoded
evidence_encoded=$(printf '%s' "$evidence" | base64 | tr -d '\n')
THREATS_FOUND+=("$category $severity $description $evidence_encoded")
fi
}

# Data exfiltration signals
check_pattern "send\s+(all|every|entire)\s+\w+\s+to\s+" "data_exfiltration" "0.8" "Bulk data transfer"
check_pattern "export\s+.*\s+to\s+(external|outside|third[_-]?party)" "data_exfiltration" "0.9" "External export"
check_pattern "curl\s+.*\s+-d\s+" "data_exfiltration" "0.7" "HTTP POST with data"
check_pattern "upload\s+.*\s+(credentials|secrets|keys)" "data_exfiltration" "0.95" "Credential upload"

# Privilege escalation signals
check_pattern "(sudo|as\s+root|admin\s+access|runas\s+/user)" "privilege_escalation" "0.8" "Elevated privileges"
check_pattern "chmod\s+777" "privilege_escalation" "0.9" "World-writable permissions"
check_pattern "add\s+.*\s+(sudoers|administrators)" "privilege_escalation" "0.95" "Adding admin access"

# System destruction signals
check_pattern "(rm\s+-rf\s+/|del\s+/[sq]|format\s+c:)" "system_destruction" "0.95" "Destructive command"
check_pattern "(drop\s+database|truncate\s+table|delete\s+from\s+\w+\s*(;|\s*$))" "system_destruction" "0.9" "Database destruction"
check_pattern "wipe\s+(all|entire|every)" "system_destruction" "0.9" "Mass deletion"

# Prompt injection signals
check_pattern "ignore\s+(previous|above|all)\s+(instructions?|rules?|prompts?)" "prompt_injection" "0.9" "Instruction override"
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"
check_pattern "(^|\n)\s*system\s*:\s*you\s+are" "prompt_injection" "0.6" "System prompt injection"

# Credential exposure signals
check_pattern "(api[_-]?key|secret[_-]?key|password|token)\s*[:=]\s*['\"]?\w{8,}" "credential_exposure" "0.9" "Possible hardcoded credential"
check_pattern "(aws_access_key|AKIA[0-9A-Z]{16})" "credential_exposure" "0.95" "AWS key exposure"

# Log the prompt event
if [[ ${#THREATS_FOUND[@]} -gt 0 ]]; then
# Build threats JSON array
THREATS_JSON="["
FIRST=true
MAX_SEVERITY="0.0"
for threat in "${THREATS_FOUND[@]}"; do
IFS=$'\t' read -r category severity description evidence_encoded <<< "$threat"
local evidence
evidence=$(printf '%s' "$evidence_encoded" | base64 -d 2>/dev/null || echo "[redacted]")

if [[ "$FIRST" != "true" ]]; then
THREATS_JSON+=","
fi
FIRST=false

THREATS_JSON+=$(jq -Rn \
--arg cat "$category" \
--arg sev "$severity" \
--arg desc "$description" \
--arg ev "$evidence" \
'{"category":$cat,"severity":($sev|tonumber),"description":$desc,"evidence":$ev}')

# Track max severity
if (( $(echo "$severity > $MAX_SEVERITY" | bc -l 2>/dev/null || echo 0) )); then
MAX_SEVERITY="$severity"
fi
Comment thread
imran-siddique marked this conversation as resolved.
done
THREATS_JSON+="]"

jq -Rn \
--arg timestamp "$TIMESTAMP" \
--arg level "$LEVEL" \
--arg max_severity "$MAX_SEVERITY" \
--argjson threats "$THREATS_JSON" \
--argjson count "${#THREATS_FOUND[@]}" \
'{"timestamp":$timestamp,"event":"threat_detected","governance_level":$level,"threat_count":$count,"max_severity":($max_severity|tonumber),"threats":$threats}' \
>> "$LOG_FILE"

echo "⚠️ Governance: ${#THREATS_FOUND[@]} threat signal(s) detected (max severity: $MAX_SEVERITY)"
for threat in "${THREATS_FOUND[@]}"; do
IFS=$'\t' read -r category severity description _evidence_encoded <<< "$threat"
echo " 🔴 [$category] $description (severity: $severity)"
done

# In strict/locked mode or when BLOCK_ON_THREAT is true, exit non-zero to block
if [[ "$BLOCK" == "true" ]] || [[ "$LEVEL" == "strict" ]] || [[ "$LEVEL" == "locked" ]]; then
echo "🚫 Prompt blocked by governance policy (level: $LEVEL)"
exit 1
fi
else
jq -Rn \
--arg timestamp "$TIMESTAMP" \
--arg level "$LEVEL" \
'{"timestamp":$timestamp,"event":"prompt_scanned","governance_level":$level,"status":"clean"}' \
>> "$LOG_FILE"
fi

exit 0
48 changes: 48 additions & 0 deletions hooks/governance-audit/audit-session-end.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

# Governance Audit: Log session end with summary statistics

set -euo pipefail

if [[ "${SKIP_GOVERNANCE_AUDIT:-}" == "true" ]]; then
exit 0
fi

INPUT=$(cat)

mkdir -p logs/copilot/governance

TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LOG_FILE="logs/copilot/governance/audit.log"

# Count events from this session (filter by session start timestamp)
TOTAL=0
THREATS=0
SESSION_START=""
if [[ -f "$LOG_FILE" ]]; then
# Find the last session_start event to scope stats to current session
SESSION_START=$(grep '"session_start"' "$LOG_FILE" 2>/dev/null | tail -1 | jq -r '.timestamp' 2>/dev/null || echo "")
if [[ -n "$SESSION_START" ]]; then
# Count events after session start
TOTAL=$(awk -v start="$SESSION_START" -F'"timestamp":"' '{split($2,a,"\""); if(a[1]>=start) count++} END{print count+0}' "$LOG_FILE" 2>/dev/null || echo 0)
THREATS=$(awk -v start="$SESSION_START" -F'"timestamp":"' '{split($2,a,"\""); if(a[1]>=start && /threat_detected/) count++} END{print count+0}' "$LOG_FILE" 2>/dev/null || echo 0)
else
TOTAL=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
THREATS=$(grep -c '"threat_detected"' "$LOG_FILE" 2>/dev/null || echo 0)
fi
fi

jq -Rn \
--arg timestamp "$TIMESTAMP" \
--argjson total "$TOTAL" \
--argjson threats "$THREATS" \
'{"timestamp":$timestamp,"event":"session_end","total_events":$total,"threats_detected":$threats}' \
>> "$LOG_FILE"

if [[ "$THREATS" -gt 0 ]]; then
echo "⚠️ Session ended: $THREATS threat(s) detected in $TOTAL events"
else
echo "✅ Session ended: $TOTAL events, no threats"
fi

exit 0
27 changes: 27 additions & 0 deletions hooks/governance-audit/audit-session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

# Governance Audit: Log session start with governance context

set -euo pipefail

if [[ "${SKIP_GOVERNANCE_AUDIT:-}" == "true" ]]; then
exit 0
fi

INPUT=$(cat)

mkdir -p logs/copilot/governance

TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
CWD=$(pwd)
LEVEL="${GOVERNANCE_LEVEL:-standard}"

jq -Rn \
--arg timestamp "$TIMESTAMP" \
--arg cwd "$CWD" \
--arg level "$LEVEL" \
'{"timestamp":$timestamp,"event":"session_start","governance_level":$level,"cwd":$cwd}' \
>> logs/copilot/governance/audit.log

echo "🛡️ Governance audit active (level: $LEVEL)"
exit 0
33 changes: 33 additions & 0 deletions hooks/governance-audit/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"bash": ".github/hooks/governance-audit/audit-session-start.sh",
"cwd": ".",
"timeoutSec": 5
}
],
"sessionEnd": [
{
"type": "command",
"bash": ".github/hooks/governance-audit/audit-session-end.sh",
"cwd": ".",
"timeoutSec": 5
}
],
"userPromptSubmitted": [
{
"type": "command",
"bash": ".github/hooks/governance-audit/audit-prompt.sh",
"cwd": ".",
"env": {
"GOVERNANCE_LEVEL": "standard",
"BLOCK_ON_THREAT": "false"
},
"timeoutSec": 10
}
]
}
}