torusguard 0.9.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.torusguard/.manifest.json +98 -79
  2. package/.torusguard/config/torusguard.json +1 -1
  3. package/.torusguard/references/csharp-security.md +41 -0
  4. package/.torusguard/references/go-security.md +41 -0
  5. package/.torusguard/references/java-security.md +40 -0
  6. package/.torusguard/references/polyglot-security-matrix.md +25 -0
  7. package/.torusguard/references/rust-security.md +40 -0
  8. package/.torusguard/rules/custom/.gitkeep +1 -0
  9. package/.torusguard/rules/custom/README.md +30 -0
  10. package/.torusguard/runs/report-latest.html +328 -0
  11. package/.torusguard/schemas/golden-recipe.schema.json +75 -0
  12. package/.torusguard/schemas/memory-context.schema.json +95 -0
  13. package/.torusguard/schemas/memory-event.schema.json +70 -0
  14. package/.torusguard/schemas/memory-pattern.schema.json +65 -0
  15. package/.torusguard/scripts/diff_guard.py +202 -15
  16. package/.torusguard/scripts/finding_scorer.py +199 -86
  17. package/.torusguard/scripts/html_reporter.py +628 -0
  18. package/.torusguard/scripts/manifest_builder.py +7 -5
  19. package/.torusguard/scripts/memory_engine.py +1334 -0
  20. package/.torusguard/scripts/monorepo_detector.py +123 -10
  21. package/.torusguard/scripts/rules_sync.py +321 -0
  22. package/.torusguard/scripts/run_manager.py +171 -89
  23. package/.torusguard/scripts/stack_detect.py +458 -26
  24. package/.torusguard/skills/torusguard/SKILL.md +4 -3
  25. package/.torusguard/skills/torusguard/bootstrap.py +344 -56
  26. package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
  27. package/.torusguard/skills/torusguard/references/go-security.md +41 -0
  28. package/.torusguard/skills/torusguard/references/java-security.md +40 -0
  29. package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  30. package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
  31. package/.torusguard/skills/torusguard-audit/SKILL.md +25 -25
  32. package/.torusguard/skills/torusguard-harden/SKILL.md +3 -3
  33. package/.torusguard/skills/torusguard-recheck/SKILL.md +3 -3
  34. package/.torusguard/workflows/memory.md +53 -0
  35. package/README.md +307 -401
  36. package/bin/torusguard.js +204 -6
  37. package/package.json +1 -1
  38. package/skills/torusguard/SKILL.md +4 -3
  39. package/skills/torusguard/bootstrap.py +68 -4
  40. package/skills/torusguard/payload/.manifest.json +98 -79
  41. package/skills/torusguard/payload/config/torusguard.json +1 -1
  42. package/skills/torusguard/payload/references/csharp-security.md +41 -0
  43. package/skills/torusguard/payload/references/go-security.md +41 -0
  44. package/skills/torusguard/payload/references/java-security.md +40 -0
  45. package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
  46. package/skills/torusguard/payload/references/rust-security.md +40 -0
  47. package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
  48. package/skills/torusguard/payload/rules/custom/README.md +30 -0
  49. package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
  50. package/skills/torusguard/payload/schemas/memory-context.schema.json +95 -0
  51. package/skills/torusguard/payload/schemas/memory-event.schema.json +70 -0
  52. package/skills/torusguard/payload/schemas/memory-pattern.schema.json +65 -0
  53. package/skills/torusguard/payload/scripts/diff_guard.py +202 -15
  54. package/skills/torusguard/payload/scripts/finding_scorer.py +199 -86
  55. package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
  56. package/skills/torusguard/payload/scripts/manifest_builder.py +7 -5
  57. package/skills/torusguard/payload/scripts/memory_engine.py +1334 -0
  58. package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
  59. package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
  60. package/skills/torusguard/payload/scripts/run_manager.py +171 -89
  61. package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
  62. package/skills/torusguard/payload/skills/torusguard/SKILL.md +4 -3
  63. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +344 -56
  64. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
  65. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
  66. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
  67. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  68. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
  69. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +25 -25
  70. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +3 -3
  71. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +3 -3
  72. package/skills/torusguard/payload/workflows/memory.md +53 -0
  73. package/skills/torusguard-audit/SKILL.md +25 -25
  74. package/skills/torusguard-harden/SKILL.md +3 -3
  75. package/skills/torusguard-recheck/SKILL.md +3 -3
  76. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  77. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  78. package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
  79. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  80. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Content-Aware Diff Line Scanner (v0.9.3)
