feat(ruleset): Phase 2 (task 3) — rules as governed data, severity from policy

Externalize detection rules to versioned TOML and remove rule-local severity, so
rules detect and the central CWE policy decides severity — fixing "rules too
influential / not centrally managed". Zero-dependency; detection is byte-identical
(benchmark recall 44/47 = 93.6%, FP 0% unchanged) and the 90/10 gate holds.

- RulesetStore (ruleset/store.py + ruleset/<lang>/rules.toml): PatternRule as data
  (rule_id, title, languages, cwe, tags, pattern, status, min_evidence_level,
  precision_estimate, version) with NO severity; load_ruleset (sorted, with
  optional rule_policy.json ledger overlay) and a stdlib TOML write_ruleset. The
  32 rules were generated from the prior PATTERN_RULES with a verified exact
  round-trip (pattern/cwe/languages/tags/title).
- findings.py: drop the hardcoded PatternRule class + PATTERN_RULES tuple; load the
  default ruleset from TOML; quick_scan_findings(root, targets, rankings, ruleset,
  policy) resolves each finding's severity from policy via resolve_severity
  (SEVERITY_LABEL maps 0-5 -> string) and tags findings with their rule status.
- Three-status volume control: StaticFinding gains status (default "enabled");
  disabled rules never fire; shadow findings are partitioned out of the report and
  score in the CLI (written to shadow_findings.json) — no shadow rules by default.
- Emission floors (RulesetConfig.min_emit_priority / min_emit_precision, default
  0.0 = emit all) replace "every match is a finding".
- CLI loads the ruleset as a stage and uses it for the fail-loud policy check,
  detection, hunter pool, and the score CWE map. Wheel ships the ruleset TOML.

85 tests pass; ruff + mypy clean. Spec tasks 3.1-3.4 checked off.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-06-22 15:27:16 +02:00
co-authored by Claude Opus 4.8
parent fb7594a3ed
commit 444d8c9490
15 changed files with 645 additions and 347 deletions
+119
View File
@@ -0,0 +1,119 @@
[[rule]]
rule_id = "python-flask-debug"
title = "Flask debug mode enabled needs review"
languages = ["python"]
cwe = "CWE-489"
tags = ["insecure_default"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\.run\s*\([^\n]*(debug)\s*=\s*True'''
[[rule]]
rule_id = "python-os-command"
title = "Python OS command execution needs review"
languages = ["python"]
cwe = "CWE-78"
tags = ["syscall_entry"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\b(os\.system|os\.popen|os\.execl|os\.execlp|os\.execv|os\.execvp)\s*\('''
[[rule]]
rule_id = "python-path-traversal"
title = "Python file open with external input needs review"
languages = ["python"]
cwe = "CWE-22"
tags = ["filesystem_entry"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\b(open)\s*\(\s*[^\n]*(?:request\.|req\.|argv|input\s*\(|params\[|GET\[|POST\[)'''
[[rule]]
rule_id = "python-shell-true"
title = "Subprocess shell execution needs review"
languages = ["python"]
cwe = "CWE-78"
tags = ["syscall_entry"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''subprocess\.[a-zA-Z_]+\s*\([^\n]*shell\s*=\s*True'''
[[rule]]
rule_id = "python-sql-injection"
title = "Python SQL string composition needs review"
languages = ["python"]
cwe = "CWE-89"
tags = ["injection"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''(\.execute)\s*\(\s*[^\n]*(?:[\"']\s*%\s*[(A-Za-z_]|\.format\s*\(|[\"']\s*\+|\bf[\"'])'''
[[rule]]
rule_id = "python-ssrf"
title = "Python outbound request with external input needs review"
languages = ["python"]
cwe = "CWE-918"
tags = ["network_entry"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\b(requests\.(?:get|post|put|delete|head|patch)|urllib\.request\.urlopen|urlopen)\s*\(\s*[^\n]*(?:request\.|req\.|argv|input\s*\()'''
[[rule]]
rule_id = "python-ssti"
title = "Python server-side template injection needs review"
languages = ["python"]
cwe = "CWE-94"
tags = ["injection"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\b(render_template_string)\s*\('''
[[rule]]
rule_id = "python-unsafe-deserialization"
title = "Unsafe Python deserialization needs review"
languages = ["python"]
cwe = "CWE-502"
tags = ["deserialization"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\b(pickle\.loads|pickle\.load|cPickle\.loads|yaml\.load|marshal\.loads)\s*\('''
[[rule]]
rule_id = "python-unsafe-eval"
title = "Dynamic Python execution needs review"
languages = ["python"]
cwe = "CWE-95"
tags = ["syscall_entry"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\b(eval|exec)\s*\('''
[[rule]]
rule_id = "python-weak-hash"
title = "Python weak hash needs review"
languages = ["python"]
cwe = "CWE-327"
tags = ["crypto"]
status = "enabled"
min_evidence_level = "static_corroboration"
precision_estimate = 0.0
version = "1"
pattern = '''\bhashlib\.(md5|sha1)\s*\('''