Commit graph

6 commits

Author SHA1 Message Date
6d0f3c31fc
fix(policy): read policy.json only from the caller's own working tree
loadPolicy() read .llm-security/policy.json from whatever root it was
given, and every scanner passes the SCANNED TARGET: scan-orchestrator
(policyRoot = resolve(args.target)), entropy-scanner (thresholds and
suppression patterns), signature-scanner (sig.custom_rules_path and
enabled_families), trigger-scanner (phrase lists) and ast-taint-scanner
(enabled, python_path). A foreign/cloned target could raise its own
entropy thresholds, disable SIG families, supply its own SIG ruleset or
name the interpreter the AST scanner spawns — configuring the scan of
itself. Same defect class as S3b's .llm-security-ignore fix.

Chosen: move isOwnWorkingTree() to scanners/lib/own-working-tree.mjs (one
copy, reused by the orchestrator's ignore-file check) and make
loadPolicy() refuse an EXPLICIT root that is not the caller's own tree —
defaults plus one stderr line, same form as S3b — because one rule in one
function covers every scanner and a future call site cannot forget it.
The IMPLICIT root (CLAUDE_PROJECT_ROOT/cwd, what every hook uses) is the
caller's own project by construction and is read as before.
entropy-scanner's calibration.policy_source no longer reports an ignored
file as its source.

New tests/scanners/policy-scope.test.mjs was red on 0d37f5a (foreign
target: entropy finding silenced, custom SIG rule loaded, findings differ
from the same tree without policy.json, no stderr line) and is green now;
its own-tree scenario (known-positive) is green before and after. The 15
existing policy tests that placed own-tree fixtures under os.tmpdir() now
use tests/helpers/own-tree.mjs (fixture under $HOME, cwd set to it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 20:15:17 +02:00
b6af9b46df feat(llm-security)!: v8 Phase 3 step 1 - remove the deprecated mode env-vars
BREAKING CHANGE: the four LLM_SECURITY_* configuration env-vars deprecated in
v7.3.0 are removed. .llm-security/policy.json is now the only source:

  LLM_SECURITY_INJECTION_MODE     -> injection.mode
  LLM_SECURITY_TRIFECTA_MODE      -> trifecta.mode
  LLM_SECURITY_ESCALATION_WINDOW  -> trifecta.escalation_window
  LLM_SECURITY_AUDIT_LOG          -> audit.log_path
  LLM_SECURITY_DEPRECATION_QUIET  -> dies with the mechanism it silenced

Setting a removed var is now inert - it does not warn, and it does not
configure. Env-vars with no policy equivalent (PRECOMPACT_MODE,
PRECOMPACT_MAX_BYTES, UPDATE_CHECK, MCP_CACHE_FILE, IDE_ROOTS) are unaffected.

getPolicyValueWithEnvWarn and its one-shot stderr warning are deleted from
policy-loader.mjs, along with the module-scoped warned-var Set. The four call
sites collapse to getPolicyValue. getPolicyValue's JSDoc claimed "environment
variables ALWAYS take precedence" - it never read env itself, so that line
described the shim, and it is corrected rather than deleted.

User-facing hook strings that advertised a removed var as the escape hatch now
name the policy key instead: the inject-scan block reason, its warn-mode note,
both escalation-window advisories, and the trifecta block message. A blocked
user following the old text would have set a var that does nothing.

Tests. tests/lib/v8-env-removal.test.mjs is the regression gate and was written
failing first (8 of 12 red before the change). It pins the NEGATIVE - setting a
removed var does not alter the outcome - because that is the half that rots
silently: a re-introduced process.env read would leave every migrated positive
test green, since those configure through policy.json and never set the var at
all. One assertion walks hooks/scripts and scanners for `process.env.<removed>`
so the re-introduction is caught structurally, not only behaviourally.

The 44 env-driven test occurrences (18 inject-scan, 13+4 session-guard, 9
audit-trail) migrate to a throwaway .llm-security/policy.json via a new
runHookWithPolicy helper in hook-helper.mjs; audit-trail runs in-process, so it
supplies the same policy through CLAUDE_PROJECT_ROOT. The D3 mechanism tests in
policy-loader.test.mjs are deleted with the mechanism.

Suite 2039 tests, 2037 pass (+12 gate, -7 D3 mechanism). The 2 failures are the
known parallel-load timing flakes (pre-compact-scan size-cap,
pre-install-supply-chain F-3); both green when run isolated.

Remaining in Phase 3: posture-scanner TRIFECTA_MODE heuristic, riskScoreV1
removal, ghost-var cleanup, docs + migration note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BB4vXvwvtW4dxbPRd6vsez
2026-08-09 10:17:47 +02:00
f3aaf5479f fix(llm-security): scanner false-positives/negatives — trigger, toxic-flow, policy-loader (#38-#41,#57,#26)
#38 toxic-flow matched trifecta-leg keywords with bare includes(), so substrings ('url' in 'curl', 'key' in 'monkey', 'auth' in 'author') fabricated CRITICAL trifectas on benign components; now word-boundary matched. #40 TRG-broad fired HIGH on a bare any/all/every anywhere ('fix any lint errors'); the universal-claim regex now requires genuine universal phrasing. #41 TRG-baiting substring-matched ('any file' in 'many files'); now boundary-anchored. #39 the broad-name list missed multi-char generic names (helper/assistant/auto/general/agent/tool); widened coherently so it does not reintroduce #40. #57 the '(recovered from obfuscation)' label compared raw against a lowercased normal form, firing on any uppercase char; now gated on an explicit decode-changed flag.

#26 (same file) getPolicyValue used 'key in sectionObj' with no type guard, so a scalar section override in policy.json (e.g. {"injection":"block"}) threw an uncaught TypeError; now guarded to fall back to the default. Suite 1931/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:15:10 +02:00
Kjell Tore Guttormsen
7f921b3cf3 feat(policy-loader): 8.7 — env-var deprecation warnings (v8.0.0 removal) 2026-04-30 17:11:07 +02:00
Kjell Tore Guttormsen
b61c631975 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
Kjell Tore Guttormsen
367bae3939 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>
2026-04-10 13:37:02 +02:00