torusguard 1.0.0 → 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 (55) hide show
  1. package/.torusguard/.manifest.json +89 -75
  2. package/.torusguard/references/csharp-security.md +41 -0
  3. package/.torusguard/references/go-security.md +41 -0
  4. package/.torusguard/references/java-security.md +40 -0
  5. package/.torusguard/references/polyglot-security-matrix.md +25 -0
  6. package/.torusguard/references/rust-security.md +40 -0
  7. package/.torusguard/rules/custom/.gitkeep +1 -0
  8. package/.torusguard/rules/custom/README.md +30 -0
  9. package/.torusguard/runs/report-latest.html +328 -0
  10. package/.torusguard/schemas/golden-recipe.schema.json +75 -0
  11. package/.torusguard/scripts/diff_guard.py +131 -4
  12. package/.torusguard/scripts/finding_scorer.py +33 -5
  13. package/.torusguard/scripts/html_reporter.py +628 -0
  14. package/.torusguard/scripts/manifest_builder.py +5 -5
  15. package/.torusguard/scripts/memory_engine.py +521 -99
  16. package/.torusguard/scripts/monorepo_detector.py +123 -10
  17. package/.torusguard/scripts/rules_sync.py +321 -0
  18. package/.torusguard/scripts/stack_detect.py +458 -26
  19. package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
  20. package/.torusguard/skills/torusguard/references/go-security.md +41 -0
  21. package/.torusguard/skills/torusguard/references/java-security.md +40 -0
  22. package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  23. package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
  24. package/README.md +307 -410
  25. package/bin/torusguard.js +108 -4
  26. package/package.json +1 -1
  27. package/skills/torusguard/bootstrap.py +2 -2
  28. package/skills/torusguard/payload/.manifest.json +89 -75
  29. package/skills/torusguard/payload/references/csharp-security.md +41 -0
  30. package/skills/torusguard/payload/references/go-security.md +41 -0
  31. package/skills/torusguard/payload/references/java-security.md +40 -0
  32. package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
  33. package/skills/torusguard/payload/references/rust-security.md +40 -0
  34. package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
  35. package/skills/torusguard/payload/rules/custom/README.md +30 -0
  36. package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
  37. package/skills/torusguard/payload/scripts/diff_guard.py +131 -4
  38. package/skills/torusguard/payload/scripts/finding_scorer.py +33 -5
  39. package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
  40. package/skills/torusguard/payload/scripts/manifest_builder.py +5 -5
  41. package/skills/torusguard/payload/scripts/memory_engine.py +521 -99
  42. package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
  43. package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
  44. package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
  45. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +2 -2
  46. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
  47. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
  48. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
  49. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  50. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
  51. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  52. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  53. package/.torusguard/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  54. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  55. package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
