ktg-plugin-marketplace/plugins/llm-security/scanners/lib/policy-loader.mjs
Kjell Tore Guttormsen 2c33e9cc64 feat(ci): add CI/CD integration — --fail-on, --compact, pipeline templates
Add threshold-based exit codes (--fail-on <severity>) and compact
output mode (--compact) to scan-orchestrator and CLI. Pipeline
templates for GitHub Actions, Azure DevOps, GitLab CI with SARIF
upload. CI/CD guide with Schrems II/NSM compliance documentation.
npm publish preparation (files whitelist, .npmignore). Policy ci
section for distributable CI defaults. Version 6.1.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 14:59:05 +02:00

150 lines
3.9 KiB
JavaScript

// policy-loader.mjs — Central policy file reader for distributable hook configuration
// Reads .llm-security/policy.json from project root. Falls back to defaults
// matching existing hardcoded behavior when no policy file exists.
// Zero external dependencies.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
// ---------------------------------------------------------------------------
// Default policy — matches all existing hardcoded values exactly
// ---------------------------------------------------------------------------
const DEFAULT_POLICY = Object.freeze({
version: '1.0',
injection: {
mode: 'block',
medium_advisory: true,
custom_patterns: [],
},
trifecta: {
mode: 'warn',
window_size: 20,
long_horizon_window: 100,
},
secrets: {
additional_patterns: [],
allowed_paths: [],
},
destructive: {
additional_blocked: [],
allowed_commands: [],
},
pathguard: {
additional_protected: [],
allowed_paths: [],
},
supply_chain: {
additional_blocked_packages: [],
trusted_registries: [],
},
mcp: {
trusted_servers: [],
volume_threshold_bytes: 100_000,
},
audit: {
log_path: null,
events: ['trifecta', 'injection', 'secrets', 'destructive'],
},
ci: {
failOn: null,
compact: false,
},
});
// Cache loaded policy per project root
const cache = new Map();
/**
* Resolve project root from env or cwd.
* @param {string} [explicitRoot]
* @returns {string}
*/
function resolveRoot(explicitRoot) {
return explicitRoot || process.env.CLAUDE_PROJECT_ROOT || process.cwd();
}
/**
* Deep merge two objects (source overrides target).
* @param {object} target
* @param {object} source
* @returns {object}
*/
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (
source[key] !== null &&
typeof source[key] === 'object' &&
!Array.isArray(source[key]) &&
typeof target[key] === 'object' &&
!Array.isArray(target[key])
) {
result[key] = deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
/**
* Load policy from .llm-security/policy.json.
* Returns defaults if no policy file exists or if parsing fails.
* Cached per project root (per process).
*
* @param {string} [projectRoot] - Explicit root, or derived from env/cwd
* @returns {object} Merged policy with defaults
*/
export function loadPolicy(projectRoot) {
const root = resolveRoot(projectRoot);
if (cache.has(root)) return cache.get(root);
const policyPath = join(root, '.llm-security', 'policy.json');
let policy;
try {
const raw = readFileSync(policyPath, 'utf-8');
const parsed = JSON.parse(raw);
policy = deepMerge(DEFAULT_POLICY, parsed);
} catch {
// No policy file or invalid JSON — use defaults
policy = { ...DEFAULT_POLICY };
}
cache.set(root, policy);
return policy;
}
/**
* Get a specific policy value with fallback.
* Environment variables ALWAYS take precedence over policy file values.
*
* @param {string} section - Policy section (e.g. 'injection', 'trifecta')
* @param {string} key - Key within section (e.g. 'mode', 'window_size')
* @param {*} defaultValue - Fallback if neither policy nor default has the value
* @param {string} [projectRoot] - Explicit root
* @returns {*}
*/
export function getPolicyValue(section, key, defaultValue, projectRoot) {
const policy = loadPolicy(projectRoot);
const sectionObj = policy[section];
if (sectionObj && key in sectionObj) return sectionObj[key];
return defaultValue;
}
/**
* Get the full default policy (for documentation/example generation).
* @returns {object}
*/
export function getDefaultPolicy() {
return JSON.parse(JSON.stringify(DEFAULT_POLICY));
}
/**
* Reset cache (for testing only).
*/
export function _resetCacheForTest() {
cache.clear();
}