@rigour-labs/core 5.0.0 → 5.1.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 (139) hide show
  1. package/README.md +9 -1
  2. package/dist/gates/agent-team.d.ts +0 -1
  3. package/dist/gates/agent-team.js +0 -1
  4. package/dist/gates/checkpoint.d.ts +0 -2
  5. package/dist/gates/checkpoint.js +0 -2
  6. package/dist/gates/context-window-artifacts.d.ts +6 -2
  7. package/dist/gates/context-window-artifacts.js +107 -31
  8. package/dist/gates/deep-analysis.d.ts +2 -0
  9. package/dist/gates/deep-analysis.js +41 -11
  10. package/dist/gates/dependency.d.ts +0 -2
  11. package/dist/gates/dependency.js +23 -5
  12. package/dist/gates/deprecated-apis.d.ts +0 -2
  13. package/dist/gates/deprecated-apis.js +33 -20
  14. package/dist/gates/duplication-drift/index.d.ts +61 -0
  15. package/dist/gates/duplication-drift/index.js +240 -0
  16. package/dist/gates/duplication-drift/similarity.d.ts +68 -0
  17. package/dist/gates/duplication-drift/similarity.js +177 -0
  18. package/dist/gates/duplication-drift/tokenizer.d.ts +55 -0
  19. package/dist/gates/duplication-drift/tokenizer.js +195 -0
  20. package/dist/gates/frontend-secret-exposure.d.ts +0 -3
  21. package/dist/gates/frontend-secret-exposure.js +1 -114
  22. package/dist/gates/frontend-secret-patterns.d.ts +33 -0
  23. package/dist/gates/frontend-secret-patterns.js +119 -0
  24. package/dist/gates/{hallucinated-imports.d.ts → hallucinated-imports/index.d.ts} +2 -29
  25. package/dist/gates/hallucinated-imports/index.js +174 -0
  26. package/dist/gates/hallucinated-imports/js-resolver.d.ts +45 -0
  27. package/dist/gates/hallucinated-imports/js-resolver.js +320 -0
  28. package/dist/gates/hallucinated-imports/manifest-discovery.d.ts +28 -0
  29. package/dist/gates/hallucinated-imports/manifest-discovery.js +114 -0
  30. package/dist/gates/hallucinated-imports/python-resolver.d.ts +24 -0
  31. package/dist/gates/hallucinated-imports/python-resolver.js +306 -0
  32. package/dist/gates/hallucinated-imports-lang.d.ts +2 -2
  33. package/dist/gates/hallucinated-imports-lang.js +269 -34
  34. package/dist/gates/hallucinated-imports.test.js +1 -2
  35. package/dist/gates/inconsistent-error-handling.d.ts +0 -5
  36. package/dist/gates/inconsistent-error-handling.js +15 -144
  37. package/dist/gates/language-adapters/csharp-adapter.d.ts +16 -0
  38. package/dist/gates/language-adapters/csharp-adapter.js +211 -0
  39. package/dist/gates/language-adapters/go-adapter.d.ts +26 -0
  40. package/dist/gates/language-adapters/go-adapter.js +195 -0
  41. package/dist/gates/language-adapters/index.d.ts +15 -0
  42. package/dist/gates/language-adapters/index.js +16 -0
  43. package/dist/gates/language-adapters/java-adapter.d.ts +16 -0
  44. package/dist/gates/language-adapters/java-adapter.js +237 -0
  45. package/dist/gates/language-adapters/js-adapter.d.ts +26 -0
  46. package/dist/gates/language-adapters/js-adapter.js +279 -0
  47. package/dist/gates/language-adapters/python-adapter.d.ts +25 -0
  48. package/dist/gates/language-adapters/python-adapter.js +183 -0
  49. package/dist/gates/language-adapters/registry.d.ts +26 -0
  50. package/dist/gates/language-adapters/registry.js +65 -0
  51. package/dist/gates/language-adapters/ruby-adapter.d.ts +25 -0
  52. package/dist/gates/language-adapters/ruby-adapter.js +217 -0
  53. package/dist/gates/language-adapters/rust-adapter.d.ts +27 -0
  54. package/dist/gates/language-adapters/rust-adapter.js +235 -0
  55. package/dist/gates/language-adapters/types.d.ts +60 -0
  56. package/dist/gates/language-adapters/types.js +22 -0
  57. package/dist/gates/logic-drift-extractors.d.ts +15 -0
  58. package/dist/gates/logic-drift-extractors.js +34 -0
  59. package/dist/gates/logic-drift.d.ts +0 -30
  60. package/dist/gates/logic-drift.js +39 -129
  61. package/dist/gates/phantom-apis.d.ts +0 -2
  62. package/dist/gates/phantom-apis.js +49 -20
  63. package/dist/gates/promise-safety.d.ts +0 -1
  64. package/dist/gates/promise-safety.js +14 -2
  65. package/dist/gates/runner.js +51 -22
  66. package/dist/gates/security-patterns-data.d.ts +14 -0
  67. package/dist/gates/security-patterns-data.js +235 -0
  68. package/dist/gates/security-patterns.d.ts +17 -3
  69. package/dist/gates/security-patterns.js +80 -211
  70. package/dist/gates/side-effect-analysis/categorizer.d.ts +32 -0
  71. package/dist/gates/side-effect-analysis/categorizer.js +83 -0
  72. package/dist/gates/{side-effect-analysis.d.ts → side-effect-analysis/index.d.ts} +3 -5
  73. package/dist/gates/{side-effect-analysis.js → side-effect-analysis/index.js} +33 -45
  74. package/dist/gates/side-effect-analysis/scope-tracker.d.ts +37 -0
  75. package/dist/gates/side-effect-analysis/scope-tracker.js +40 -0
  76. package/dist/gates/side-effect-helpers/index.d.ts +4 -0
  77. package/dist/gates/side-effect-helpers/index.js +4 -0
  78. package/dist/gates/side-effect-helpers/pattern-detection.d.ts +123 -0
  79. package/dist/gates/{side-effect-helpers.js → side-effect-helpers/pattern-detection.js} +22 -468
  80. package/dist/gates/side-effect-helpers/resource-tracking.d.ts +80 -0
  81. package/dist/gates/side-effect-helpers/resource-tracking.js +281 -0
  82. package/dist/gates/side-effect-helpers/scope-analysis.d.ts +21 -0
  83. package/dist/gates/side-effect-helpers/scope-analysis.js +146 -0
  84. package/dist/gates/side-effect-helpers/types.d.ts +38 -0
  85. package/dist/gates/side-effect-helpers/types.js +41 -0
  86. package/dist/gates/side-effect-rules.d.ts +0 -1
  87. package/dist/gates/side-effect-rules.js +0 -1
  88. package/dist/gates/style-drift-rules.d.ts +86 -0
  89. package/dist/gates/style-drift-rules.js +103 -0
  90. package/dist/gates/style-drift.d.ts +7 -16
  91. package/dist/gates/style-drift.js +101 -119
  92. package/dist/gates/test-quality-matchers.d.ts +53 -0
  93. package/dist/gates/test-quality-matchers.js +86 -0
  94. package/dist/gates/test-quality.d.ts +0 -3
  95. package/dist/gates/test-quality.js +47 -44
  96. package/dist/hooks/checker.d.ts +0 -1
  97. package/dist/hooks/checker.js +1 -3
  98. package/dist/hooks/dlp-templates.d.ts +0 -1
  99. package/dist/hooks/dlp-templates.js +0 -4
  100. package/dist/hooks/index.d.ts +0 -2
  101. package/dist/hooks/index.js +0 -2
  102. package/dist/hooks/input-validator.d.ts +0 -1
  103. package/dist/hooks/input-validator.js +0 -1
  104. package/dist/hooks/input-validator.test.js +0 -1
  105. package/dist/hooks/standalone-checker.d.ts +0 -1
  106. package/dist/hooks/standalone-checker.js +0 -1
  107. package/dist/hooks/standalone-dlp-checker.d.ts +0 -1
  108. package/dist/hooks/standalone-dlp-checker.js +0 -1
  109. package/dist/hooks/templates.d.ts +6 -1
  110. package/dist/hooks/templates.js +6 -1
  111. package/dist/hooks/types.d.ts +1 -2
  112. package/dist/hooks/types.js +1 -1
  113. package/dist/index.d.ts +1 -1
  114. package/dist/index.js +1 -1
  115. package/dist/services/adaptive-thresholds.d.ts +0 -2
  116. package/dist/services/adaptive-thresholds.js +0 -2
  117. package/dist/services/filesystem-cache.d.ts +0 -1
  118. package/dist/services/filesystem-cache.js +0 -1
  119. package/dist/services/score-history.d.ts +0 -1
  120. package/dist/services/score-history.js +0 -1
  121. package/dist/services/temporal-drift.d.ts +1 -2
  122. package/dist/services/temporal-drift.js +7 -8
  123. package/dist/storage/db.d.ts +23 -7
  124. package/dist/storage/db.js +116 -55
  125. package/dist/storage/findings.d.ts +4 -3
  126. package/dist/storage/findings.js +13 -20
  127. package/dist/storage/local-memory.d.ts +4 -4
  128. package/dist/storage/local-memory.js +20 -22
  129. package/dist/storage/patterns.d.ts +5 -5
  130. package/dist/storage/patterns.js +20 -26
  131. package/dist/storage/scans.d.ts +6 -6
  132. package/dist/storage/scans.js +12 -21
  133. package/dist/types/index.d.ts +1 -0
  134. package/dist/utils/scanner.js +1 -1
  135. package/package.json +7 -8
  136. package/dist/gates/duplication-drift.d.ts +0 -128
  137. package/dist/gates/duplication-drift.js +0 -585
  138. package/dist/gates/hallucinated-imports.js +0 -641
  139. package/dist/gates/side-effect-helpers.d.ts +0 -260