@@ -0,0 +1,328 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TorusGuard Security Posture &middot; TorusGuard</title>
7
+ <style>
8
+ :root {
9
+ --bg-dark: #0d1117;
10
+ --bg-surface: #161b22;
11
+ --bg-card: #21262d;
12
+ --border-subtle: #30363d;
13
+ --text-main: #f0f6fc;
14
+ --text-muted: #8b949e;
15
+ --accent-blue: #58a6ff;
16
+ --accent-green: #3fb950;
17
+ --accent-yellow: #d29922;
18
+ --accent-red: #f85149;
19
+ --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
20
+ }
21
+ * { box-sizing: border-box; margin: 0; padding: 0; }
22
+ body {
23
+ background: var(--bg-dark);
24
+ color: var(--text-main);
25
+ font-family: var(--font-stack);
26
+ line-height: 1.5;
27
+ padding: 24px;
28
+ }
29
+ .container { max-width: 1200px; margin: 0 auto; }
30
+ header {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ padding-bottom: 20px;
35
+ border-bottom: 1px solid var(--border-subtle);
36
+ margin-bottom: 24px;
37
+ }
38
+ .logo-group h1 {
39
+ font-size: 24px;
40
+ font-weight: 700;
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 8px;
44
+ }
45
+ .shield-icon { font-size: 26px; }
46
+ .project-meta { color: var(--text-muted); font-size: 14px; margin-top: 4px; }
47
+ .status-badge {
48
+ background: rgba(63, 185, 80, 0.15);
49
+ border: 1px solid var(--accent-green);
50
+ color: var(--accent-green);
51
+ padding: 6px 14px;
52
+ border-radius: 20px;
53
+ font-size: 13px;
54
+ font-weight: 600;
55
+ }
56
+ .grid-top {
57
+ display: grid;
58
+ grid-template-columns: 280px 1fr 1fr 1fr;
59
+ gap: 16px;
60
+ margin-bottom: 24px;
61
+ }
62
+ @media (max-width: 900px) {
63
+ .grid-top { grid-template-columns: 1fr 1fr; }
64
+ }
65
+ @media (max-width: 600px) {
66
+ .grid-top { grid-template-columns: 1fr; }
67
+ }
68
+ .card {
69
+ background: var(--bg-surface);
70
+ border: 1px solid var(--border-subtle);
71
+ border-radius: 8px;
72
+ padding: 20px;
73
+ }
74
+ .gauge-card {
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: center;
78
+ justify-content: center;
79
+ text-align: center;
80
+ }
81
+ .gauge-svg { width: 120px; height: 120px; transform: rotate(-90deg); }
82
+ .gauge-bg { fill: none; stroke: var(--bg-card); stroke-width: 8; }
83
+ .gauge-fill {
84
+ fill: none;
85
+ stroke: #3fb950;
86
+ stroke-width: 8;
87
+ stroke-linecap: round;
88
+ stroke-dasharray: 251.2;
89
+ stroke-dashoffset: 37.68000000000001;
90
+ transition: stroke-dashoffset 0.8s ease;
91
+ }
92
+ .gauge-value {
93
+ position: absolute;
94
+ font-size: 32px;
95
+ font-weight: 700;
96
+ color: var(--text-main);
97
+ }
98
+ .stat-label { font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
99
+ .stat-value { font-size: 28px; font-weight: 700; color: var(--text-main); }
100
+ .stat-sub { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
101
+ .pipeline-section { margin-bottom: 24px; }
102
+ .pipeline-tracker {
103
+ display: flex;
104
+ justify-content: space-between;
105
+ position: relative;
106
+ margin-top: 16px;
107
+ }
108
+ .pipeline-step {
109
+ display: flex;
110
+ flex-direction: column;
111
+ align-items: center;
112
+ flex: 1;
113
+ text-align: center;
114
+ position: relative;
115
+ z-index: 2;
116
+ }
117
+ .step-circle {
118
+ width: 32px;
119
+ height: 32px;
120
+ border-radius: 50%;
121
+ background: var(--bg-card);
122
+ border: 2px solid var(--accent-blue);
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ font-size: 13px;
127
+ font-weight: 700;
128
+ color: var(--accent-blue);
129
+ margin-bottom: 8px;
130
+ }
131
+ .step-circle.active { background: var(--accent-blue); color: var(--bg-dark); }
132
+ .step-name { font-size: 12px; font-weight: 600; color: var(--text-main); }
133
+ .step-cmd { font-size: 11px; color: var(--text-muted); font-family: monospace; margin-top: 2px; }
134
+ .pipeline-line {
135
+ position: absolute;
136
+ top: 16px;
137
+ left: 6%;
138
+ right: 6%;
139
+ height: 2px;
140
+ background: var(--border-subtle);
141
+ z-index: 1;
142
+ }
143
+ .grid-bottom {
144
+ display: grid;
145
+ grid-template-columns: 1fr 1fr;
146
+ gap: 24px;
147
+ margin-top: 24px;
148
+ }
149
+ @media (max-width: 800px) {
150
+ .grid-bottom { grid-template-columns: 1fr; }
151
+ }
152
+ .section-title {
153
+ font-size: 18px;
154
+ font-weight: 600;
155
+ margin-bottom: 16px;
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 8px;
159
+ }
160
+ .badge {
161
+ display: inline-block;
162
+ padding: 3px 8px;
163
+ border-radius: 12px;
164
+ font-size: 11px;
165
+ font-weight: 600;
166
+ font-family: monospace;
167
+ }
168
+ .badge-success { background: rgba(63, 185, 80, 0.15); color: var(--accent-green); border: 1px solid rgba(63, 185, 80, 0.3); }
169
+ .badge-danger { background: rgba(248, 81, 73, 0.15); color: var(--accent-red); border: 1px solid rgba(248, 81, 73, 0.3); }
170
+ .recipe-card {
171
+ background: var(--bg-card);
172
+ border: 1px solid var(--border-subtle);
173
+ border-radius: 6px;
174
+ padding: 12px;
175
+ margin-bottom: 12px;
176
+ }
177
+ .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
178
+ .meta-label { font-size: 11px; color: var(--text-muted); }
179
+ .recipe-desc { font-size: 13px; color: var(--text-main); margin-bottom: 8px; }
180
+ .diff-block {
181
+ background: var(--bg-dark);
182
+ border: 1px solid var(--border-subtle);
183
+ border-radius: 4px;
184
+ padding: 8px 12px;
185
+ font-family: monospace;
186
+ font-size: 12px;
187
+ overflow-x: auto;
188
+ color: #e6edf3;
189
+ }
190
+ table { width: 100%; border-collapse: collapse; }
191
+ th, td { padding: 10px 12px; text-align: left; font-size: 13px; }
192
+ th { color: var(--text-muted); border-bottom: 1px solid var(--border-subtle); font-weight: 600; font-size: 12px; text-transform: uppercase; }
193
+ td { border-bottom: 1px solid rgba(48, 54, 61, 0.5); }
194
+ .empty-state { text-align: center; padding: 24px; color: var(--text-muted); font-size: 13px; }
195
+ .empty-cell { text-align: center; color: var(--text-muted); padding: 16px; font-style: italic; }
196
+ footer {
197
+ margin-top: 40px;
198
+ text-align: center;
199
+ color: var(--text-muted);
200
+ font-size: 12px;
201
+ border-top: 1px solid var(--border-subtle);
202
+ padding-top: 20px;
203
+ }
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <div class="container">
208
+ <header>
209
+ <div class="logo-group">
210
+ <h1><span class="shield-icon">&#128737;</span> TorusGuard Security Posture</h1>
211
+ <div class="project-meta">Workspace: <strong>TorusGuard</strong> &middot; Stack: <strong>TypeScript</strong> &middot; 2026-09-07 11:55:40 UTC</div>
212
+ </div>
213
+ <div class="status-badge">&check; Local Governance Active</div>
214
+ </header>
215
+
216
+ <!-- Top KPI Grid -->
217
+ <div class="grid-top">
218
+ <div class="card gauge-card">
219
+ <div style="position: relative; display: flex; align-items: center; justify-content: center;">
220
+ <svg class="gauge-svg" viewBox="0 0 100 100">
221
+ <circle class="gauge-bg" cx="50" cy="50" r="40" />
222
+ <circle class="gauge-fill" cx="50" cy="50" r="40" />
223
+ </svg>
224
+ <div class="gauge-value">85</div>
225
+ </div>
226
+ <div class="stat-label" style="margin-top: 12px;">Security Posture Score</div>
227
+ </div>
228
+
229
+ <div class="card">
230
+ <div class="stat-label">Verified Golden Recipes</div>
231
+ <div class="stat-value" style="color: var(--accent-green);">0</div>
232
+ <div class="stat-sub">Distilled Ponytail fixes (<=35 add, <=25 del)</div>
233
+ </div>
234
+
235
+ <div class="card">
236
+ <div class="stat-label">Active Memory Patterns</div>
237
+ <div class="stat-value">0</div>
238
+ <div class="stat-sub">0 suppressed &middot; 0 recurring</div>
239
+ </div>
240
+
241
+ <div class="card">
242
+ <div class="stat-label">Security Rules Catalog</div>
243
+ <div class="stat-value" style="color: var(--accent-blue);">71</div>
244
+ <div class="stat-sub">11 rule families &middot; Multi-agent verified</div>
245
+ </div>
246
+ </div>
247
+
248
+ <!-- 7-Stage Canonical Pipeline Tracker -->
249
+ <div class="card pipeline-section">
250
+ <div class="section-title">&#9658; Canonical 7-Stage Closed-Loop Governance Flow</div>
251
+ <div class="pipeline-tracker">
252
+ <div class="pipeline-line"></div>
253
+ <div class="pipeline-step">
254
+ <div class="step-circle active">1</div>
255
+ <div class="step-name">Scan</div>
256
+ <div class="step-cmd">/audit</div>
257
+ </div>
258
+ <div class="pipeline-step">
259
+ <div class="step-circle active">2</div>
260
+ <div class="step-name">Score</div>
261
+ <div class="step-cmd">0-100</div>
262
+ </div>
263
+ <div class="pipeline-step">
264
+ <div class="step-circle active">3</div>
265
+ <div class="step-name">Harden</div>
266
+ <div class="step-cmd">/harden</div>
267
+ </div>
268
+ <div class="pipeline-step">
269
+ <div class="step-circle active">4</div>
270
+ <div class="step-name">Authorize</div>
271
+ <div class="step-cmd">Human Gate</div>
272
+ </div>
273
+ <div class="pipeline-step">
274
+ <div class="step-circle active">5</div>
275
+ <div class="step-name">Apply</div>
276
+ <div class="step-cmd">.bak Snapshot</div>
277
+ </div>
278
+ <div class="pipeline-step">
279
+ <div class="step-circle active">6</div>
280
+ <div class="step-name">Recheck</div>
281
+ <div class="step-cmd">Differential</div>
282
+ </div>
283
+ <div class="pipeline-step">
284
+ <div class="step-circle active">7</div>
285
+ <div class="step-name">Report</div>
286
+ <div class="step-cmd">SARIF v2.1.0</div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- Bottom Details Grid -->
292
+ <div class="grid-bottom">
293
+ <!-- Golden Recipes Catalog -->
294
+ <div class="card">
295
+ <div class="section-title">&#10024; Active Golden Fix Recipes (0)</div>
296
+ <div class="recipes-list">
297
+
298
+ <div class="empty-state">
299
+ <p>No golden fix recipes captured yet. Run <code>/torusguard-apply</code> on verified remediations to distill reusable recipes.</p>
300
+ </div>
301
+
302
+ </div>
303
+ </div>
304
+
305
+ <!-- Regression Watches & False Positives -->
306
+ <div class="card">
307
+ <div class="section-title">&#128065; Regression Watches & Suppressions</div>
308
+ <table>
309
+ <thead>
310
+ <tr>
311
+ <th>Rule ID</th>
312
+ <th>Description / Reason</th>
313
+ <th>Target Path</th>
314
+ </tr>
315
+ </thead>
316
+ <tbody>
317
+ <tr><td colspan='3' class='empty-cell'>Zero active security regressions detected. Posture clean.</td></tr>
318
+ </tbody>
319
+ </table>
320
+ </div>
321
+ </div>
322
+
323
+ <footer>
324
+ TorusGuard v1.2.0 &middot; Autonomous Security Engine for AI-Built Applications &middot; 100% Local-First &middot; Zero Cloud Telemetry Leakage
325
+ </footer>
326
+ </div>
327
+ </body>
328
+ </html>
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://torusguard.dev/schemas/golden-recipe.schema.json",
4
+ "title": "TorusGuard Golden Fix Recipe Schema",
5
+ "description": "Schema for reusable, verified AST remediation code snippets distilled from passing security patches.",
6
+ "type": "object",
7
+ "required": [
8
+ "recipe_id",
9
+ "rule_id",
10
+ "file_type",
11
+ "before_snippet",
12
+ "after_snippet",
13
+ "diff_snippet",
14
+ "ponytail_metrics",
15
+ "verified_count",
16
+ "last_verified"
17
+ ],
18
+ "properties": {
19
+ "recipe_id": {
20
+ "type": "string",
21
+ "pattern": "^recipe-[A-Za-z0-9_-]+$"
22
+ },
23
+ "rule_id": {
24
+ "type": "string",
25
+ "pattern": "^TG-[A-Z]+-[0-9]{3}$"
26
+ },
27
+ "file_type": {
28
+ "type": "string",
29
+ "description": "File extension or language identifier (e.g., .py, .ts, .js)"
30
+ },
31
+ "framework": {
32
+ "type": ["string", "null"],
33
+ "description": "Target web framework (e.g., Django, FastAPI, Next.js)"
34
+ },
35
+ "description": {
36
+ "type": "string",
37
+ "description": "Human-readable summary of the fix strategy"
38
+ },
39
+ "before_snippet": {
40
+ "type": "string",
41
+ "description": "Vulnerable code pattern before remediation"
42
+ },
43
+ "after_snippet": {
44
+ "type": "string",
45
+ "description": "Remediated code pattern adhering to Ponytail bounds"
46
+ },
47
+ "diff_snippet": {
48
+ "type": "string",
49
+ "description": "Unified diff snippet representing the surgical fix"
50
+ },
51
+ "ponytail_metrics": {
52
+ "type": "object",
53
+ "required": ["additions", "deletions"],
54
+ "properties": {
55
+ "additions": {
56
+ "type": "integer",
57
+ "maximum": 35
58
+ },
59
+ "deletions": {
60
+ "type": "integer",
61
+ "maximum": 25
62
+ }
63
+ }
64
+ },
65
+ "verified_count": {
66
+ "type": "integer",
67
+ "minimum": 1
68
+ },
69
+ "last_verified": {
70
+ "type": "string",
71
+ "format": "date-time"
72
+ }
73
+ },
74
+ "additionalProperties": false
75
+ }
@@ -15,12 +15,39 @@ from pathlib import Path
15
15
  from typing import Dict, List, Any, Optional
16
16
 
17
17
  BYPASS_PATTERNS = [
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"),
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
19
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
20
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"),
21
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
22
34
  (re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled in patch"),
23
- (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")
24
51
  ]
25
52
 
26
53
  SECRET_PATTERNS = [
@@ -35,7 +62,10 @@ TENANT_FILTER_PATTERNS = [
35
62
  re.compile(r'\.filter\([^)]*\btenant\s*='),
36
63
  re.compile(r'\.filter_by\([^)]*\btenant\s*='),
37
64
  re.compile(r'\bwhere\s+tenant_id\s*='),
38
- 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')
39
69
  ]
40
70
 
41
71
 
@@ -198,14 +228,110 @@ def audit_diff_file(file_path: str, check_memory: bool = False, root_dir: Option
198
228
  return audit_diff(content, check_memory=check_memory, root_dir=root_dir)
199
229
 
200
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
296
+
297
+
201
298
  def main():
202
299
  parser = argparse.ArgumentParser(description="TorusGuard Unified Diff Security Scanner")
203
300
  parser.add_argument("diff_path", nargs="?", help="Path to unified diff file (or read stdin if omitted)")
204
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")
205
305
  parser.add_argument("--json", action="store_true", help="Output results as JSON")
206
306
  args = parser.parse_args()
207
307
 
208
- if 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:
209
335
  res = audit_diff_file(args.diff_path, check_memory=args.check_memory)
210
336
  else:
211
337
  if sys.stdin.isatty():
@@ -229,3 +355,4 @@ def main():
229
355
 
230
356
  if __name__ == "__main__":
231
357
  main()
358
+
@@ -44,11 +44,13 @@ def compute_memory_boost(
44
44
  try:
45
45
  with open(patterns_file, "r", encoding="utf-8") as f:
46
46
  patterns = json.load(f)
47
- except Exception:
47
+ except (json.JSONDecodeError, OSError, UnicodeDecodeError):
48
48
  return 0
49
49
 
50
50
  boost = 0
51
51
  matched_file = False
52
+ matched_dir = False
53
+ target_path = Path(file_path) if file_path else None
52
54
 
53
55
  for pat in patterns:
54
56
  if pat.get("rule_id") == rule_id:
@@ -62,18 +64,37 @@ def compute_memory_boost(
62
64
  elif ptype == "security_idiom":
63
65
  boost += 5
64
66
 
65
- # File-specific correlation bonus
66
- if file_path and file_path in pat.get("affected_files", []):
67
+ # File-specific and directory-level correlation bonus
68
+ affected = pat.get("affected_files", [])
69
+ if file_path and file_path in affected:
67
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
68
73
 
69
74
  if matched_file and boost > 0:
70
75
  boost += 5
71
76
  elif matched_file and boost < 0:
72
77
  boost -= 10
78
+ elif matched_dir and boost > 0:
79
+ boost += 2
80
+ elif matched_dir and boost < 0:
81
+ boost -= 4
73
82
 
74
83
  return boost
75
84
 
76
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
+
77
98
  def compute_confidence_score(
78
99
  evidence_quality: int = 35,
79
100
  reproduction_success: int = 0,
@@ -84,7 +105,7 @@ def compute_confidence_score(
84
105
  rule_id: Optional[str] = None,
85
106
  file_path: Optional[str] = None,
86
107
  root_dir: Optional[Path] = None
87
- ) -> Tuple[int, str, Dict[str, int]]:
108
+ ) -> Tuple[int, str, Dict[str, Any]]:
88
109
  """
89
110
  Computes total score and assigns confidence band.
90
111
  Max points:
@@ -94,6 +115,7 @@ def compute_confidence_score(
94
115
  - environmental_clarity: 15
95
116
  - manual_review_status: 10
96
117
  - memory_boost: -30 to +20 (modifier from persistent memory)
118
+ - test_deduction: -30 if file is located in a test/mock path
97
119
  Total is clamped to [0, 100].
98
120
  """
99
121
  eq = min(max(evidence_quality, 0), 35)
@@ -107,7 +129,11 @@ def compute_confidence_score(
107
129
  if rule_id and eff_mem_boost == 0:
108
130
  eff_mem_boost = compute_memory_boost(rule_id, file_path=file_path, root_dir=root_dir)
109
131
 
110
- raw_total = eq + rs + ic + ec + mr + eff_mem_boost
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
111
137
  total = min(max(raw_total, 0), 100)
112
138
 
113
139
  if total >= 90:
@@ -126,6 +152,8 @@ def compute_confidence_score(
126
152
  "environmental_clarity": ec,
127
153
  "manual_review_status": mr,
128
154
  "memory_boost": eff_mem_boost,
155
+ "test_exemption": is_test,
156
+ "test_deduction": test_deduction,
129
157
  "total_score": total,
130
158
  "classification_band": band
131
159
  }