4
- Evaluates unified diff patches against security invariants before harden or apply.
3
+ TorusGuard Content-Aware Diff Line Scanner (v1.0.0)
4
+ Evaluates unified diff patches against security invariants before harden or apply,
5
+ including regression watch checks against persistent memory patterns.
5
6
  Pure Python 3.10+ standard library (zero external dependencies).
6
7
  """
7
8
 
@@ -14,12 +15,39 @@ from pathlib import Path
14
15
  from typing import Dict, List, Any, Optional
15
16
 
16
17
  BYPASS_PATTERNS = [
17
- (re.compile(r'(?:#|//)\s*(?:TODO\s*:?\s*)?(?:bypass|skip|disable)[_\s]*(?:auth|security|check|guard|filter|csrf|tenant)', re.IGNORECASE), "Suspicious authentication or security check bypass comment"),
18
+ (re.compile(r'(?:#|//|/\*)\s*(?:TODO\s*:?\s*)?(?:bypass|skip|disable)[_\s]*(?:auth|security|check|guard|filter|csrf|tenant)', re.IGNORECASE), "Suspicious authentication or security check bypass comment"),
19
+ # TLS / Certificate validation bypasses across Python, Go, Node, Java, C#, PHP, Rust
18
20
  (re.compile(r'\bverify\s*=\s*False\b'), "Disabled TLS certificate verification (verify=False)"),
21
+ (re.compile(r'\bInsecureSkipVerify\s*:\s*true\b'), "Disabled Go TLS certificate verification (InsecureSkipVerify: true)"),
22
+ (re.compile(r'\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*[\'"]?0[\'"]?'), "Disabled Node.js TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0)"),
23
+ (re.compile(r'\brejectUnauthorized\s*:\s*false\b'), "Disabled Node.js TLS rejectUnauthorized"),
24
+ (re.compile(r'\b(?:TrustAllStrategy|NoopHostnameVerifier|DangerousAcceptAnyServerCertificateValidator)\b'), "Disabled Java / C# TLS certificate validation"),
25
+ (re.compile(r'\bServerCertificateCustomValidationCallback\s*=\s*(?:\([^)]*\)\s*=>\s*true|HttpClientHandler\.DangerousAcceptAnyServerCertificateValidator)'), "Disabled C# TLS certificate callback"),
26
+ (re.compile(r'\bCURLOPT_SSL_VERIFYPEER\s*(?:=>|,)\s*(?:false|0)\b', re.IGNORECASE), "Disabled PHP curl SSL verification (CURLOPT_SSL_VERIFYPEER => false)"),
27
+ (re.compile(r'\bdanger_accept_invalid_certs\s*\(\s*true\s*\)'), "Disabled Rust reqwest TLS certificate validation"),
28
+ # Auth & CSRF bypasses
19
29
  (re.compile(r'\b(?:skip_auth|bypass_auth|disable_auth|allow_all|permit_all)\s*=\s*True\b', re.IGNORECASE), "Explicit security bypass flag enabled"),
20
30
  (re.compile(r'@(?:csrf_exempt|allow_anonymous|disable_token_check)\b'), "Decorator disabling route-level security or CSRF protection"),
31
+ (re.compile(r'\[(?:AllowAnonymous|Authorize\s*\(\s*Roles\s*=\s*"\*"\s*\))\]'), "Attribute disabling ASP.NET authentication or authorization"),
32
+ (re.compile(r'\.csrf\(\)\.disable\(\)|\.csrf\([^)]*AbstractHttpConfigurer::disable\)'), "Spring Security CSRF protection explicitly disabled"),
33
+ # CORS wildcard with credentials
21
34
  (re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled in patch"),
22
- (re.compile(r'(?:#|//)\s*nosec\b', re.IGNORECASE), "Suppression comment (nosec) hiding potential vulnerability")
35
+ (re.compile(r'@CrossOrigin\s*\(\s*(?:origins\s*=\s*)?["\']\*["\']'), "Wildcard CORS origin annotation in Java/Spring"),
36
+ # Native dangerous constructs
37
+ (re.compile(r'\bextract\s*\(\s*\$_(?:POST|GET|REQUEST)\s*\)'), "Dangerous PHP extract() on untrusted user input"),
38
+ (re.compile(r'\bunsafe\s*\{'), "Unvetted Rust unsafe block introduced in patch"),
39
+ # Polyglot additional security bypasses (Python, Ruby, Java, Kotlin, Elixir, Dart, C++)
40
+ (re.compile(r'\bSESSION_COOKIE_SECURE\s*=\s*False\b'), "Session cookie secure flag disabled"),
41
+ (re.compile(r'\bparams\.permit!'), "Rails strong parameters mass assignment bypass (params.permit!)"),
42
+ (re.compile(r'\b(?:csrf(?:\(\))?\.disable\(\)|\.csrf\(\)\.disable\(\)|\.csrf\([^)]*AbstractHttpConfigurer::disable\))'), "CSRF protection explicitly disabled"),
43
+ (re.compile(r'\b(?:verify_peer\s*:\s*false|verify\s*:\s*:verify_none)\b'), "Disabled Elixir TLS certificate verification"),
44
+ (re.compile(r'\bbadCertificateCallback\s*='), "Disabled Dart/Flutter badCertificateCallback validation"),
45
+ (re.compile(r'\bSSL_VERIFY_NONE\b'), "Disabled C/C++ OpenSSL certificate verification (SSL_VERIFY_NONE)"),
46
+ (re.compile(r'\bpermitAll\(\)'), "Unrestricted access granted via permitAll()"),
47
+ (re.compile(r'\bverify\s*(?:=>|=)\s*false\b', re.IGNORECASE), "Disabled TLS/HTTP client verification (verify => false)"),
48
+ # Linters and scanners suppressions
49
+ (re.compile(r'(?:#|//|/\*)\s*nosec\b', re.IGNORECASE), "Suppression comment (nosec) hiding potential vulnerability"),
50
+ (re.compile(r'@SuppressWarnings\s*\(\s*["\'](?:security|all)["\']\s*\)'), "Java security suppression annotation")
23
51
  ]
24
52
 
25
53
  SECRET_PATTERNS = [
@@ -34,20 +62,76 @@ TENANT_FILTER_PATTERNS = [
34
62
  re.compile(r'\.filter\([^)]*\btenant\s*='),
35
63
  re.compile(r'\.filter_by\([^)]*\btenant\s*='),
36
64
  re.compile(r'\bwhere\s+tenant_id\s*='),
37
- re.compile(r'\btenant_id\s*==?')
65
+ re.compile(r'\btenant_id\s*==?'),
66
+ re.compile(r'\btenantId\s*:'),
67
+ re.compile(r'\.Where\([^)]*(?:tenant_id|TenantId)'),
68
+ re.compile(r'\bwhere\s*:\s*\{[^}]*tenant')
38
69
  ]
39
70
 
40
71
 
41
- def audit_diff(diff_content: str) -> Dict[str, Any]:
72
+ def check_memory_regressions(
73
+ current_file: str,
74
+ current_line: int,
75
+ added_content: str,
76
+ root_dir: Optional[Path] = None
77
+ ) -> List[Dict[str, Any]]:
78
+ """
79
+ Check if the added line re-introduces a vulnerability in a file
80
+ that is tracked under active Regression Watch in .torusguard/memory/patterns.json.
81
+ """
82
+ violations = []
83
+ base = Path(root_dir or Path.cwd()).resolve()
84
+ torusguard_dir = base / ".torusguard"
85
+ if not torusguard_dir.exists():
86
+ for parent in base.parents:
87
+ if (parent / ".torusguard").exists():
88
+ torusguard_dir = parent / ".torusguard"
89
+ break
90
+
91
+ patterns_file = torusguard_dir / "memory" / "patterns.json"
92
+ if not patterns_file.exists():
93
+ return violations
94
+
95
+ try:
96
+ with open(patterns_file, "r", encoding="utf-8") as f:
97
+ patterns = json.load(f)
98
+ except Exception:
99
+ return violations
100
+
101
+ clean_file = current_file.replace("\\", "/")
102
+ for pat in patterns:
103
+ if pat.get("pattern_type") == "regression_watch":
104
+ affected = [f.replace("\\", "/") for f in pat.get("affected_files", [])]
105
+ if clean_file in affected or any(clean_file.endswith(a) for a in affected):
106
+ rule_id = pat.get("rule_id", "TG-REG")
107
+ desc = pat.get("description", "Regression watch violation")
108
+ violations.append({
109
+ "rule_id": "TG-DIFF-004",
110
+ "category": "Regression Watch",
111
+ "severity": "HIGH",
112
+ "file": current_file,
113
+ "line": current_line,
114
+ "content": added_content.strip(),
115
+ "description": f"File {current_file} matches active Regression Watch for {rule_id}: {desc}"
116
+ })
117
+ return violations
118
+
119
+
120
+ def audit_diff(
121
+ diff_content: str,
122
+ check_memory: bool = False,
123
+ root_dir: Optional[Path] = None
124
+ ) -> Dict[str, Any]:
42
125
  """
