Skip to content

Commit 41b70bc

Browse files
authored
Merge pull request #756 from imran-siddique/add-governance-audit-hook
feat: add governance-audit hook — threat detection for Copilot sessions
2 parents f256cb2 + 32d8f7f commit 41b70bc

File tree

6 files changed

+344
-0
lines changed

6 files changed

+344
-0
lines changed

docs/README.hooks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ Hooks enable automated workflows triggered by specific events during GitHub Copi
2727

2828
| Name | Description | Events | Bundled Assets |
2929
| ---- | ----------- | ------ | -------------- |
30+
| [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` |
3031
| [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` |
3132
| [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` |

hooks/governance-audit/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
name: 'Governance Audit'
3+
description: 'Scans Copilot agent prompts for threat signals and logs governance events'
4+
tags: ['security', 'governance', 'audit', 'safety']
5+
---
6+
7+
# Governance Audit Hook
8+
9+
Real-time threat detection and audit logging for GitHub Copilot coding agent sessions. Scans user prompts for dangerous patterns before the agent processes them.
10+
11+
## Overview
12+
13+
This hook provides governance controls for Copilot coding agent sessions:
14+
- **Threat detection**: Scans prompts for data exfiltration, privilege escalation, system destruction, prompt injection, and credential exposure
15+
- **Governance levels**: Open, standard, strict, locked — from audit-only to full blocking
16+
- **Audit trail**: Append-only JSON log of all governance events
17+
- **Session summary**: Reports threat counts at session end
18+
19+
## Threat Categories
20+
21+
| Category | Examples | Severity |
22+
|----------|----------|----------|
23+
| `data_exfiltration` | "send all records to external API" | 0.7 - 0.95 |
24+
| `privilege_escalation` | "sudo", "chmod 777", "add to sudoers" | 0.8 - 0.95 |
25+
| `system_destruction` | "rm -rf /", "drop database" | 0.9 - 0.95 |
26+
| `prompt_injection` | "ignore previous instructions" | 0.6 - 0.9 |
27+
| `credential_exposure` | Hardcoded API keys, AWS access keys | 0.9 - 0.95 |
28+
29+
## Governance Levels
30+
31+
| Level | Behavior |
32+
|-------|----------|
33+
| `open` | Log threats only, never block |
34+
| `standard` | Log threats, block only if `BLOCK_ON_THREAT=true` |
35+
| `strict` | Log and block all detected threats |
36+
| `locked` | Log and block all detected threats |
37+
38+
## Installation
39+
40+
1. Copy the hook folder to your repository:
41+
```bash
42+
cp -r hooks/governance-audit .github/hooks/
43+
```
44+
45+
2. Ensure scripts are executable:
46+
```bash
47+
chmod +x .github/hooks/governance-audit/*.sh
48+
```
49+
50+
3. Create the logs directory and add to `.gitignore`:
51+
```bash
52+
mkdir -p logs/copilot/governance
53+
echo "logs/" >> .gitignore
54+
```
55+
56+
4. Commit to your repository's default branch.
57+
58+
## Configuration
59+
60+
Set environment variables in `hooks.json`:
61+
62+
```json
63+
{
64+
"env": {
65+
"GOVERNANCE_LEVEL": "strict",
66+
"BLOCK_ON_THREAT": "true"
67+
}
68+
}
69+
```
70+
71+
| Variable | Values | Default | Description |
72+
|----------|--------|---------|-------------|
73+
| `GOVERNANCE_LEVEL` | `open`, `standard`, `strict`, `locked` | `standard` | Controls blocking behavior |
74+
| `BLOCK_ON_THREAT` | `true`, `false` | `false` | Block prompts with threats (standard level) |
75+
| `SKIP_GOVERNANCE_AUDIT` | `true` | unset | Disable governance audit entirely |
76+
77+
## Log Format
78+
79+
Events are written to `logs/copilot/governance/audit.log` in JSON Lines format:
80+
81+
```json
82+
{"timestamp":"2026-01-15T10:30:00Z","event":"session_start","governance_level":"standard","cwd":"/workspace/project"}
83+
{"timestamp":"2026-01-15T10:31:00Z","event":"prompt_scanned","governance_level":"standard","status":"clean"}
84+
{"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"}]}
85+
{"timestamp":"2026-01-15T10:45:00Z","event":"session_end","total_events":12,"threats_detected":1}
86+
```
87+
88+
## Requirements
89+
90+
- `jq` for JSON processing (pre-installed on most CI environments and macOS)
91+
- `grep` with `-E` (extended regex) support
92+
- `bc` for floating-point comparison (optional, gracefully degrades)
93+
94+
## Privacy & Security
95+
96+
- Full prompts are **never** logged — only matched threat patterns (minimal evidence snippets) and metadata are recorded
97+
- Add `logs/` to `.gitignore` to keep audit data local
98+
- Set `SKIP_GOVERNANCE_AUDIT=true` to disable entirely
99+
- All data stays local — no external network calls
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
3+
# Governance Audit: Log session end with summary statistics
4+
5+
set -euo pipefail
6+
7+
if [[ "${SKIP_GOVERNANCE_AUDIT:-}" == "true" ]]; then
8+
exit 0
9+
fi
10+
11+
INPUT=$(cat)
12+
13+
mkdir -p logs/copilot/governance
14+
15+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
16+
LOG_FILE="logs/copilot/governance/audit.log"
17+
18+
# Count events from this session (filter by session start timestamp)
19+
TOTAL=0
20+
THREATS=0
21+
SESSION_START=""
22+
if [[ -f "$LOG_FILE" ]]; then
23+
# Find the last session_start event to scope stats to current session
24+
SESSION_START=$(grep '"session_start"' "$LOG_FILE" 2>/dev/null | tail -1 | jq -r '.timestamp' 2>/dev/null || echo "")
25+
if [[ -n "$SESSION_START" ]]; then
26+
# Count events after session start
27+
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)
28+
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)
29+
else
30+
TOTAL=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
31+
THREATS=$(grep -c '"threat_detected"' "$LOG_FILE" 2>/dev/null || echo 0)
32+
fi
33+
fi
34+
35+
jq -Rn \
36+
--arg timestamp "$TIMESTAMP" \
37+
--argjson total "$TOTAL" \
38+
--argjson threats "$THREATS" \
39+
'{"timestamp":$timestamp,"event":"session_end","total_events":$total,"threats_detected":$threats}' \
40+
>> "$LOG_FILE"
41+
42+
if [[ "$THREATS" -gt 0 ]]; then
43+
echo "⚠️ Session ended: $THREATS threat(s) detected in $TOTAL events"
44+
else
45+
echo "✅ Session ended: $TOTAL events, no threats"
46+
fi
47+
48+
exit 0
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
# Governance Audit: Log session start with governance context
4+
5+
set -euo pipefail
6+
7+
if [[ "${SKIP_GOVERNANCE_AUDIT:-}" == "true" ]]; then
8+
exit 0
9+
fi
10+
11+
INPUT=$(cat)
12+
13+
mkdir -p logs/copilot/governance
14+
15+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
16+
CWD=$(pwd)
17+
LEVEL="${GOVERNANCE_LEVEL:-standard}"
18+
19+
jq -Rn \
20+
--arg timestamp "$TIMESTAMP" \
21+
--arg cwd "$CWD" \
22+
--arg level "$LEVEL" \
23+
'{"timestamp":$timestamp,"event":"session_start","governance_level":$level,"cwd":$cwd}' \
24+
>> logs/copilot/governance/audit.log
25+
26+
echo "🛡️ Governance audit active (level: $LEVEL)"
27+
exit 0

hooks/governance-audit/hooks.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"version": 1,
3+
"hooks": {
4+
"sessionStart": [
5+
{
6+
"type": "command",
7+
"bash": ".github/hooks/governance-audit/audit-session-start.sh",
8+
"cwd": ".",
9+
"timeoutSec": 5
10+
}
11+
],
12+
"sessionEnd": [
13+
{
14+
"type": "command",
15+
"bash": ".github/hooks/governance-audit/audit-session-end.sh",
16+
"cwd": ".",
17+
"timeoutSec": 5
18+
}
19+
],
20+
"userPromptSubmitted": [
21+
{
22+
"type": "command",
23+
"bash": ".github/hooks/governance-audit/audit-prompt.sh",
24+
"cwd": ".",
25+
"env": {
26+
"GOVERNANCE_LEVEL": "standard",
27+
"BLOCK_ON_THREAT": "false"
28+
},
29+
"timeoutSec": 10
30+
}
31+
]
32+
}
33+
}

0 commit comments

Comments
 (0)