mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Implements three roadmap features: - Agent description compression (#491): New `agent-compress` module with catalog/summary/full compression modes and lazy-loading. Reduces ~26k token agent descriptions to ~2-3k catalog entries for context efficiency. - Inspection logic (#485): New `inspection` module that detects recurring failure patterns in skill_runs. Groups by skill + normalized failure reason, generates structured reports with suggested remediation actions. Configurable threshold (default: 3 failures). - Governance event capture hook (#482): PreToolUse/PostToolUse hook that detects secrets, policy violations, approval-required commands, and elevated privilege usage. Gated behind ECC_GOVERNANCE_CAPTURE=1 flag. Writes to governance_events table via JSON-line stderr output. 59 new tests (16 + 16 + 27), all passing.
This commit is contained in:
280
scripts/hooks/governance-capture.js
Normal file
280
scripts/hooks/governance-capture.js
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Governance Event Capture Hook
|
||||
*
|
||||
* PreToolUse/PostToolUse hook that detects governance-relevant events
|
||||
* and writes them to the governance_events table in the state store.
|
||||
*
|
||||
* Captured event types:
|
||||
* - secret_detected: Hardcoded secrets in tool input/output
|
||||
* - policy_violation: Actions that violate configured policies
|
||||
* - security_finding: Security-relevant tool invocations
|
||||
* - approval_requested: Operations requiring explicit approval
|
||||
*
|
||||
* Enable: Set ECC_GOVERNANCE_CAPTURE=1
|
||||
* Configure session: Set ECC_SESSION_ID for session correlation
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
|
||||
// Patterns that indicate potential hardcoded secrets
|
||||
const SECRET_PATTERNS = [
|
||||
{ name: 'aws_key', pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/i },
|
||||
{ name: 'generic_secret', pattern: /(?:secret|password|token|api[_-]?key)\s*[:=]\s*["'][^"']{8,}/i },
|
||||
{ name: 'private_key', pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/ },
|
||||
{ name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
||||
{ name: 'github_token', pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/ },
|
||||
];
|
||||
|
||||
// Tool names that represent security-relevant operations
|
||||
const SECURITY_RELEVANT_TOOLS = new Set([
|
||||
'Bash', // Could execute arbitrary commands
|
||||
]);
|
||||
|
||||
// Commands that require governance approval
|
||||
const APPROVAL_COMMANDS = [
|
||||
/git\s+push\s+.*--force/,
|
||||
/git\s+reset\s+--hard/,
|
||||
/rm\s+-rf?\s/,
|
||||
/DROP\s+(?:TABLE|DATABASE)/i,
|
||||
/DELETE\s+FROM\s+\w+\s*(?:;|$)/i,
|
||||
];
|
||||
|
||||
// File patterns that indicate policy-sensitive paths
|
||||
const SENSITIVE_PATHS = [
|
||||
/\.env(?:\.|$)/,
|
||||
/credentials/i,
|
||||
/secrets?\./i,
|
||||
/\.pem$/,
|
||||
/\.key$/,
|
||||
/id_rsa/,
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate a unique event ID.
|
||||
*/
|
||||
function generateEventId() {
|
||||
return `gov-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan text content for hardcoded secrets.
|
||||
* Returns array of { name, match } for each detected secret.
|
||||
*/
|
||||
function detectSecrets(text) {
|
||||
if (!text || typeof text !== 'string') return [];
|
||||
|
||||
const findings = [];
|
||||
for (const { name, pattern } of SECRET_PATTERNS) {
|
||||
if (pattern.test(text)) {
|
||||
findings.push({ name });
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command requires governance approval.
|
||||
*/
|
||||
function detectApprovalRequired(command) {
|
||||
if (!command || typeof command !== 'string') return [];
|
||||
|
||||
const findings = [];
|
||||
for (const pattern of APPROVAL_COMMANDS) {
|
||||
if (pattern.test(command)) {
|
||||
findings.push({ pattern: pattern.source });
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path is policy-sensitive.
|
||||
*/
|
||||
function detectSensitivePath(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') return false;
|
||||
|
||||
return SENSITIVE_PATHS.some(pattern => pattern.test(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a hook input payload and return governance events to capture.
|
||||
*
|
||||
* @param {Object} input - Parsed hook input (tool_name, tool_input, tool_output)
|
||||
* @param {Object} [context] - Additional context (sessionId, hookPhase)
|
||||
* @returns {Array<Object>} Array of governance event objects
|
||||
*/
|
||||
function analyzeForGovernanceEvents(input, context = {}) {
|
||||
const events = [];
|
||||
const toolName = input.tool_name || '';
|
||||
const toolInput = input.tool_input || {};
|
||||
const toolOutput = typeof input.tool_output === 'string' ? input.tool_output : '';
|
||||
const sessionId = context.sessionId || null;
|
||||
const hookPhase = context.hookPhase || 'unknown';
|
||||
|
||||
// 1. Secret detection in tool input content
|
||||
const inputText = typeof toolInput === 'object'
|
||||
? JSON.stringify(toolInput)
|
||||
: String(toolInput);
|
||||
|
||||
const inputSecrets = detectSecrets(inputText);
|
||||
const outputSecrets = detectSecrets(toolOutput);
|
||||
const allSecrets = [...inputSecrets, ...outputSecrets];
|
||||
|
||||
if (allSecrets.length > 0) {
|
||||
events.push({
|
||||
id: generateEventId(),
|
||||
sessionId,
|
||||
eventType: 'secret_detected',
|
||||
payload: {
|
||||
toolName,
|
||||
hookPhase,
|
||||
secretTypes: allSecrets.map(s => s.name),
|
||||
location: inputSecrets.length > 0 ? 'input' : 'output',
|
||||
severity: 'critical',
|
||||
},
|
||||
resolvedAt: null,
|
||||
resolution: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Approval-required commands (Bash only)
|
||||
if (toolName === 'Bash') {
|
||||
const command = toolInput.command || '';
|
||||
const approvalFindings = detectApprovalRequired(command);
|
||||
|
||||
if (approvalFindings.length > 0) {
|
||||
events.push({
|
||||
id: generateEventId(),
|
||||
sessionId,
|
||||
eventType: 'approval_requested',
|
||||
payload: {
|
||||
toolName,
|
||||
hookPhase,
|
||||
command: command.slice(0, 200),
|
||||
matchedPatterns: approvalFindings.map(f => f.pattern),
|
||||
severity: 'high',
|
||||
},
|
||||
resolvedAt: null,
|
||||
resolution: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Policy violation: writing to sensitive paths
|
||||
const filePath = toolInput.file_path || toolInput.path || '';
|
||||
if (filePath && detectSensitivePath(filePath)) {
|
||||
events.push({
|
||||
id: generateEventId(),
|
||||
sessionId,
|
||||
eventType: 'policy_violation',
|
||||
payload: {
|
||||
toolName,
|
||||
hookPhase,
|
||||
filePath: filePath.slice(0, 200),
|
||||
reason: 'sensitive_file_access',
|
||||
severity: 'warning',
|
||||
},
|
||||
resolvedAt: null,
|
||||
resolution: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Security-relevant tool usage tracking
|
||||
if (SECURITY_RELEVANT_TOOLS.has(toolName) && hookPhase === 'post') {
|
||||
const command = toolInput.command || '';
|
||||
const hasElevated = /sudo\s/.test(command) || /chmod\s/.test(command) || /chown\s/.test(command);
|
||||
|
||||
if (hasElevated) {
|
||||
events.push({
|
||||
id: generateEventId(),
|
||||
sessionId,
|
||||
eventType: 'security_finding',
|
||||
payload: {
|
||||
toolName,
|
||||
hookPhase,
|
||||
command: command.slice(0, 200),
|
||||
reason: 'elevated_privilege_command',
|
||||
severity: 'medium',
|
||||
},
|
||||
resolvedAt: null,
|
||||
resolution: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core hook logic — exported so run-with-flags.js can call directly.
|
||||
*
|
||||
* @param {string} rawInput - Raw JSON string from stdin
|
||||
* @returns {string} The original input (pass-through)
|
||||
*/
|
||||
function run(rawInput) {
|
||||
// Gate on feature flag
|
||||
if (String(process.env.ECC_GOVERNANCE_CAPTURE || '').toLowerCase() !== '1') {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
try {
|
||||
const input = JSON.parse(rawInput);
|
||||
const sessionId = process.env.ECC_SESSION_ID || null;
|
||||
const hookPhase = process.env.CLAUDE_HOOK_EVENT_NAME || 'unknown';
|
||||
|
||||
const events = analyzeForGovernanceEvents(input, {
|
||||
sessionId,
|
||||
hookPhase: hookPhase.startsWith('Pre') ? 'pre' : 'post',
|
||||
});
|
||||
|
||||
if (events.length > 0) {
|
||||
// Write events to stderr as JSON-lines for the caller to capture.
|
||||
// The state store write is async and handled by a separate process
|
||||
// to avoid blocking the hook pipeline.
|
||||
for (const event of events) {
|
||||
process.stderr.write(
|
||||
`[governance] ${JSON.stringify(event)}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore parse errors — never block the tool pipeline.
|
||||
}
|
||||
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
// ── stdin entry point ────────────────────────────────
|
||||
if (require.main === module) {
|
||||
let raw = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
if (raw.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - raw.length;
|
||||
raw += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw);
|
||||
process.stdout.write(result);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APPROVAL_COMMANDS,
|
||||
SECRET_PATTERNS,
|
||||
SECURITY_RELEVANT_TOOLS,
|
||||
SENSITIVE_PATHS,
|
||||
analyzeForGovernanceEvents,
|
||||
detectApprovalRequired,
|
||||
detectSecrets,
|
||||
detectSensitivePath,
|
||||
generateEventId,
|
||||
run,
|
||||
};
|
||||
Reference in New Issue
Block a user