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.
- package/.torusguard/.manifest.json +98 -79
- package/.torusguard/config/torusguard.json +1 -1
- package/.torusguard/references/csharp-security.md +41 -0
- package/.torusguard/references/go-security.md +41 -0
- package/.torusguard/references/java-security.md +40 -0
- package/.torusguard/references/polyglot-security-matrix.md +25 -0
- package/.torusguard/references/rust-security.md +40 -0
- package/.torusguard/rules/custom/.gitkeep +1 -0
- package/.torusguard/rules/custom/README.md +30 -0
- package/.torusguard/runs/report-latest.html +328 -0
- package/.torusguard/schemas/golden-recipe.schema.json +75 -0
- package/.torusguard/schemas/memory-context.schema.json +95 -0
- package/.torusguard/schemas/memory-event.schema.json +70 -0
- package/.torusguard/schemas/memory-pattern.schema.json +65 -0
- package/.torusguard/scripts/diff_guard.py +202 -15
- package/.torusguard/scripts/finding_scorer.py +199 -86
- package/.torusguard/scripts/html_reporter.py +628 -0
- package/.torusguard/scripts/manifest_builder.py +7 -5
- package/.torusguard/scripts/memory_engine.py +1334 -0
- package/.torusguard/scripts/monorepo_detector.py +123 -10
- package/.torusguard/scripts/rules_sync.py +321 -0
- package/.torusguard/scripts/run_manager.py +171 -89
- package/.torusguard/scripts/stack_detect.py +458 -26
- package/.torusguard/skills/torusguard/SKILL.md +4 -3
- package/.torusguard/skills/torusguard/bootstrap.py +344 -56
- package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
- package/.torusguard/skills/torusguard/references/go-security.md +41 -0
- package/.torusguard/skills/torusguard/references/java-security.md +40 -0
- package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
- package/.torusguard/skills/torusguard-audit/SKILL.md +25 -25
- package/.torusguard/skills/torusguard-harden/SKILL.md +3 -3
- package/.torusguard/skills/torusguard-recheck/SKILL.md +3 -3
- package/.torusguard/workflows/memory.md +53 -0
- package/README.md +307 -401
- package/bin/torusguard.js +204 -6
- package/package.json +1 -1
- package/skills/torusguard/SKILL.md +4 -3
- package/skills/torusguard/bootstrap.py +68 -4
- package/skills/torusguard/payload/.manifest.json +98 -79
- package/skills/torusguard/payload/config/torusguard.json +1 -1
- package/skills/torusguard/payload/references/csharp-security.md +41 -0
- package/skills/torusguard/payload/references/go-security.md +41 -0
- package/skills/torusguard/payload/references/java-security.md +40 -0
- package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/payload/references/rust-security.md +40 -0
- package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
- package/skills/torusguard/payload/rules/custom/README.md +30 -0
- package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
- package/skills/torusguard/payload/schemas/memory-context.schema.json +95 -0
- package/skills/torusguard/payload/schemas/memory-event.schema.json +70 -0
- package/skills/torusguard/payload/schemas/memory-pattern.schema.json +65 -0
- package/skills/torusguard/payload/scripts/diff_guard.py +202 -15
- package/skills/torusguard/payload/scripts/finding_scorer.py +199 -86
- package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
- package/skills/torusguard/payload/scripts/manifest_builder.py +7 -5
- package/skills/torusguard/payload/scripts/memory_engine.py +1334 -0
- package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
- package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
- package/skills/torusguard/payload/scripts/run_manager.py +171 -89
- package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +4 -3
- package/skills/torusguard/payload/skills/torusguard/bootstrap.py +344 -56
- package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
- package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
- package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
- package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +25 -25
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +3 -3
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +3 -3
- package/skills/torusguard/payload/workflows/memory.md +53 -0
- package/skills/torusguard-audit/SKILL.md +25 -25
- package/skills/torusguard-harden/SKILL.md +3 -3
- package/skills/torusguard-recheck/SKILL.md +3 -3
- package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
- package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- 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 (
|
|
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'(
|
|
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'(
|
|
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
|
|
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.
|
|
149
|
-
|
|
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
|
|
4
|
-
Evaluates finding confidence against the objective 0–100 scoring model
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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()
|