feat(governance): add policy-as-code — .llm-security/policy.json for distributable hook configuration

New policy-loader.mjs reads .llm-security/policy.json with deep-merge against
defaults that exactly match existing hardcoded values. Integrated into all 7 hooks:
- pre-prompt-inject-scan: injection.mode (env var still takes precedence)
- post-session-guard: trifecta.mode, window_size, long_horizon_window
- pre-edit-secrets: secrets.additional_patterns
- pre-bash-destructive: destructive.additional_blocked
- pre-write-pathguard: pathguard.additional_protected
- pre-install-supply-chain: supply_chain.additional_blocked_packages
- post-mcp-verify: mcp.volume_threshold_bytes, mcp.trusted_servers

Backward compatible: no policy file = identical behavior to v5.1.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-10 13:37:02 +02:00
commit 8ec320f40c
9 changed files with 300 additions and 13 deletions

View file

@ -20,6 +20,7 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { scanForInjection } from '../../scanners/lib/injection-patterns.mjs';
import { checkDescriptionDrift } from '../../scanners/lib/mcp-description-cache.mjs';
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
// ---------------------------------------------------------------------------
// Secret patterns — same set as pre-edit-secrets.mjs so any secret that
@ -73,7 +74,7 @@ const MIN_INJECTION_SCAN_LENGTH = 100;
// a session. Warns when a single tool produces disproportionate output.
// State file: ${os.tmpdir()}/llm-security-mcp-volume-${ppid}.json
// ---------------------------------------------------------------------------
const MCP_TOOL_VOLUME_THRESHOLD = 100_000; // 100 KB from a single MCP tool
const MCP_TOOL_VOLUME_THRESHOLD = getPolicyValue('mcp', 'volume_threshold_bytes', 100_000);
const VOLUME_STATE_FILE = join(tmpdir(), `llm-security-mcp-volume-${process.ppid}.json`);
// ---------------------------------------------------------------------------
@ -199,6 +200,11 @@ if (!outputText.trim()) {
const advisories = [];
const isBash = toolName === 'Bash';
// Policy: trusted MCP servers are exempt from volume tracking and drift checks
const trustedServers = new Set(getPolicyValue('mcp', 'trusted_servers', []));
const mcpServerName = toolName.includes('mcp__') ? toolName.split('__')[1] : null;
const isTrustedMcp = mcpServerName && trustedServers.has(mcpServerName);
// =========================================================================
// Bash-specific checks: secrets, external URLs, large MCP output
// These checks are only relevant for shell command output.
@ -322,7 +328,7 @@ if (isHtmlSource && outputText.length >= MIN_INJECTION_SCAN_LENGTH) {
// Only relevant for MCP tools that provide a description in tool_input.
// =========================================================================
const isMcpTool = toolName?.startsWith('mcp__');
if (isMcpTool) {
if (isMcpTool && !isTrustedMcp) {
const description = toolInput?.description || toolInput?.tool_description || '';
if (description && typeof description === 'string' && description.length > 10) {
try {
@ -345,7 +351,7 @@ if (isMcpTool) {
// Tracks cumulative output size per MCP tool within a session. Warns when
// a single tool produces disproportionate output (>100 KB cumulative).
// =========================================================================
if (isMcpTool && outputText.length > 0) {
if (isMcpTool && !isTrustedMcp && outputText.length > 0) {
const volState = loadVolumeState();
volState.volumes[toolName] = (volState.volumes[toolName] || 0) + outputText.length;
const toolTotal = volState.volumes[toolName];