43
126
  Parses a unified diff and returns detected violations.
127
+ Optionally checks against active regression watch patterns in memory.
44
128
  """
45
129
  lines = diff_content.splitlines()
46
130
  violations: List[Dict[str, Any]] = []
47
-
131
+
48
132
  current_file = "unknown"
49
133
  current_line = 0
50
-
134
+
51
135
  additions: List[tuple[int, str]] = []
52
136
  deletions: List[tuple[int, str]] = []
53
137
 
@@ -68,7 +152,7 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
68
152
  current_line += 1
69
153
  added_content = line[1:]
70
154
  additions.append((current_line, added_content))
71
-
155
+
72
156
  # Check bypass patterns
73
157
  for pat, desc in BYPASS_PATTERNS:
74
158
  if pat.search(added_content):
@@ -81,7 +165,7 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
81
165
  "content": added_content.strip(),
82
166
  "description": desc
83
167
  })
84
-
168
+
85
169
  # Check secret ingestion
86
170
  for pat, desc in SECRET_PATTERNS:
87
171
  if pat.search(added_content):
@@ -95,6 +179,11 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
95
179
  "description": desc
96
180
  })
97
181
 
182
+ # Check memory regression watch if requested
183
+ if check_memory:
184
+ reg_violations = check_memory_regressions(current_file, current_line, added_content, root_dir=root_dir)
185
+ violations.extend(reg_violations)
186
+
98
187
  elif line.startswith('-') and not line.startswith('---'):
99
188
  deleted_content = line[1:]
100
189
  deletions.append((line_idx, deleted_content))
@@ -127,7 +216,7 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
127
216
  }
128
217
 
129
218
 
130
- def audit_diff_file(file_path: str) -> Dict[str, Any]:
219
+ def audit_diff_file(file_path: str, check_memory: bool = False, root_dir: Optional[Path] = None) -> Dict[str, Any]:
131
220
  path = Path(file_path)
132
221
  if not path.is_file():
133
222
  return {
@@ -136,23 +225,120 @@ def audit_diff_file(file_path: str) -> Dict[str, Any]:
136
225
  "violations": []
137
226
  }
138
227
  content = path.read_text(encoding="utf-8", errors="replace")
139
- return audit_diff(content)
228
+ return audit_diff(content, check_memory=check_memory, root_dir=root_dir)
229
+
230
+
231
+ def check_diff_content(diff_content: str, file_path: str = "") -> List[Dict[str, Any]]:
232
+ """Convenience helper to audit diff string directly and return list of violations."""
233
+ res = audit_diff(diff_content)
234
+ return res.get("violations", [])
235
+
236
+
237
+ def install_pre_commit_hook(root_dir: Optional[Path] = None) -> bool:
238
+ """Install TorusGuard pre-commit security hook into .git/hooks/pre-commit."""
239
+ base = Path(root_dir or Path.cwd()).resolve()
240
+ git_dir = base / ".git"
241
+ if not git_dir.exists():
242
+ for parent in base.parents:
243
+ if (parent / ".git").exists():
244
+ git_dir = parent / ".git"
245
+ break
246
+ if not git_dir.exists():
247
+ print("[ERROR] Diff Guard: Not a git repository (no .git directory found).", file=sys.stderr)
248
+ return False
249
+
250
+ hooks_dir = git_dir / "hooks"
251
+ hooks_dir.mkdir(parents=True, exist_ok=True)
252
+ hook_file = hooks_dir / "pre-commit"
253
+
254
+ hook_script = """#!/usr/bin/env sh
255
+ # TorusGuard Pre-Commit Security Diff Guard
256
+ # Automatically blocks commits containing security bypasses, secret leaks, or tenant stripping.
257
+ python .torusguard/scripts/diff_guard.py --pre-commit
258
+ """
259
+ try:
260
+ hook_file.write_text(hook_script, encoding="utf-8")
261
+ if os.name != "nt":
262
+ import stat
263
+ os.chmod(hook_file, hook_file.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
264
+ print(f"[SUCCESS] Installed TorusGuard pre-commit hook at: {hook_file}")
265
+ return True
266
+ except Exception as e:
267
+ print(f"[ERROR] Diff Guard: Failed to install pre-commit hook: {e}", file=sys.stderr)
268
+ return False
269
+
270
+
271
+ def uninstall_pre_commit_hook(root_dir: Optional[Path] = None) -> bool:
272
+ """Uninstall TorusGuard pre-commit hook from .git/hooks/pre-commit."""
273
+ base = Path(root_dir or Path.cwd()).resolve()
274
+ git_dir = base / ".git"
275
+ if not git_dir.exists():
276
+ for parent in base.parents:
277
+ if (parent / ".git").exists():
278
+ git_dir = parent / ".git"
279
+ break
280
+ if not git_dir.exists():
281
+ print("[WARN] Diff Guard: No .git directory found.", file=sys.stderr)
282
+ return False
283
+
284
+ hook_file = git_dir / "hooks" / "pre-commit"
285
+ if hook_file.exists():
286
+ try:
287
+ hook_file.unlink()
288
+ print("[SUCCESS] Uninstalled TorusGuard pre-commit hook.")
289
+ return True
290
+ except Exception as e:
291
+ print(f"[ERROR] Diff Guard: Failed to delete hook file: {e}", file=sys.stderr)
292
+ return False
293
+ else:
294
+ print("[INFO] Diff Guard: No pre-commit hook currently installed.")
295
+ return True
140
296
 
141
297
 
142
298
  def main():
143
299
  parser = argparse.ArgumentParser(description="TorusGuard Unified Diff Security Scanner")
144
300
  parser.add_argument("diff_path", nargs="?", help="Path to unified diff file (or read stdin if omitted)")
301
+ parser.add_argument("--check-memory", action="store_true", help="Check additions against persistent memory regression watch patterns")
302
+ parser.add_argument("--pre-commit", action="store_true", help="Audit git staged diff (--cached) for pre-commit hook")
303
+ parser.add_argument("--install-hook", action="store_true", help="Install TorusGuard pre-commit hook into .git/hooks/pre-commit")
304
+ parser.add_argument("--uninstall-hook", action="store_true", help="Uninstall TorusGuard pre-commit hook")
145
305
  parser.add_argument("--json", action="store_true", help="Output results as JSON")
146
306
  args = parser.parse_args()
147
307
 
148
- if args.diff_path:
149
- res = audit_diff_file(args.diff_path)
308
+ if args.install_hook:
309
+ sys.exit(0 if install_pre_commit_hook() else 1)
310
+
311
+ if args.uninstall_hook:
312
+ sys.exit(0 if uninstall_pre_commit_hook() else 1)
313
+
314
+ if args.pre_commit:
315
+ import subprocess
316
+ try:
317
+ res_git = subprocess.run(
318
+ ["git", "diff", "--cached"],
319
+ capture_output=True,
320
+ text=True,
321
+ encoding="utf-8",
322
+ errors="replace"
323
+ )
324
+ diff_text = res_git.stdout
325
+ except Exception as e:
326
+ print(f"[ERROR] Diff Guard: Could not read git staged diff: {e}", file=sys.stderr)
327
+ sys.exit(1)
328
+
329
+ if not diff_text.strip():
330
+ print("[PASS] Diff Guard (pre-commit): No staged changes detected.")
331
+ sys.exit(0)
332
+
333
+ res = audit_diff(diff_text, check_memory=True)
334
+ elif args.diff_path:
335
+ res = audit_diff_file(args.diff_path, check_memory=args.check_memory)
150
336
  else:
151
337
  if sys.stdin.isatty():
152
338
  parser.print_help()
153
339
  sys.exit(1)
154
340
  diff_text = sys.stdin.read()
155
- res = audit_diff(diff_text)
341
+ res = audit_diff(diff_text, check_memory=args.check_memory)
156
342
 
157
343
  if args.json:
158
344
  print(json.dumps(res, indent=2))
@@ -169,3 +355,4 @@ def main():
169
355
 
170
356
  if __name__ == "__main__":
171
357
  main()
358
+
@@ -1,86 +1,199 @@
1
- #!/usr/bin/env python3
2
- """
3
- TorusGuard 5-Factor Finding Confidence Scorer
4
- Evaluates finding confidence against the objective 0–100 scoring model.
5
- """
6
-
7
- import sys
8
- import json
9
- import argparse
10
- from typing import Dict, Any, Tuple
11
-
12
- def compute_confidence_score(
13
- evidence_quality: int = 35,
14
- reproduction_success: int = 0,
15
- independent_confirmations: int = 5,
16
- environmental_clarity: int = 15,
17
- manual_review_status: int = 0
18
- ) -> Tuple[int, str, Dict[str, int]]:
19
- """
20
- Computes total score and assigns confidence band.
21
- Max points:
22
- - evidence_quality: 35
23
- - reproduction_success: 25
24
- - independent_confirmations: 15
25
- - environmental_clarity: 15
26
- - manual_review_status: 10
27
- """
28
- eq = min(max(evidence_quality, 0), 35)
29
- rs = min(max(reproduction_success, 0), 25)
30
- ic = min(max(independent_confirmations, 0), 15)
31
- ec = min(max(environmental_clarity, 0), 15)
32
- mr = min(max(manual_review_status, 0), 10)
33
-
34
- total = eq + rs + ic + ec + mr
35
-
36
- if total >= 90:
37
- band = "Confirmed"
38
- elif total >= 70:
39
- band = "High Confidence"
40
- elif total >= 50:
41
- band = "Medium Confidence"
42
- else:
43
- band = "Needs Review"
44
-
45
- factors = {
46
- "evidence_quality": eq,
47
- "reproduction_success": rs,
48
- "independent_confirmations": ic,
49
- "environmental_clarity": ec,
50
- "manual_review_status": mr,
51
- "total_score": total,
52
- "classification_band": band
53
- }
54
- return total, band, factors
55
-
56
-
57
- def main():
58
- parser = argparse.ArgumentParser(description="TorusGuard Confidence Scorer")
59
- parser.add_argument("--eq", type=int, default=35, help="Evidence quality score (0-35)")
60
- parser.add_argument("--rs", type=int, default=0, help="Reproduction success score (0-25)")
61
- parser.add_argument("--ic", type=int, default=5, help="Independent confirmations score (0-15)")
62
- parser.add_argument("--ec", type=int, default=15, help="Environmental clarity score (0-15)")
63
- parser.add_argument("--mr", type=int, default=0, help="Manual review status score (0-10)")
64
- parser.add_argument("--json", action="store_true", help="Output JSON")
65
- args = parser.parse_args()
66
-
67
- total, band, factors = compute_confidence_score(
68
- evidence_quality=args.eq,
69
- reproduction_success=args.rs,
70
- independent_confirmations=args.ic,
71
- environmental_clarity=args.ec,
72
- manual_review_status=args.mr
73
- )
74
-
75
- if args.json:
76
- print(json.dumps(factors, indent=2))
77
- else:
78
- print(f"Total Confidence Score: {total}/100")
79
- print(f"Classification Band: {band}")
80
- print("Factor Breakdown:")
81
- for k, v in factors.items():
82
- if k not in ("total_score", "classification_band"):
83
- print(f" - {k}: {v}")
84
-
85
- if __name__ == "__main__":
86
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Finding Confidence Scorer (v1.0.0)
4
+ Evaluates finding confidence against the objective 0–100 scoring model,
5
+ augmented by persistent memory patterns and false-positive suppression.
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import argparse
11
+ from pathlib import Path
12
+ from typing import Dict, Any, Tuple, Optional
13
+
14
+
15
+ def compute_memory_boost(
16
+ rule_id: str,
17
+ file_path: Optional[str] = None,
18
+ root_dir: Optional[Path] = None
19
+ ) -> int:
20
+ """
21
+ Query .torusguard/memory/patterns.json to calculate memory confidence modifier:
22
+ - False positive class: -30 (heavily suppressed)
23
+ - Regression watch: +15 (high alert / known recurrence)
24
+ - Recurring fix / Common vulnerability: +10
25
+ - Security idiom: +5
26
+ - Multi-file pattern match bonus: +5
27
+ """
28
+ if not rule_id:
29
+ return 0
30
+
31
+ base = Path(root_dir or Path.cwd()).resolve()
32
+ # Search for .torusguard
33
+ torusguard_dir = base / ".torusguard"
34
+ if not torusguard_dir.exists():
35
+ for parent in base.parents:
36
+ if (parent / ".torusguard").exists():
37
+ torusguard_dir = parent / ".torusguard"
38
+ break
39
+
40
+ patterns_file = torusguard_dir / "memory" / "patterns.json"
41
+ if not patterns_file.exists():
42
+ return 0
43
+
44
+ try:
45
+ with open(patterns_file, "r", encoding="utf-8") as f:
46
+ patterns = json.load(f)
47
+ except (json.JSONDecodeError, OSError, UnicodeDecodeError):
48
+ return 0
49
+
50
+ boost = 0
51
+ matched_file = False
52
+ matched_dir = False
53
+ target_path = Path(file_path) if file_path else None
54
+
55
+ for pat in patterns:
56
+ if pat.get("rule_id") == rule_id:
57
+ ptype = pat.get("pattern_type")
58
+ if ptype == "false_positive_class":
59
+ boost -= 30
60
+ elif ptype == "regression_watch":
61
+ boost += 15
62
+ elif ptype in ("recurring_fix", "common_vulnerability"):
63
+ boost += 10
64
+ elif ptype == "security_idiom":
65
+ boost += 5
66
+
67
+ # File-specific and directory-level correlation bonus
68
+ affected = pat.get("affected_files", [])
69
+ if file_path and file_path in affected:
70
+ matched_file = True
71
+ elif target_path and any(target_path.parent == Path(af).parent for af in affected if af):
72
+ matched_dir = True
73
+
74
+ if matched_file and boost > 0:
75
+ boost += 5
76
+ elif matched_file and boost < 0:
77
+ boost -= 10
78
+ elif matched_dir and boost > 0:
79
+ boost += 2
80
+ elif matched_dir and boost < 0:
81
+ boost -= 4
82
+
83
+ return boost
84
+
85
+
86
+ def is_test_path(file_path: Optional[str]) -> bool:
87
+ """Detects whether a file path belongs to a test suite, fixture, or mock."""
88
+ if not file_path:
89
+ return False
90
+ clean = file_path.replace("\\", "/").lower()
91
+ test_markers = [
92
+ "/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
93
+ "/e2e/", "__tests__", "test_", "_test.", ".test.", ".spec.", "testcase"
94
+ ]
95
+ return any(marker in clean for marker in test_markers) or clean.endswith(("_test.go", "test.java", "test.py", ".spec.ts", ".test.ts", ".spec.js", ".test.js"))
96
+
97
+
98
+ def compute_confidence_score(
99
+ evidence_quality: int = 35,
100
+ reproduction_success: int = 0,
101
+ independent_confirmations: int = 5,
102
+ environmental_clarity: int = 15,
103
+ manual_review_status: int = 0,
104
+ memory_boost: int = 0,
105
+ rule_id: Optional[str] = None,
106
+ file_path: Optional[str] = None,
107
+ root_dir: Optional[Path] = None
108
+ ) -> Tuple[int, str, Dict[str, Any]]:
109
+ """
110
+ Computes total score and assigns confidence band.
111
+ Max points:
112
+ - evidence_quality: 35
113
+ - reproduction_success: 25
114
+ - independent_confirmations: 15
115
+ - environmental_clarity: 15
116
+ - manual_review_status: 10
117
+ - memory_boost: -30 to +20 (modifier from persistent memory)
118
+ - test_deduction: -30 if file is located in a test/mock path
119
+ Total is clamped to [0, 100].
120
+ """
121
+ eq = min(max(evidence_quality, 0), 35)
122
+ rs = min(max(reproduction_success, 0), 25)
123
+ ic = min(max(independent_confirmations, 0), 15)
124
+ ec = min(max(environmental_clarity, 0), 15)
125
+ mr = min(max(manual_review_status, 0), 10)
126
+
127
+ # Auto-calculate memory boost if rule_id provided and memory_boost == 0
128
+ eff_mem_boost = memory_boost
129
+ if rule_id and eff_mem_boost == 0:
130
+ eff_mem_boost = compute_memory_boost(rule_id, file_path=file_path, root_dir=root_dir)
131
+
132
+ # Test path noise suppression: -30 deduction for test mocks/fixtures
133
+ is_test = is_test_path(file_path)
134
+ test_deduction = -30 if is_test else 0
135
+
136
+ raw_total = eq + rs + ic + ec + mr + eff_mem_boost + test_deduction
137
+ total = min(max(raw_total, 0), 100)
138
+
139
+ if total >= 90:
140
+ band = "Confirmed"
141
+ elif total >= 70:
142
+ band = "High Confidence"
143
+ elif total >= 50:
144
+ band = "Medium Confidence"
145
+ else:
146
+ band = "Needs Review"
147
+
148
+ factors = {
149
+ "evidence_quality": eq,
150
+ "reproduction_success": rs,
151
+ "independent_confirmations": ic,
152
+ "environmental_clarity": ec,
153
+ "manual_review_status": mr,
154
+ "memory_boost": eff_mem_boost,
155
+ "test_exemption": is_test,
156
+ "test_deduction": test_deduction,
157
+ "total_score": total,
158
+ "classification_band": band
159
+ }
160
+ return total, band, factors
161
+
162
+
163
+ def main():
164
+ parser = argparse.ArgumentParser(description="TorusGuard Confidence Scorer")
165
+ parser.add_argument("--eq", type=int, default=35, help="Evidence quality score (0-35)")
166
+ parser.add_argument("--rs", type=int, default=0, help="Reproduction success score (0-25)")
167
+ parser.add_argument("--ic", type=int, default=5, help="Independent confirmations score (0-15)")
168
+ parser.add_argument("--ec", type=int, default=15, help="Environmental clarity score (0-15)")
169
+ parser.add_argument("--mr", type=int, default=0, help="Manual review status score (0-10)")
170
+ parser.add_argument("--memory-boost", type=int, default=0, help="Memory boost score (-30 to +20)")
171
+ parser.add_argument("--rule-id", type=str, help="TorusGuard rule ID for memory pattern matching")
172
+ parser.add_argument("--file", type=str, help="File path for correlation matching")
173
+ parser.add_argument("--json", action="store_true", help="Output JSON")
174
+ args = parser.parse_args()
175
+
176
+ total, band, factors = compute_confidence_score(
177
+ evidence_quality=args.eq,
178
+ reproduction_success=args.rs,
179
+ independent_confirmations=args.ic,
180
+ environmental_clarity=args.ec,
181
+ manual_review_status=args.mr,
182
+ memory_boost=args.memory_boost,
183
+ rule_id=args.rule_id,
184
+ file_path=args.file
185
+ )
186
+
187
+ if args.json:
188
+ print(json.dumps(factors, indent=2))
189
+ else:
190
+ print(f"Total Confidence Score: {total}/100")
191
+ print(f"Classification Band: {band}")
192
+ print("Factor Breakdown:")
193
+ for k, v in factors.items():
194
+ if k not in ("total_score", "classification_band"):
195
+ print(f" - {k}: {v}")
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()