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

@ -28,6 +28,10 @@ import {
queryOSV, extractOSVSeverity,
} from '../../scanners/lib/supply-chain-data.mjs';
import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs';
import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
// Policy-defined additional blocked packages (merged with built-in lists)
const POLICY_BLOCKED = new Set(getPolicyValue('supply_chain', 'additional_blocked_packages', []));
// ===========================================================================
// Read stdin
@ -119,10 +123,10 @@ async function checkNpm() {
for (const spec of packages) {
const { name, version } = parseSpec(spec);
if (isCompromised(NPM_COMPROMISED, name, version)) {
if (isCompromised(NPM_COMPROMISED, name, version) || POLICY_BLOCKED.has(name)) {
blocks.push(
`COMPROMISED: ${name}${version ? '@' + version : ''}\n` +
` Known supply chain attack. See: https://socket.dev/npm/package/${name}`
` ${POLICY_BLOCKED.has(name) ? 'Blocked by policy.' : 'Known supply chain attack.'} See: https://socket.dev/npm/package/${name}`
);
continue;
}
@ -325,10 +329,10 @@ async function checkPip() {
for (const spec of packages) {
const { name, version } = parsePipSpec(spec);
if (isCompromised(PIP_COMPROMISED, name, version)) {
if (isCompromised(PIP_COMPROMISED, name, version) || POLICY_BLOCKED.has(name)) {
blocks.push(
`COMPROMISED: ${name} (PyPI)\n` +
` Known malicious package (likely typosquat).\n` +
` ${POLICY_BLOCKED.has(name) ? 'Blocked by policy.' : 'Known malicious package (likely typosquat).'}\n` +
` See: https://pypi.org/project/${name}/`
);
continue;