@@ -11,219 +11,13 @@
11
11
  * - Hardcoded Secrets
12
12
  * - Insecure Randomness
13
13
  * - Command Injection
14
- *
15
- * @since v2.14.0
16
14
  */
17
15
  import { Gate } from './base.js';
18
16
  import { FileScanner } from '../utils/scanner.js';
19
17
  import { Logger } from '../utils/logger.js';
18
+ import { VULNERABILITY_PATTERNS } from './security-patterns-data.js';
20
19
  import fs from 'fs-extra';
21
20
  import path from 'path';
22
- // Pattern definitions with regex and metadata
23
- const VULNERABILITY_PATTERNS = [
24
- // SQL Injection
25
- {
26
- type: 'sql_injection',
27
- regex: /(?:execute|query|raw|exec)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|WITH)[^`]*\$\{[^}]+\}[^`]*`/gi,
28
- severity: 'critical',
29
- description: 'Potential SQL injection: User input concatenated into SQL query',
30
- cwe: 'CWE-89',
31
- languages: ['ts', 'js', 'py']
32
- },
33
- {
34
- type: 'sql_injection',
35
- regex: /(?:execute|query|raw|exec)\s*\(\s*['"`][^'"`]*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|WITH)[^'"`]*['"`]\s*\+\s*[^)]+\)/gi,
36
- severity: 'critical',
37
- description: 'SQL query built with string concatenation',
38
- cwe: 'CWE-89',
39
- languages: ['ts', 'js']
40
- },
41
- // XSS
42
- {
43
- type: 'xss',
44
- regex: /innerHTML\s*=\s*(?!\s*['"`]\s*['"`])[^;]+/g,
45
- severity: 'high',
46
- description: 'Potential XSS: innerHTML assignment with dynamic content',
47
- cwe: 'CWE-79',
48
- languages: ['ts', 'js', 'tsx', 'jsx']
49
- },
50
- {
51
- type: 'xss',
52
- regex: /dangerouslySetInnerHTML\s*=\s*\{/g,
53
- severity: 'high',
54
- description: 'dangerouslySetInnerHTML usage (ensure content is sanitized)',
55
- cwe: 'CWE-79',
56
- languages: ['tsx', 'jsx']
57
- },
58
- {
59
- type: 'xss',
60
- regex: /document\.write\s*\(/g,
61
- severity: 'high',
62
- description: 'document.write is dangerous for XSS',
63
- cwe: 'CWE-79',
64
- languages: ['ts', 'js']
65
- },
66
- // Path Traversal
67
- {
68
- type: 'path_traversal',
69
- regex: /(?:readFile|writeFile|readdir|unlink|rmdir)\s*\([^)]*(?:req\.(?:params|query|body)|\.\.\/)/g,
70
- severity: 'high',
71
- description: 'Potential path traversal: File operation with user input',
72
- cwe: 'CWE-22',
73
- languages: ['ts', 'js']
74
- },
75
- {
76
- type: 'path_traversal',
77
- regex: /path\.join\s*\([^)]*req\./g,
78
- severity: 'medium',
79
- description: 'path.join with request data (verify input sanitization)',
80
- cwe: 'CWE-22',
81
- languages: ['ts', 'js']
82
- },
83
- // Hardcoded Secrets
84
- {
85
- type: 'hardcoded_secrets',
86
- regex: /(?:password|secret|api_key|apikey|auth_token|access_token|private_key)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
87
- severity: 'critical',
88
- description: 'Hardcoded secret detected in code',
89
- cwe: 'CWE-798',
90
- languages: ['ts', 'js', 'py', 'java', 'go']
91
- },
92
- {
93
- type: 'hardcoded_secrets',
94
- regex: /(?:sk-|pk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_)[a-zA-Z0-9]{20,}/g,
95
- severity: 'critical',
96
- description: 'API key pattern detected (OpenAI, GitHub, etc.)',
97
- cwe: 'CWE-798',
98
- languages: ['*']
99
- },
100
- {
101
- type: 'hardcoded_secrets',
102
- regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
103
- severity: 'critical',
104
- description: 'Private key embedded in source code',
105
- cwe: 'CWE-798',
106
- languages: ['*']
107
- },
108
- // Insecure Randomness
109
- {
110
- type: 'insecure_randomness',
111
- regex: /Math\.random\s*\(\s*\)/g,
112
- severity: 'medium',
113
- description: 'Math.random() is not cryptographically secure',
114
- cwe: 'CWE-338',
115
- languages: ['ts', 'js', 'tsx', 'jsx']
116
- },
117
- // Command Injection
118
- {
119
- type: 'command_injection',
120
- regex: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{[^}]*(?:req\.|query|params|body|input|user|argv|process\.env)[^}]*\}[^`]*`|[^)]*(?:req\.|query|params|body|input|user|argv|process\.env)[^)]*)\)/g,
121
- severity: 'critical',
122
- description: 'Potential command injection: shell execution with user input',
123
- cwe: 'CWE-78',
124
- languages: ['ts', 'js']
125
- },
126
- {
127
- type: 'command_injection',
128
- regex: /child_process.*\s*\.\s*(?:exec|spawn)\s*\([^)]*(?:req\.|query|params|body|input|user|argv|process\.env)/g,
129
- severity: 'high',
130
- description: 'child_process usage detected (verify input sanitization)',
131
- cwe: 'CWE-78',
132
- languages: ['ts', 'js']
133
- },
134
- // ReDoS — Denial of Service via regex (OWASP #7)
135
- {
136
- type: 'redos',
137
- regex: /new RegExp\s*\([^)]*(?:req\.|params|query|body|input|user)/g,
138
- severity: 'high',
139
- description: 'Dynamic regex from user input — potential ReDoS',
140
- cwe: 'CWE-1333',
141
- languages: ['ts', 'js']
142
- },
143
- {
144
- type: 'redos',
145
- regex: /\(\?:[^)]*\+[^)]*\)\+|\([^)]*\*[^)]*\)\+|\(\.\*\)\{/g,
146
- severity: 'medium',
147
- description: 'Regex with nested quantifiers — potential ReDoS',
148
- cwe: 'CWE-1333',
149
- languages: ['ts', 'js', 'py']
150
- },
151
- // Overly Permissive Code (OWASP #9)
152
- {
153
- type: 'overly_permissive',
154
- regex: /cors\s*\(\s*\{[^}]*origin\s*:\s*(?:true|['"`]\*['"`])/g,
155
- severity: 'high',
156
- description: 'CORS wildcard origin — allows any domain',
157
- cwe: 'CWE-942',
158
- languages: ['ts', 'js']
159
- },
160
- {
161
- type: 'overly_permissive',
162
- regex: /(?:listen|bind)\s*\(\s*(?:\d+\s*,\s*)?['"`]0\.0\.0\.0['"`]/g,
163
- severity: 'medium',
164
- description: 'Binding to 0.0.0.0 exposes service to all interfaces',
165
- cwe: 'CWE-668',
166
- languages: ['ts', 'js', 'py', 'go']
167
- },
168
- {
169
- type: 'overly_permissive',
170
- regex: /chmod\s*\(\s*[^,]*,\s*['"`]?(?:0o?)?777['"`]?\s*\)/g,
171
- severity: 'high',
172
- description: 'chmod 777 — world-readable/writable permissions',
173
- cwe: 'CWE-732',
174
- languages: ['ts', 'js', 'py']
175
- },
176
- {
177
- type: 'overly_permissive',
178
- regex: /(?:Access-Control-Allow-Origin|x-powered-by)['"`,\s:]+\*/gi,
179
- severity: 'high',
180
- description: 'Wildcard Access-Control-Allow-Origin header',
181
- cwe: 'CWE-942',
182
- languages: ['ts', 'js', 'py']
183
- },
184
- // Unsafe Output Handling (OWASP #6)
185
- {
186
- type: 'unsafe_output',
187
- regex: /res\.(?:send|write|end)\s*\(\s*(?:req\.|params|query|body|input|user)/g,
188
- severity: 'high',
189
- description: 'Reflecting user input in response without sanitization',
190
- cwe: 'CWE-79',
191
- languages: ['ts', 'js']
192
- },
193
- {
194
- type: 'unsafe_output',
195
- regex: /\$\{[^}]*(?:req\.|params|query|body|input|user)[^}]*\}.*(?:html|template|render)/gi,
196
- severity: 'high',
197
- description: 'User input interpolated into template/HTML output',
198
- cwe: 'CWE-79',
199
- languages: ['ts', 'js', 'py']
200
- },
201
- {
202
- type: 'unsafe_output',
203
- regex: /eval\s*\(\s*(?:req\.|params|query|body|input|user)/g,
204
- severity: 'critical',
205
- description: 'eval() with user input — code injection',
206
- cwe: 'CWE-94',
207
- languages: ['ts', 'js', 'py']
208
- },
209
- // Missing Input Validation (OWASP #8)
210
- {
211
- type: 'missing_input_validation',
212
- regex: /JSON\.parse\s*\(\s*(?:req\.body|request\.body|body|data|input)\s*\)/g,
213
- severity: 'medium',
214
- description: 'JSON.parse on raw input without schema validation',
215
- cwe: 'CWE-20',
216
- languages: ['ts', 'js']
217
- },
218
- {
219
- type: 'missing_input_validation',
220
- regex: /(?:as\s+any|:\s*any)\s*(?:[;,)\]}])/g,
221
- severity: 'medium',
222
- description: 'Type assertion to "any" bypasses type safety',
223
- cwe: 'CWE-20',
224
- languages: ['ts']
225
- },
226
- ];
227
21
  export class SecurityPatternsGate extends Gate {
228
22
  config;
229
23
  severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
@@ -347,6 +141,14 @@ export class SecurityPatternsGate extends Gate {
347
141
  if (pattern.type === 'hardcoded_secrets' && this.isDummySecretValue(match[0])) {
348
142
  continue;
349
143
  }
144
+ // For XSS: check if innerHTML/dangerouslySetInnerHTML is wrapped in a sanitizer
145
+ if (pattern.type === 'xss' && this.isSanitizedAssignment(match[0])) {
146
+ continue;
147
+ }
148
+ // For command_injection: check if spawn/exec uses { shell: false } (safe)
149
+ if (pattern.type === 'command_injection' && this.isSafeShellCall(match[0], content, match.index)) {
150
+ continue;
151
+ }
350
152
  // Find line number
351
153
  const beforeMatch = content.slice(0, match.index);
352
154
  const lineNumber = beforeMatch.split('\n').length;
@@ -364,7 +166,8 @@ export class SecurityPatternsGate extends Gate {
364
166
  }
365
167
  /**
366
168
  * Check if a hardcoded secret match is actually a dummy/placeholder value.
367
- * Filters out test values, placeholder defaults, and env-var-name assignments.
169
+ * Filters out test values, placeholder defaults, env-var-name assignments,
170
+ * store action types, and low-entropy constants.
368
171
  */
369
172
  isDummySecretValue(matchText) {
370
173
  // Extract the quoted value from the match (e.g., api_key="test-api-key" → test-api-key)
@@ -380,13 +183,79 @@ export class SecurityPatternsGate extends Gate {
380
183
  return true;
381
184
  if (/^testpass(?:word)?$/i.test(value))
382
185
  return true;
383
- // All-caps with underscores = env var names, not actual secrets
384
- // e.g., API_KEY = "OPEN_SANDBOX_API_KEY" is referencing a var name, not a real key
385
- if (/^[A-Z][A-Z0-9_]{7,}$/.test(value))
186
+ // All-caps with underscores/dollars = env var names or constants, not actual secrets
187
+ // e.g., API_KEY = "OPEN_SANDBOX_API_KEY", SECRETS$ADD_SECRET (Redux action types)
188
+ if (/^[A-Z][A-Z0-9_$]{7,}$/.test(value))
189
+ return true;
190
+ // Store action type patterns: NAMESPACE$ACTION or namespace/ACTION (Redux, Zustand, Flux)
191
+ if (/^\w+\$\w+$/.test(value))
192
+ return true;
193
+ if (/^[a-z][\w-]*\/[A-Z_]+$/.test(value))
386
194
  return true;
387
195
  // Common documentation/tutorial dummy values
388
196
  if (/^(?:sk_test_|pk_test_|sk_live_xxx|password123|secret123|abcdef|abc123)/i.test(value))
389
197
  return true;
198
+ // Shannon entropy check: low-entropy values are likely constants, not real secrets
199
+ // Real secrets have high entropy (>4.0 bits/char); constants and names have low entropy
200
+ if (this.shannonEntropy(value) < 3.0)
201
+ return true;
202
+ // ALL_CAPS_SNAKE_CASE without any lowercase/special chars = likely enum or constant
203
+ if (/^[A-Z][A-Z0-9_$]*$/.test(value) && value.length >= 6)
204
+ return true;
205
+ // URL-like values that are just config (not secrets): localhost, 127.0.0.1, etc.
206
+ if (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(value))
207
+ return true;
208
+ // Common non-secret patterns: UUIDs, semver, file paths
209
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value))
210
+ return true;
211
+ if (/^\d+\.\d+\.\d+/.test(value))
212
+ return true; // semver
213
+ return false;
214
+ }
215
+ /**
216
+ * Calculate Shannon entropy (bits per character) of a string.
217
+ * Real secrets have high entropy (>4.5); constants/names have low entropy (<3.0).
218
+ */
219
+ shannonEntropy(str) {
220
+ if (!str || str.length === 0)
221
+ return 0;
222
+ const freq = new Map();
223
+ for (const ch of str) {
224
+ freq.set(ch, (freq.get(ch) || 0) + 1);
225
+ }
226
+ let entropy = 0;
227
+ for (const count of freq.values()) {
228
+ const p = count / str.length;
229
+ if (p > 0)
230
+ entropy -= p * Math.log2(p);
231
+ }
232
+ return entropy;
233
+ }
234
+ /**
235
+ * Check if an innerHTML or dangerouslySetInnerHTML assignment is wrapped in a sanitizer.
236
+ * Known sanitizers: DOMPurify.sanitize(), sanitize(), xss(), escapeHtml(), htmlEncode().
237
+ */
238
+ isSanitizedAssignment(matchText) {
239
+ const sanitizers = [
240
+ 'sanitize(', 'DOMPurify.sanitize(', 'dompurify.sanitize(',
241
+ 'xss(', 'escape(', 'escapeHtml(', 'htmlEncode(',
242
+ 'sanitizeHtml(', 'clean(', 'purify(',
243
+ ];
244
+ const rhs = matchText.includes('=') ? matchText.split('=').slice(1).join('=') : matchText;
245
+ return sanitizers.some(s => rhs.toLowerCase().includes(s.toLowerCase()));
246
+ }
247
+ /**
248
+ * Check if a shell execution call is using { shell: false } option (safe pattern).
249
+ * Also, spawn() without { shell: true } is safe by default — don't flag it.
250
+ */
251
+ isSafeShellCall(matchText, fullContent, matchIndex) {
252
+ // Check surrounding context (100 chars after match) for shell: false
253
+ const context = fullContent.slice(matchIndex, matchIndex + matchText.length + 100);
254
+ if (/shell\s*:\s*false/.test(context))
255
+ return true;
256
+ // spawn() without shell: true is safe by default
257
+ if (/\bspawn(?:Sync)?\s*\(/.test(matchText) && !/shell\s*:\s*true/.test(context))
258
+ return true;
390
259
  return false;
391
260
  }
392
261
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Side Effect Categorization and Severity Assignment
3
+ *
4
+ * Functions that classify side effects by type/severity and assign
5
+ * appropriate titles and descriptions.
6
+ */
7
+ import { SideEffectViolation } from '../side-effect-helpers/index.js';
8
+ import { Failure } from '../../types/index.js';
9
+ /**
10
+ * Gets the human-readable title for a rule
11
+ */
12
+ export declare function getRuleTitle(rule: string): string;
13
+ /**
14
+ * Gets the severity level for a rule
15
+ */
16
+ export declare function getRuleSeverity(rule: string): 'low' | 'medium' | 'high' | 'critical';
17
+ /**
18
+ * Categorizes a violation and assigns it proper severity and title
19
+ */
20
+ export declare function categorizeViolation(violation: SideEffectViolation): SideEffectViolation;
21
+ /**
22
+ * Converts a side effect violation to a Failure object
23
+ */
24
+ export declare function violationToFailure(violation: SideEffectViolation, createFailure: (message: string, files: string[], hint?: string, title?: string, startLine?: number, endLine?: number, severity?: string) => Failure): Failure;
25
+ /**
26
+ * Groups violations by severity level
27
+ */
28
+ export declare function groupBySeverity(violations: SideEffectViolation[]): Record<string, SideEffectViolation[]>;
29
+ /**
30
+ * Filters violations by severity threshold
31
+ */
32
+ export declare function filterBySeverity(violations: SideEffectViolation[], minSeverity: 'critical' | 'high' | 'medium' | 'low'): SideEffectViolation[];
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Side Effect Categorization and Severity Assignment
3
+ *
4
+ * Functions that classify side effects by type/severity and assign
5
+ * appropriate titles and descriptions.
6
+ */
7
+ /**
8
+ * Categorization information for each rule type
9
+ */
10
+ const RULE_TITLES = {
11
+ 'unbounded-timer': 'Unbounded Timer',
12
+ 'orphan-process': 'Orphan Process',
13
+ 'unbounded-io-loop': 'Unbounded I/O Loop',
14
+ 'retry-without-limit': 'Retry Without Limit',
15
+ 'circular-trigger': 'Circular File Trigger',
16
+ 'resource-leak': 'Resource Leak',
17
+ 'unbounded-recursion': 'Unbounded Recursion',
18
+ 'auto-restart-bomb': 'Auto-Restart Bomb',
19
+ };
20
+ /**
21
+ * Maps rule IDs to their severity categories
22
+ */
23
+ const RULE_SEVERITIES = {
24
+ 'unbounded-timer': 'high',
25
+ 'orphan-process': 'high',
26
+ 'unbounded-io-loop': 'critical',
27
+ 'retry-without-limit': 'high',
28
+ 'circular-trigger': 'critical',
29
+ 'resource-leak': 'medium',
30
+ 'unbounded-recursion': 'high',
31
+ 'auto-restart-bomb': 'critical',
32
+ };
33
+ /**
34
+ * Gets the human-readable title for a rule
35
+ */
36
+ export function getRuleTitle(rule) {
37
+ return RULE_TITLES[rule] || rule;
38
+ }
39
+ /**
40
+ * Gets the severity level for a rule
41
+ */
42
+ export function getRuleSeverity(rule) {
43
+ return RULE_SEVERITIES[rule] || 'medium';
44
+ }
45
+ /**
46
+ * Categorizes a violation and assigns it proper severity and title
47
+ */
48
+ export function categorizeViolation(violation) {
49
+ // Override severity if necessary (already set in violation)
50
+ return violation;
51
+ }
52
+ /**
53
+ * Converts a side effect violation to a Failure object
54
+ */
55
+ export function violationToFailure(violation, createFailure) {
56
+ return createFailure(violation.description, [violation.file], violation.hint, `Side-Effect: ${getRuleTitle(violation.rule)}`, violation.line, violation.line, violation.severity);
57
+ }
58
+ /**
59
+ * Groups violations by severity level
60
+ */
61
+ export function groupBySeverity(violations) {
62
+ const grouped = {
63
+ critical: [],
64
+ high: [],
65
+ medium: [],
66
+ low: [],
67
+ };
68
+ for (const violation of violations) {
69
+ const severity = violation.severity;
70
+ if (severity in grouped) {
71
+ grouped[severity].push(violation);
72
+ }
73
+ }
74
+ return grouped;
75
+ }
76
+ /**
77
+ * Filters violations by severity threshold
78
+ */
79
+ export function filterBySeverity(violations, minSeverity) {
80
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
81
+ const minLevel = severityOrder[minSeverity];
82
+ return violations.filter(v => severityOrder[v.severity] <= minLevel);
83
+ }
@@ -32,11 +32,9 @@
32
32
  *
33
33
  * This is a CORE gate (enabled by default, provenance: ai-drift).
34
34
  * Supports: JS/TS, Python, Go, Rust, C#, Java, Ruby
35
- *
36
- * @since v4.3.0
37
35
  */
38
- import { Gate, GateContext } from './base.js';
39
- import { Failure, Provenance } from '../types/index.js';
36
+ import { Gate, GateContext } from '../base.js';
37
+ import { Failure, Provenance } from '../../types/index.js';
40
38
  export interface SideEffectAnalysisConfig {
41
39
  enabled?: boolean;
42
40
  check_unbounded_timers?: boolean;
@@ -63,5 +61,5 @@ export declare class SideEffectAnalysisGate extends Gate {
63
61
  private checkResourceLifecycle;
64
62
  private checkRecursiveDepth;
65
63
  private checkAutoRestart;
66
- private buildFailures;
67
64
  }
65
+ export { SideEffectViolation, SideEffectLang } from '../side-effect-helpers/index.js';
@@ -32,25 +32,33 @@
32
32
  *
33
33
  * This is a CORE gate (enabled by default, provenance: ai-drift).
34
34
  * Supports: JS/TS, Python, Go, Rust, C#, Java, Ruby
35
- *
36
- * @since v4.3.0
37
35
  */
38
- import { Gate } from './base.js';
39
- import { FileScanner } from '../utils/scanner.js';
40
- import { Logger } from '../utils/logger.js';
36
+ import { Gate } from '../base.js';
37
+ import { FileScanner } from '../../utils/scanner.js';
38
+ import { Logger } from '../../utils/logger.js';
41
39
  import fs from 'fs-extra';
42
40
  import path from 'path';
43
41
  import { LANG_MAP, FILE_GLOBS, stripStrings,
44
- // Scope analysis
45
- findEnclosingFunction, extractLoopBody, extractFunctionDefs,
46
- // Variable binding
47
- extractVariableBinding, hasCleanupForVariable,
48
- // Framework awareness
49
- isInUseEffectWithCleanup, hasGoDefer, isPythonWithStatement, isJavaTryWithResources, isCSharpUsing, isRubyBlockForm, isRustAutoDropped, isInsideCleanupContext,
50
42
  // Detectors
51
43
  isTimerCreation, getTimerCleanupPatterns, isProcessSpawn, getProcessCleanupPatterns, isUnboundedLoop, containsIO, isFileWatcher, extractWatchedPath, extractWritePath, pathsOverlap, findWriteInBody, hasDebounceProtection, isResourceOpen, getResourceClosePatterns, isExitHandler,
52
44
  // Loop/recursion
53
- hasRetryLimit, hasCatchWithContinue, hasBaseCase, hasDepthParameter, } from './side-effect-helpers.js';
45
+ hasRetryLimit, hasCatchWithContinue, hasBaseCase, hasDepthParameter,
46
+ // Variable binding
47
+ extractVariableBinding, hasCleanupForVariable,
48
+ // Go defer
49
+ hasGoDefer,
50
+ // Python with
51
+ isPythonWithStatement,
52
+ // Java try-with
53
+ isJavaTryWithResources,
54
+ // C# using
55
+ isCSharpUsing,
56
+ // Ruby block
57
+ isRubyBlockForm,
58
+ // Rust
59
+ isRustAutoDropped, } from '../side-effect-helpers/index.js';
60
+ import { findFunctionScope, getBlockBody, isInFrameworkCleanup, getAllFunctions, } from './scope-tracker.js';
61
+ import { violationToFailure, } from './categorizer.js';
54
62
  export class SideEffectAnalysisGate extends Gate {
55
63
  cfg;
56
64
  constructor(config = {}) {
@@ -102,7 +110,7 @@ export class SideEffectAnalysisGate extends Gate {
102
110
  }
103
111
  catch { /* skip unreadable files */ }
104
112
  }
105
- return this.buildFailures(violations);
113
+ return violations.map(v => violationToFailure(v, (msg, files, hint, title, sl, el, sev) => this.createFailure(msg, files, hint, title, sl, el, sev)));
106
114
  }
107
115
  scanFile(lang, lines, content, file, violations) {
108
116
  if (this.cfg.check_unbounded_timers) {
@@ -148,14 +156,10 @@ export class SideEffectAnalysisGate extends Gate {
148
156
  // Extract variable binding: `const timer = setInterval(...)`
149
157
  const varName = extractVariableBinding(lines[i], lang);
150
158
  // Find enclosing function scope
151
- const scope = findEnclosingFunction(lines, i, lang);
152
- // FRAMEWORK CHECK: React useEffect with cleanup return
153
- if ((lang === 'js' || lang === 'ts') && isInUseEffectWithCleanup(lines, i)) {
154
- continue; // useEffect cleanup handles it
155
- }
156
- // FRAMEWORK CHECK: Inside a cleanup/teardown context already
157
- if (isInsideCleanupContext(lines, i, lang)) {
158
- continue; // Timer in cleanup = intentional short-lived timer
159
+ const scope = findFunctionScope(lines, i, lang);
160
+ // FRAMEWORK CHECK: React useEffect with cleanup return or cleanup context
161
+ if (isInFrameworkCleanup(lines, i, lang)) {
162
+ continue; // Framework cleanup handles it
159
163
  }
160
164
  if (varName) {
161
165
  // Variable is stored — check for cleanup using that specific variable
@@ -202,7 +206,7 @@ export class SideEffectAnalysisGate extends Gate {
202
206
  if (!spawnMatch)
203
207
  continue;
204
208
  const varName = extractVariableBinding(lines[i], lang);
205
- const scope = findEnclosingFunction(lines, i, lang);
209
+ const scope = findFunctionScope(lines, i, lang);
206
210
  // Check if the process result is awaited on the same line
207
211
  if (/\bawait\b/.test(lines[i]))
208
212
  continue;
@@ -263,7 +267,7 @@ export class SideEffectAnalysisGate extends Gate {
263
267
  if (!isUnboundedLoop(lines[i], lang))
264
268
  continue;
265
269
  // Extract actual loop body using scope tracking
266
- const { body, end } = extractLoopBody(lines, i, lang);
270
+ const { body, end } = getBlockBody(lines, i, lang);
267
271
  // Check for I/O inside the extracted body
268
272
  if (!containsIO(body, lang))
269
273
  continue;
@@ -308,7 +312,7 @@ export class SideEffectAnalysisGate extends Gate {
308
312
  const isLoop = isUnboundedLoop(lines[i], lang) || /\bwhile\s*\(/.test(stripped);
309
313
  if (!isLoop)
310
314
  continue;
311
- const { body, end } = extractLoopBody(lines, i, lang);
315
+ const { body, end } = getBlockBody(lines, i, lang);
312
316
  // Check if this is actually a retry pattern (catch + continue)
313
317
  if (!hasCatchWithContinue(body, lang))
314
318
  continue;
@@ -341,7 +345,7 @@ export class SideEffectAnalysisGate extends Gate {
341
345
  // Extract the watched path
342
346
  const watchedPath = extractWatchedPath(lines[i]);
343
347
  // Extract the watcher callback body
344
- const { body, end } = extractLoopBody(lines, i, lang);
348
+ const { body, end } = getBlockBody(lines, i, lang);
345
349
  // Check for file write operations in the callback body
346
350
  const writeCall = findWriteInBody(body, lang);
347
351
  if (!writeCall)
@@ -426,10 +430,10 @@ export class SideEffectAnalysisGate extends Gate {
426
430
  if (lang === 'rs' && isRustAutoDropped(lines, i))
427
431
  continue;
428
432
  // Inside a cleanup context (finally, __exit__, Dispose, etc.)
429
- if (isInsideCleanupContext(lines, i, lang))
433
+ if (isInFrameworkCleanup(lines, i, lang))
430
434
  continue;
431
435
  // ── VARIABLE-BOUND CLEANUP CHECK ──
432
- const scope = findEnclosingFunction(lines, i, lang);
436
+ const scope = findFunctionScope(lines, i, lang);
433
437
  if (varName) {
434
438
  // Check for cleanup using the specific variable
435
439
  const hasPairedCleanup = hasCleanupForVariable(lines, varName, scope.start, scope.end, closePats, lang);
@@ -472,7 +476,7 @@ export class SideEffectAnalysisGate extends Gate {
472
476
  // it's just a stack overflow — bad but not a side effect).
473
477
  // ═══════════════════════════════════════════════════════════════
474
478
  checkRecursiveDepth(lang, lines, file, violations) {
475
- const funcDefs = extractFunctionDefs(lines, lang);
479
+ const funcDefs = getAllFunctions(lines, lang);
476
480
  for (const func of funcDefs) {
477
481
  const bodyLines = lines.slice(func.start + 1, func.end);
478
482
  const body = bodyLines.join('\n');
@@ -517,7 +521,7 @@ export class SideEffectAnalysisGate extends Gate {
517
521
  if (!isExitHandler(lines[i], lang))
518
522
  continue;
519
523
  // Extract the handler body
520
- const { body, end } = extractLoopBody(lines, i, lang);
524
+ const { body, end } = getBlockBody(lines, i, lang);
521
525
  // Check if the handler spawns a process
522
526
  const hasSpawn = body.split('\n').some(l => isProcessSpawn(l, lang) !== null);
523
527
  if (!hasSpawn)
@@ -541,20 +545,4 @@ export class SideEffectAnalysisGate extends Gate {
541
545
  });
542
546
  }
543
547
  }
544
- // ═══════════════════════════════════════════════════════════════
545
- // OUTPUT
546
- // ═══════════════════════════════════════════════════════════════
547
- buildFailures(violations) {
548
- return violations.map(v => this.createFailure(v.description, [v.file], v.hint, `Side-Effect: ${RULE_TITLES[v.rule] || v.rule}`, v.line, v.line, v.severity));
549
- }
550
548
  }
551
- const RULE_TITLES = {
552
- 'unbounded-timer': 'Unbounded Timer',
553
- 'orphan-process': 'Orphan Process',
554
- 'unbounded-io-loop': 'Unbounded I/O Loop',
555
- 'retry-without-limit': 'Retry Without Limit',
556
- 'circular-trigger': 'Circular File Trigger',
557
- 'resource-leak': 'Resource Leak',
558
- 'unbounded-recursion': 'Unbounded Recursion',
559
- 'auto-restart-bomb': 'Auto-Restart Bomb',
560
- };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Scope and Context Tracking for Side-Effect Analysis
3
+ *
4
+ * Tracks scope boundaries, context tracking, and state during analysis.
5
+ */
6
+ import { SideEffectLang } from '../side-effect-helpers/index.js';
7
+ /**
8
+ * Finds the enclosing function scope for a given line index.
9
+ * Returns start and end line numbers of the scope.
10
+ */
11
+ export declare function findFunctionScope(lines: string[], lineIndex: number, lang: SideEffectLang): {
12
+ start: number;
13
+ end: number;
14
+ };
15
+ /**
16
+ * Extracts the body of a loop or block starting at the given line.
17
+ * Handles scope-aware block detection (brace/indent tracking).
18
+ */
19
+ export declare function getBlockBody(lines: string[], lineIndex: number, lang: SideEffectLang): {
20
+ body: string;
21
+ end: number;
22
+ };
23
+ /**
24
+ * Checks if a timer creation is in a React useEffect with cleanup return.
25
+ * Framework-aware pattern detection for safe side effects.
26
+ */
27
+ export declare function isInFrameworkCleanup(lines: string[], lineIndex: number, lang: SideEffectLang): boolean;
28
+ /**
29
+ * Extracts all function definitions from the source code.
30
+ * Used for recursion analysis.
31
+ */
32
+ export declare function getAllFunctions(lines: string[], lang: SideEffectLang): Array<{
33
+ name: string;
34
+ start: number;
35
+ end: number;
36
+ params: string;
37
+ }>;