@oculum/scanner 1.0.11 → 1.0.12

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 (309) hide show
  1. package/dist/ai-context/index.d.ts +6 -0
  2. package/dist/ai-context/index.d.ts.map +1 -0
  3. package/dist/ai-context/index.js +13 -0
  4. package/dist/ai-context/index.js.map +1 -0
  5. package/dist/ai-context/manager.d.ts +67 -0
  6. package/dist/ai-context/manager.d.ts.map +1 -0
  7. package/dist/ai-context/manager.js +104 -0
  8. package/dist/ai-context/manager.js.map +1 -0
  9. package/dist/category-filter.d.ts +125 -0
  10. package/dist/category-filter.d.ts.map +1 -0
  11. package/dist/category-filter.js +360 -0
  12. package/dist/category-filter.js.map +1 -0
  13. package/dist/filtering/context-adjustments.d.ts +23 -0
  14. package/dist/filtering/context-adjustments.d.ts.map +1 -0
  15. package/dist/filtering/context-adjustments.js +100 -0
  16. package/dist/filtering/context-adjustments.js.map +1 -0
  17. package/dist/filtering/index.d.ts +3 -0
  18. package/dist/filtering/index.d.ts.map +1 -0
  19. package/dist/filtering/index.js +8 -0
  20. package/dist/filtering/index.js.map +1 -0
  21. package/dist/filtering/pipeline.d.ts +48 -0
  22. package/dist/filtering/pipeline.d.ts.map +1 -0
  23. package/dist/filtering/pipeline.js +76 -0
  24. package/dist/filtering/pipeline.js.map +1 -0
  25. package/dist/formatters/ai-context.d.ts +23 -0
  26. package/dist/formatters/ai-context.d.ts.map +1 -0
  27. package/dist/formatters/ai-context.js +238 -0
  28. package/dist/formatters/ai-context.js.map +1 -0
  29. package/dist/formatters/github-comment.d.ts +1 -1
  30. package/dist/formatters/github-comment.d.ts.map +1 -1
  31. package/dist/formatters/github-comment.js +2 -2
  32. package/dist/formatters/github-comment.js.map +1 -1
  33. package/dist/formatters/ide/claude-code.d.ts +17 -0
  34. package/dist/formatters/ide/claude-code.d.ts.map +1 -0
  35. package/dist/formatters/ide/claude-code.js +94 -0
  36. package/dist/formatters/ide/claude-code.js.map +1 -0
  37. package/dist/formatters/ide/cursor.d.ts +13 -0
  38. package/dist/formatters/ide/cursor.d.ts.map +1 -0
  39. package/dist/formatters/ide/cursor.js +125 -0
  40. package/dist/formatters/ide/cursor.js.map +1 -0
  41. package/dist/formatters/ide/index.d.ts +62 -0
  42. package/dist/formatters/ide/index.d.ts.map +1 -0
  43. package/dist/formatters/ide/index.js +184 -0
  44. package/dist/formatters/ide/index.js.map +1 -0
  45. package/dist/formatters/ide/windsurf.d.ts +13 -0
  46. package/dist/formatters/ide/windsurf.d.ts.map +1 -0
  47. package/dist/formatters/ide/windsurf.js +117 -0
  48. package/dist/formatters/ide/windsurf.js.map +1 -0
  49. package/dist/formatters/index.d.ts +2 -0
  50. package/dist/formatters/index.d.ts.map +1 -1
  51. package/dist/formatters/index.js +17 -1
  52. package/dist/formatters/index.js.map +1 -1
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +272 -44
  56. package/dist/index.js.map +1 -1
  57. package/dist/layer1/comments.d.ts +4 -1
  58. package/dist/layer1/comments.d.ts.map +1 -1
  59. package/dist/layer1/comments.js +1 -1
  60. package/dist/layer1/comments.js.map +1 -1
  61. package/dist/layer1/config-audit.d.ts +4 -1
  62. package/dist/layer1/config-audit.d.ts.map +1 -1
  63. package/dist/layer1/config-audit.js +45 -11
  64. package/dist/layer1/config-audit.js.map +1 -1
  65. package/dist/layer1/config-mcp-audit.d.ts +4 -1
  66. package/dist/layer1/config-mcp-audit.d.ts.map +1 -1
  67. package/dist/layer1/config-mcp-audit.js +2 -2
  68. package/dist/layer1/config-mcp-audit.js.map +1 -1
  69. package/dist/layer1/entropy.d.ts +4 -1
  70. package/dist/layer1/entropy.d.ts.map +1 -1
  71. package/dist/layer1/entropy.js +212 -1
  72. package/dist/layer1/entropy.js.map +1 -1
  73. package/dist/layer1/file-flags.d.ts +4 -1
  74. package/dist/layer1/file-flags.d.ts.map +1 -1
  75. package/dist/layer1/file-flags.js +12 -5
  76. package/dist/layer1/file-flags.js.map +1 -1
  77. package/dist/layer1/index.d.ts.map +1 -1
  78. package/dist/layer1/index.js +14 -19
  79. package/dist/layer1/index.js.map +1 -1
  80. package/dist/layer1/patterns.d.ts +4 -1
  81. package/dist/layer1/patterns.d.ts.map +1 -1
  82. package/dist/layer1/patterns.js +34 -4
  83. package/dist/layer1/patterns.js.map +1 -1
  84. package/dist/layer1/urls.d.ts +4 -1
  85. package/dist/layer1/urls.d.ts.map +1 -1
  86. package/dist/layer1/urls.js +162 -14
  87. package/dist/layer1/urls.js.map +1 -1
  88. package/dist/layer1/weak-crypto.d.ts +4 -1
  89. package/dist/layer1/weak-crypto.d.ts.map +1 -1
  90. package/dist/layer1/weak-crypto.js +144 -7
  91. package/dist/layer1/weak-crypto.js.map +1 -1
  92. package/dist/layer2/ai-agent-tools.d.ts +4 -1
  93. package/dist/layer2/ai-agent-tools.d.ts.map +1 -1
  94. package/dist/layer2/ai-agent-tools.js +661 -2
  95. package/dist/layer2/ai-agent-tools.js.map +1 -1
  96. package/dist/layer2/ai-endpoint-protection.d.ts +2 -0
  97. package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -1
  98. package/dist/layer2/ai-endpoint-protection.js +1 -1
  99. package/dist/layer2/ai-endpoint-protection.js.map +1 -1
  100. package/dist/layer2/ai-execution-sinks.d.ts +4 -1
  101. package/dist/layer2/ai-execution-sinks.d.ts.map +1 -1
  102. package/dist/layer2/ai-execution-sinks.js +252 -43
  103. package/dist/layer2/ai-execution-sinks.js.map +1 -1
  104. package/dist/layer2/ai-fingerprinting.d.ts +4 -1
  105. package/dist/layer2/ai-fingerprinting.d.ts.map +1 -1
  106. package/dist/layer2/ai-fingerprinting.js +25 -32
  107. package/dist/layer2/ai-fingerprinting.js.map +1 -1
  108. package/dist/layer2/ai-mcp-security.d.ts +4 -1
  109. package/dist/layer2/ai-mcp-security.d.ts.map +1 -1
  110. package/dist/layer2/ai-mcp-security.js +200 -2
  111. package/dist/layer2/ai-mcp-security.js.map +1 -1
  112. package/dist/layer2/ai-package-hallucination.d.ts +4 -1
  113. package/dist/layer2/ai-package-hallucination.d.ts.map +1 -1
  114. package/dist/layer2/ai-package-hallucination.js +136 -4
  115. package/dist/layer2/ai-package-hallucination.js.map +1 -1
  116. package/dist/layer2/ai-prompt-hygiene.d.ts +4 -1
  117. package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -1
  118. package/dist/layer2/ai-prompt-hygiene.js +342 -28
  119. package/dist/layer2/ai-prompt-hygiene.js.map +1 -1
  120. package/dist/layer2/ai-rag-safety.d.ts +4 -1
  121. package/dist/layer2/ai-rag-safety.d.ts.map +1 -1
  122. package/dist/layer2/ai-rag-safety.js +82 -2
  123. package/dist/layer2/ai-rag-safety.js.map +1 -1
  124. package/dist/layer2/ai-schema-validation.d.ts +4 -1
  125. package/dist/layer2/ai-schema-validation.d.ts.map +1 -1
  126. package/dist/layer2/ai-schema-validation.js +2 -2
  127. package/dist/layer2/ai-schema-validation.js.map +1 -1
  128. package/dist/layer2/auth-antipatterns.d.ts +2 -0
  129. package/dist/layer2/auth-antipatterns.d.ts.map +1 -1
  130. package/dist/layer2/auth-antipatterns.js +205 -20
  131. package/dist/layer2/auth-antipatterns.js.map +1 -1
  132. package/dist/layer2/byok-patterns.d.ts +4 -1
  133. package/dist/layer2/byok-patterns.d.ts.map +1 -1
  134. package/dist/layer2/byok-patterns.js +2 -2
  135. package/dist/layer2/byok-patterns.js.map +1 -1
  136. package/dist/layer2/dangerous-functions/dom-xss.d.ts +9 -4
  137. package/dist/layer2/dangerous-functions/dom-xss.d.ts.map +1 -1
  138. package/dist/layer2/dangerous-functions/dom-xss.js +73 -22
  139. package/dist/layer2/dangerous-functions/dom-xss.js.map +1 -1
  140. package/dist/layer2/dangerous-functions/index.d.ts +4 -1
  141. package/dist/layer2/dangerous-functions/index.d.ts.map +1 -1
  142. package/dist/layer2/dangerous-functions/index.js +551 -20
  143. package/dist/layer2/dangerous-functions/index.js.map +1 -1
  144. package/dist/layer2/dangerous-functions/math-random.d.ts +54 -4
  145. package/dist/layer2/dangerous-functions/math-random.d.ts.map +1 -1
  146. package/dist/layer2/dangerous-functions/math-random.js +241 -16
  147. package/dist/layer2/dangerous-functions/math-random.js.map +1 -1
  148. package/dist/layer2/dangerous-functions/patterns.d.ts.map +1 -1
  149. package/dist/layer2/dangerous-functions/patterns.js +3 -1
  150. package/dist/layer2/dangerous-functions/patterns.js.map +1 -1
  151. package/dist/layer2/dangerous-functions/utils/control-flow.d.ts +3 -2
  152. package/dist/layer2/dangerous-functions/utils/control-flow.d.ts.map +1 -1
  153. package/dist/layer2/dangerous-functions/utils/control-flow.js +41 -120
  154. package/dist/layer2/dangerous-functions/utils/control-flow.js.map +1 -1
  155. package/dist/layer2/dangerous-functions/utils/helpers.d.ts.map +1 -1
  156. package/dist/layer2/dangerous-functions/utils/helpers.js +26 -3
  157. package/dist/layer2/dangerous-functions/utils/helpers.js.map +1 -1
  158. package/dist/layer2/dangerous-functions/utils/schema-validation.d.ts.map +1 -1
  159. package/dist/layer2/dangerous-functions/utils/schema-validation.js +14 -1
  160. package/dist/layer2/dangerous-functions/utils/schema-validation.js.map +1 -1
  161. package/dist/layer2/data-exposure.d.ts +4 -1
  162. package/dist/layer2/data-exposure.d.ts.map +1 -1
  163. package/dist/layer2/data-exposure.js +11 -38
  164. package/dist/layer2/data-exposure.js.map +1 -1
  165. package/dist/layer2/framework-checks.d.ts +4 -1
  166. package/dist/layer2/framework-checks.d.ts.map +1 -1
  167. package/dist/layer2/framework-checks.js +2 -2
  168. package/dist/layer2/framework-checks.js.map +1 -1
  169. package/dist/layer2/index.d.ts +9 -1
  170. package/dist/layer2/index.d.ts.map +1 -1
  171. package/dist/layer2/index.js +57 -51
  172. package/dist/layer2/index.js.map +1 -1
  173. package/dist/layer2/logic-gates.d.ts +4 -1
  174. package/dist/layer2/logic-gates.d.ts.map +1 -1
  175. package/dist/layer2/logic-gates.js +54 -20
  176. package/dist/layer2/logic-gates.js.map +1 -1
  177. package/dist/layer2/model-supply-chain.d.ts +4 -1
  178. package/dist/layer2/model-supply-chain.d.ts.map +1 -1
  179. package/dist/layer2/model-supply-chain.js +72 -4
  180. package/dist/layer2/model-supply-chain.js.map +1 -1
  181. package/dist/layer2/risky-imports.d.ts +4 -1
  182. package/dist/layer2/risky-imports.d.ts.map +1 -1
  183. package/dist/layer2/risky-imports.js +2 -2
  184. package/dist/layer2/risky-imports.js.map +1 -1
  185. package/dist/layer2/variables.d.ts +4 -1
  186. package/dist/layer2/variables.d.ts.map +1 -1
  187. package/dist/layer2/variables.js +2 -2
  188. package/dist/layer2/variables.js.map +1 -1
  189. package/dist/layer3/anthropic/auto-dismiss.d.ts.map +1 -1
  190. package/dist/layer3/anthropic/auto-dismiss.js +11 -0
  191. package/dist/layer3/anthropic/auto-dismiss.js.map +1 -1
  192. package/dist/modes/incremental.js +1 -1
  193. package/dist/tiers.d.ts +2 -2
  194. package/dist/tiers.d.ts.map +1 -1
  195. package/dist/tiers.js +7 -7
  196. package/dist/tiers.js.map +1 -1
  197. package/dist/types.d.ts +78 -8
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/types.js +34 -0
  200. package/dist/types.js.map +1 -1
  201. package/dist/utils/code-analysis.d.ts +39 -0
  202. package/dist/utils/code-analysis.d.ts.map +1 -0
  203. package/dist/utils/code-analysis.js +159 -0
  204. package/dist/utils/code-analysis.js.map +1 -0
  205. package/dist/utils/comment-analyzer.d.ts +38 -0
  206. package/dist/utils/comment-analyzer.d.ts.map +1 -0
  207. package/dist/utils/comment-analyzer.js +218 -0
  208. package/dist/utils/comment-analyzer.js.map +1 -0
  209. package/dist/utils/context-helpers.d.ts +108 -1
  210. package/dist/utils/context-helpers.d.ts.map +1 -1
  211. package/dist/utils/context-helpers.js +351 -2
  212. package/dist/utils/context-helpers.js.map +1 -1
  213. package/dist/utils/environment-context.d.ts +76 -0
  214. package/dist/utils/environment-context.d.ts.map +1 -0
  215. package/dist/utils/environment-context.js +271 -0
  216. package/dist/utils/environment-context.js.map +1 -0
  217. package/dist/utils/intent-detector.d.ts +66 -0
  218. package/dist/utils/intent-detector.d.ts.map +1 -0
  219. package/dist/utils/intent-detector.js +282 -0
  220. package/dist/utils/intent-detector.js.map +1 -0
  221. package/dist/utils/parsed-file.d.ts +51 -0
  222. package/dist/utils/parsed-file.d.ts.map +1 -0
  223. package/dist/utils/parsed-file.js +95 -0
  224. package/dist/utils/parsed-file.js.map +1 -0
  225. package/dist/utils/route-hierarchy.d.ts +50 -0
  226. package/dist/utils/route-hierarchy.d.ts.map +1 -0
  227. package/dist/utils/route-hierarchy.js +226 -0
  228. package/dist/utils/route-hierarchy.js.map +1 -0
  229. package/dist/utils/schema-semantics.d.ts +45 -0
  230. package/dist/utils/schema-semantics.d.ts.map +1 -0
  231. package/dist/utils/schema-semantics.js +193 -0
  232. package/dist/utils/schema-semantics.js.map +1 -0
  233. package/package.json +1 -1
  234. package/src/__tests__/benchmark/fixtures/layer2/index.ts +12 -0
  235. package/src/__tests__/benchmark/fixtures/layer2/phase5-excessive-agency.ts +580 -0
  236. package/src/__tests__/benchmark/fixtures/layer2/sprint6-ai-enhancements.ts +515 -0
  237. package/src/__tests__/benchmark/run-depth-validation.ts +9 -9
  238. package/src/__tests__/category-filter.test.ts +478 -0
  239. package/src/__tests__/regression/known-false-positives.test.ts +490 -0
  240. package/src/__tests__/snapshots/__snapshots__/anthropic-validation-refactor.test.ts.snap +18 -14
  241. package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +0 -9
  242. package/src/__tests__/snapshots/anthropic-validation-refactor.test.ts +1 -1
  243. package/src/__tests__/validation/run-validation.ts +7 -7
  244. package/src/ai-context/__tests__/manager.test.ts +193 -0
  245. package/src/ai-context/index.ts +15 -0
  246. package/src/ai-context/manager.ts +145 -0
  247. package/src/baseline/__tests__/manager.test.ts +2 -2
  248. package/src/category-filter.ts +400 -0
  249. package/src/filtering/__tests__/pipeline.test.ts +134 -0
  250. package/src/filtering/context-adjustments.ts +111 -0
  251. package/src/filtering/index.ts +10 -0
  252. package/src/filtering/pipeline.ts +130 -0
  253. package/src/formatters/__tests__/ai-context.test.ts +254 -0
  254. package/src/formatters/ai-context.ts +302 -0
  255. package/src/formatters/github-comment.ts +3 -3
  256. package/src/formatters/ide/__tests__/ide.test.ts +319 -0
  257. package/src/formatters/ide/claude-code.ts +110 -0
  258. package/src/formatters/ide/cursor.ts +147 -0
  259. package/src/formatters/ide/index.ts +216 -0
  260. package/src/formatters/ide/windsurf.ts +135 -0
  261. package/src/formatters/index.ts +24 -0
  262. package/src/index.ts +312 -34
  263. package/src/layer1/comments.ts +3 -1
  264. package/src/layer1/config-audit.ts +50 -11
  265. package/src/layer1/config-mcp-audit.ts +4 -2
  266. package/src/layer1/entropy.ts +234 -1
  267. package/src/layer1/file-flags.ts +17 -6
  268. package/src/layer1/index.ts +14 -18
  269. package/src/layer1/patterns.ts +42 -4
  270. package/src/layer1/urls.ts +188 -14
  271. package/src/layer1/weak-crypto.ts +168 -16
  272. package/src/layer2/ai-agent-tools.ts +707 -2
  273. package/src/layer2/ai-endpoint-protection.ts +3 -1
  274. package/src/layer2/ai-execution-sinks.ts +265 -43
  275. package/src/layer2/ai-fingerprinting.ts +28 -32
  276. package/src/layer2/ai-mcp-security.ts +206 -3
  277. package/src/layer2/ai-package-hallucination.ts +153 -4
  278. package/src/layer2/ai-prompt-hygiene.ts +369 -26
  279. package/src/layer2/ai-rag-safety.ts +85 -2
  280. package/src/layer2/ai-schema-validation.ts +4 -2
  281. package/src/layer2/auth-antipatterns.ts +230 -20
  282. package/src/layer2/byok-patterns.ts +4 -2
  283. package/src/layer2/dangerous-functions/dom-xss.ts +94 -22
  284. package/src/layer2/dangerous-functions/index.ts +635 -51
  285. package/src/layer2/dangerous-functions/math-random.ts +268 -16
  286. package/src/layer2/dangerous-functions/patterns.ts +3 -1
  287. package/src/layer2/dangerous-functions/utils/control-flow.ts +8 -135
  288. package/src/layer2/dangerous-functions/utils/schema-validation.ts +16 -1
  289. package/src/layer2/data-exposure.ts +13 -38
  290. package/src/layer2/framework-checks.ts +4 -2
  291. package/src/layer2/index.ts +69 -50
  292. package/src/layer2/logic-gates.ts +59 -22
  293. package/src/layer2/model-supply-chain.ts +79 -4
  294. package/src/layer2/risky-imports.ts +4 -2
  295. package/src/layer2/variables.ts +4 -2
  296. package/src/layer3/anthropic/auto-dismiss.ts +11 -0
  297. package/src/modes/incremental.ts +1 -1
  298. package/src/tiers.ts +9 -9
  299. package/src/types.ts +122 -8
  300. package/src/utils/__tests__/code-analysis.test.ts +165 -0
  301. package/src/utils/__tests__/parsed-file.test.ts +124 -0
  302. package/src/utils/code-analysis.ts +179 -0
  303. package/src/utils/comment-analyzer.ts +249 -0
  304. package/src/utils/context-helpers.ts +408 -2
  305. package/src/utils/environment-context.ts +304 -0
  306. package/src/utils/intent-detector.ts +318 -0
  307. package/src/utils/parsed-file.ts +103 -0
  308. package/src/utils/route-hierarchy.ts +250 -0
  309. package/src/utils/schema-semantics.ts +233 -0
@@ -10,8 +10,87 @@ import {
10
10
  isSeedOrDataGenFile,
11
11
  isEducationalVulnerabilityFile,
12
12
  } from '../../utils/context-helpers'
13
+ import type { ParsedFile } from '../../utils/parsed-file'
13
14
  import { extractFunctionContext } from './utils/control-flow'
14
15
 
16
+ /**
17
+ * Check if Math.random() is used in a jitter/backoff/retry context
18
+ * These are legitimate uses of Math.random() for network resilience,
19
+ * not security-sensitive randomness.
20
+ *
21
+ * Examples:
22
+ * const delay = baseDelay + Math.random() * jitter
23
+ * setTimeout(retry, delay * Math.random())
24
+ * await sleep(backoff * (1 + Math.random() * 0.1))
25
+ *
26
+ * @param content - Full file content
27
+ * @param lineNumber - The 0-indexed line number where Math.random() was found
28
+ */
29
+ export function isJitterOrBackoffContext(
30
+ content: string,
31
+ lineNumber: number,
32
+ lines?: string[]
33
+ ): boolean {
34
+ const _lines = lines ?? content.split('\n')
35
+ const start = Math.max(0, lineNumber - 10)
36
+ const end = Math.min(_lines.length, lineNumber + 5)
37
+ const context = _lines.slice(start, end).join('\n')
38
+
39
+ // Patterns indicating jitter/backoff/retry context
40
+ const jitterPatterns = [
41
+ // Direct keyword matches
42
+ /\b(jitter|backoff|retry|retries|exponential)\b/i,
43
+ // Delay/timeout with random
44
+ /setTimeout.*Math\.random/i,
45
+ /setInterval.*Math\.random/i,
46
+ /sleep.*Math\.random/i,
47
+ /await.*delay.*Math\.random/i,
48
+ // Common backoff patterns
49
+ /\* Math\.random\(\).*delay/i,
50
+ /delay\s*\*\s*Math\.random/i,
51
+ /Math\.random\(\)\s*\*\s*\d+.*delay/i,
52
+ // Retry-related function names
53
+ /function\s+(retry|withRetry|backoff|withBackoff|exponentialBackoff)/i,
54
+ // Common retry library patterns
55
+ /retryPolicy|retryConfig|retryOptions|maxRetries|retryCount/i,
56
+ // Network resilience patterns
57
+ /\b(throttle|debounce|rateLimit)\b.*Math\.random/i,
58
+ // Sampling/probability for non-security uses
59
+ /sampleRate|samplingRate|probability\s*[<>]=?\s*Math\.random/i,
60
+ ]
61
+
62
+ return jitterPatterns.some(p => p.test(context))
63
+ }
64
+
65
+ /**
66
+ * Check if Math.random() is used in a React key prop context
67
+ * This is a common pattern to force re-renders, not a security issue.
68
+ *
69
+ * Examples:
70
+ * key={Math.random()}
71
+ * key={`prefix-${Math.random()}`}
72
+ * key={Math.random().toString()}
73
+ *
74
+ * @param lineContent - The line of code to check
75
+ * @param filePath - The file path (only check JSX/TSX files)
76
+ */
77
+ export function isReactKeyPattern(lineContent: string, filePath: string): boolean {
78
+ // Only check in JSX/TSX files
79
+ if (!/\.[jt]sx$/.test(filePath)) {
80
+ return false
81
+ }
82
+
83
+ // Pattern: key={...Math.random()...}
84
+ // Matches:
85
+ // key={Math.random()}
86
+ // key={`foo-${Math.random()}`}
87
+ // key={Math.random().toString()}
88
+ // key={`${Math.random()}-bar`}
89
+ // key={something + Math.random()}
90
+ const keyPattern = /key\s*=\s*\{[^}]*Math\.random\(\)/
91
+ return keyPattern.test(lineContent)
92
+ }
93
+
15
94
  /**
16
95
  * Check if Math.random() is used for cosmetic/UI purposes (not security)
17
96
  * Cosmetic uses: CSS values, animations, UI variations, demo data
@@ -20,9 +99,10 @@ import { extractFunctionContext } from './utils/control-flow'
20
99
  export function isCosmeticMathRandom(
21
100
  lineContent: string,
22
101
  content: string,
23
- lineNumber: number
102
+ lineNumber: number,
103
+ lines?: string[]
24
104
  ): boolean {
25
- const lines = content.split('\n')
105
+ const _lines = lines ?? content.split('\n')
26
106
 
27
107
  // Check the line itself for cosmetic indicators
28
108
  const cosmeticLinePatterns = [
@@ -61,8 +141,8 @@ export function isCosmeticMathRandom(
61
141
 
62
142
  // Check surrounding context (5 lines before and after)
63
143
  const contextStart = Math.max(0, lineNumber - 5)
64
- const contextEnd = Math.min(lines.length, lineNumber + 5)
65
- const context = lines.slice(contextStart, contextEnd).join('\n')
144
+ const contextEnd = Math.min(_lines.length, lineNumber + 5)
145
+ const context = _lines.slice(contextStart, contextEnd).join('\n')
66
146
 
67
147
  // Context indicators of cosmetic use
68
148
  const cosmeticContextPatterns = [
@@ -402,7 +482,8 @@ export function classifyVariableNameRisk(
402
482
  export function analyzeMathRandomContext(
403
483
  content: string,
404
484
  filePath: string,
405
- lineNumber: number
485
+ lineNumber: number,
486
+ lines?: string[]
406
487
  ): {
407
488
  inSecurityContext: boolean
408
489
  inTestContext: boolean
@@ -410,10 +491,10 @@ export function analyzeMathRandomContext(
410
491
  inBusinessLogicContext: boolean
411
492
  contextDescription: string
412
493
  } {
413
- const lines = content.split('\n')
494
+ const _lines = lines ?? content.split('\n')
414
495
  const start = Math.max(0, lineNumber - 10)
415
- const end = Math.min(lines.length, lineNumber + 5)
416
- const context = lines.slice(start, end).join('\n')
496
+ const end = Math.min(_lines.length, lineNumber + 5)
497
+ const context = _lines.slice(start, end).join('\n')
417
498
 
418
499
  // Security context indicators (functions, imports, comments)
419
500
  const securityPatterns = [
@@ -438,8 +519,8 @@ export function analyzeMathRandomContext(
438
519
  testContextPatterns.some(p => p.test(context))
439
520
 
440
521
  // UI/cosmetic context (reuse existing logic)
441
- const lineContent = lines[lineNumber]
442
- const inUIContext = isCosmeticMathRandom(lineContent, content, lineNumber)
522
+ const lineContent = _lines[lineNumber]
523
+ const inUIContext = isCosmeticMathRandom(lineContent, content, lineNumber, _lines)
443
524
 
444
525
  // Business logic context (non-security ID generation)
445
526
  // Note: UUID/CAPTCHA patterns excluded - handled by functionIntent classification
@@ -448,6 +529,14 @@ export function analyzeMathRandomContext(
448
529
  /\b(reference|tracking|confirmation)Number\b/i,
449
530
  /\b(backoff|retry|jitter|delay|timeout|latency)\b/i,
450
531
  /\b(sample|sampling|probability|chance|rollout|experiment|abtest|cohort|bucket|variant)\b/i,
532
+ // Load balancing and selection patterns
533
+ /mode\s*===?\s*['"]random['"]/i, // mode === 'random'
534
+ /\.\w*index\s*%/i, // round-robin patterns
535
+ /keys?\[.*Math\.random/i, // keys[Math.floor(Math.random() * keys.length)]
536
+ /\[\s*Math\.floor\s*\(\s*Math\.random/i, // array[Math.floor(Math.random()...)]
537
+ /shuffle/i, // shuffle functions
538
+ /pickRandom/i, // pickRandom helpers
539
+ /randomElement/i, // randomElement helpers
451
540
  ]
452
541
  const inBusinessLogicContext =
453
542
  businessLogicPatterns.some(p => p.test(context)) && !inSecurityContext
@@ -473,29 +562,169 @@ export function analyzeMathRandomContext(
473
562
  }
474
563
  }
475
564
 
565
+ /**
566
+ * Check if file is an animation/motion component based on file name
567
+ * Files with animation-related names typically use Math.random for visual effects
568
+ */
569
+ export function isAnimationFile(filePath: string): boolean {
570
+ const animationPatterns = [
571
+ /animated[-_]/i, // animated-document-scanner.tsx
572
+ /[-_]animation/i, // document-animation.tsx
573
+ /motion[-_]/i, // motion-component.tsx
574
+ /[-_]motion/i, // scroll-motion.tsx
575
+ /particles?[-_]/i, // particles-background.tsx
576
+ /confetti/i, // confetti.tsx
577
+ /[-_]effect/i, // hover-effect.tsx
578
+ /transition[-_]/i, // transition-wrapper.tsx
579
+ /visual[-_]/i, // visual-effects.tsx
580
+ /canvas[-_]/i, // canvas-animation.tsx
581
+ ]
582
+ return animationPatterns.some(p => p.test(filePath))
583
+ }
584
+
585
+ /**
586
+ * Check if Math.random() is used for animation/motion coordinates
587
+ * Common in animation libraries like framer-motion, react-spring, Three.js, etc.
588
+ */
589
+ export function isAnimationCoordinateUsage(lineContent: string, context: string): boolean {
590
+ // Object property assignments for coordinates
591
+ const coordinatePatterns = [
592
+ /\bx:\s*Math\.random/i, // x: Math.random()
593
+ /\by:\s*Math\.random/i, // y: Math.random()
594
+ /\bz:\s*Math\.random/i, // z: Math.random()
595
+ /\bleft:\s*.*Math\.random/i, // left: Math.random()
596
+ /\btop:\s*.*Math\.random/i, // top: Math.random()
597
+ /\bright:\s*.*Math\.random/i, // right: Math.random()
598
+ /\bbottom:\s*.*Math\.random/i, // bottom: Math.random()
599
+ /\brotation:\s*.*Math\.random/i, // rotation: Math.random()
600
+ /\brotateX:\s*.*Math\.random/i, // rotateX: Math.random()
601
+ /\brotateY:\s*.*Math\.random/i, // rotateY: Math.random()
602
+ /\brotateZ:\s*.*Math\.random/i, // rotateZ: Math.random()
603
+ /\bscaleX?:\s*.*Math\.random/i, // scale/scaleX: Math.random()
604
+ /\bscaleY:\s*.*Math\.random/i, // scaleY: Math.random()
605
+ /\bopacity:\s*.*Math\.random/i, // opacity: Math.random()
606
+ /\bduration:\s*.*Math\.random/i, // duration: Math.random()
607
+ /\bdelay:\s*.*Math\.random/i, // delay: Math.random()
608
+ // 3D/Three.js specific patterns
609
+ /\boffset\s*=.*Math\.random/i, // offset = Math.random()
610
+ /useMemo\s*\(\s*\(\s*\)\s*=>\s*Math\.random/i, // useMemo(() => Math.random(), [])
611
+ /\bphase\s*[:=].*Math\.random/i, // phase: Math.random() or phase = Math.random()
612
+ /\bspeed\s*[:=].*Math\.random/i, // speed: Math.random()
613
+ /\bangle\s*[:=].*Math\.random/i, // angle: Math.random()
614
+ ]
615
+
616
+ if (coordinatePatterns.some(p => p.test(lineContent))) {
617
+ return true
618
+ }
619
+
620
+ // Motion/animation library context patterns
621
+ const motionLibraryPatterns = [
622
+ /framer-motion/i,
623
+ /react-spring/i,
624
+ /react-motion/i,
625
+ /gsap/i,
626
+ /animejs/i,
627
+ /popmotion/i,
628
+ /motion\.div/i,
629
+ /useSpring/i,
630
+ /useAnimation/i,
631
+ /animate\s*\(/i,
632
+ /keyframes/i,
633
+ // Three.js and React Three Fiber patterns
634
+ /three/i,
635
+ /useFrame/i,
636
+ /@react-three/i,
637
+ /Canvas/i, // React Three Fiber Canvas
638
+ /mesh/i, // Three.js mesh
639
+ /geometry/i, // Three.js geometry
640
+ /material/i, // Three.js material
641
+ ]
642
+
643
+ return motionLibraryPatterns.some(p => p.test(context))
644
+ }
645
+
646
+ /**
647
+ * Check if Math.random() is used in a template placeholder generator context
648
+ * Template systems often use random generators for placeholder values
649
+ *
650
+ * Examples:
651
+ * const templates = { random: () => Math.random().toString() }
652
+ * random_hex: () => Math.random().toString(16)
653
+ * {{random}} placeholder generation
654
+ */
655
+ export function isTemplatePlaceholderGenerator(
656
+ line: string,
657
+ content: string,
658
+ lineNumber: number,
659
+ lines?: string[]
660
+ ): boolean {
661
+ const _lines = lines ?? content.split('\n')
662
+ const contextStart = Math.max(0, lineNumber - 10)
663
+ const contextEnd = Math.min(_lines.length, lineNumber + 5)
664
+ const context = _lines.slice(contextStart, contextEnd).join('\n')
665
+
666
+ const templatePatterns = [
667
+ /\{\{random\w*\}\}/i, // {{random}}, {{random_hex}}, etc.
668
+ /random:\s*\(\s*\)\s*=>\s*Math\.random/i, // random: () => Math.random()
669
+ /random_\w+:\s*\(\s*\)\s*=>/i, // random_int: () => ...
670
+ /placeholder.*random/i, // placeholder context
671
+ /templates?\s*[=:]\s*\{/i, // templates = { or template: {
672
+ /generatePlaceholder/i, // generatePlaceholder function
673
+ /placeholder\s*[:=]/i, // placeholder: or placeholder =
674
+ ]
675
+
676
+ return templatePatterns.some(p => p.test(context) || p.test(line))
677
+ }
678
+
476
679
  /**
477
680
  * Check if Math.random() should be skipped entirely
478
- * Returns true for seed files, test fixtures, captcha/puzzle, uuid, and pure cosmetic uses
681
+ * Returns true for seed files, test fixtures, captcha/puzzle, uuid, React keys, jitter/backoff, and pure cosmetic uses
479
682
  */
480
683
  export function shouldSkipMathRandom(
481
684
  content: string,
482
685
  filePath: string,
483
- lineNumber: number
686
+ lineNumber: number,
687
+ options?: { parsed?: ParsedFile }
484
688
  ): boolean {
485
689
  // Seed/data generation files - skip entirely
486
690
  if (isSeedOrDataGenFile(filePath)) {
487
691
  return true
488
692
  }
489
693
 
694
+ // Animation/motion component files - skip entirely
695
+ // These use Math.random for visual effects, not security
696
+ if (isAnimationFile(filePath)) {
697
+ return true
698
+ }
699
+
490
700
  // Educational/intentional vulnerability files - skip entirely
491
701
  // These include OWASP Juice Shop, intentionally-vulnerable examples, etc.
492
702
  if (isEducationalVulnerabilityFile(filePath)) {
493
703
  return true
494
704
  }
495
705
 
706
+ // Check for React key pattern - this is a common pattern to force re-renders
707
+ // It's not a security issue, just a way to reset component state
708
+ const lines = options?.parsed?.lines ?? content.split('\n')
709
+ const lineContent = lines[lineNumber] || ''
710
+ if (isReactKeyPattern(lineContent, filePath)) {
711
+ return true
712
+ }
713
+
714
+ // Template placeholder generators - skip entirely
715
+ // These generate placeholder values for templates, not security tokens
716
+ if (isTemplatePlaceholderGenerator(lineContent, content, lineNumber, lines)) {
717
+ return true
718
+ }
719
+
720
+ // Jitter/backoff/retry patterns - legitimate non-security use of randomness
721
+ // Used for network resilience, rate limiting, exponential backoff, etc.
722
+ if (isJitterOrBackoffContext(content, lineNumber, lines)) {
723
+ return true
724
+ }
725
+
496
726
  // Test files with test fixture patterns
497
727
  if (isTestOrMockFile(filePath)) {
498
- const lines = content.split('\n')
499
728
  const line = lines[lineNumber]
500
729
  // If in a test file and generating test data, skip
501
730
  if (
@@ -507,9 +736,7 @@ export function shouldSkipMathRandom(
507
736
  }
508
737
 
509
738
  // Pure cosmetic usage (CSS values, animations)
510
- const lines = content.split('\n')
511
- const lineContent = lines[lineNumber] || ''
512
- if (isCosmeticMathRandom(lineContent, content, lineNumber)) {
739
+ if (isCosmeticMathRandom(lineContent, content, lineNumber, lines)) {
513
740
  // Additional check: if this is for animation/style, truly skip
514
741
  const pureStylePatterns = [
515
742
  /\.style\./,
@@ -524,6 +751,15 @@ export function shouldSkipMathRandom(
524
751
  }
525
752
  }
526
753
 
754
+ // Animation coordinate usage (x, y, rotation, etc.)
755
+ // Get surrounding context for animation library detection
756
+ const contextStart = Math.max(0, lineNumber - 15)
757
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
758
+ const animContext = lines.slice(contextStart, contextEnd).join('\n')
759
+ if (isAnimationCoordinateUsage(lineContent, animContext)) {
760
+ return true
761
+ }
762
+
527
763
  // Check function context for demo/seed/captcha/uuid functions
528
764
  const functionName = extractFunctionContext(content, lineNumber)
529
765
  const functionIntent = classifyFunctionIntent(functionName)
@@ -533,5 +769,21 @@ export function shouldSkipMathRandom(
533
769
  return true
534
770
  }
535
771
 
772
+ // Check for fallback pattern: crypto.randomUUID?.() ?? Math.random()
773
+ // When a secure primary method is used with Math.random as fallback,
774
+ // the code is safe (Math.random only runs in environments without crypto API)
775
+ const prevLines = lines.slice(Math.max(0, lineNumber - 2), lineNumber + 1).join(' ')
776
+ const multiLineFallbackPatterns = [
777
+ /crypto\??\.?\s*randomUUID\??\.?\s*\(\s*\)\s*\?\?/i, // crypto.randomUUID() ??
778
+ /globalThis\.crypto\??\.?\s*randomUUID\??\.?\s*\(/i, // globalThis.crypto?.randomUUID?.()
779
+ /window\.crypto\??\.?\s*randomUUID\??\.?\s*\(/i, // window.crypto?.randomUUID?.()
780
+ /\?\.\s*randomUUID\??\.?\s*\(\s*\)\s*\?\?/i, // ?.randomUUID?.() ??
781
+ /crypto\??\.?\s*getRandomValues\s*\(/i, // crypto.getRandomValues()
782
+ /randomUUID\??\.?\s*\(\s*\)\s*\?\?/i, // randomUUID?.() ?? (generic)
783
+ ]
784
+ if (multiLineFallbackPatterns.some(p => p.test(prevLines))) {
785
+ return true // Skip - Math.random is only a fallback for missing crypto API
786
+ }
787
+
536
788
  return false
537
789
  }
@@ -58,9 +58,11 @@ export const DANGEROUS_FUNCTIONS: DangerousFunctionPattern[] = [
58
58
  },
59
59
 
60
60
  // SQL injection risks
61
+ // Note: .execute is too generic (used by axios, tRPC, request utilities)
62
+ // Focus on .query and .raw which are more SQL-specific
61
63
  {
62
64
  name: 'Raw SQL query construction',
63
- pattern: /\.(query|execute|raw)\s*\(\s*[`'"].*\$\{|\.query\s*\(\s*['"].*\+/gi,
65
+ pattern: /\.(query|raw)\s*\(\s*[`'"].*\$\{|\.query\s*\(\s*['"].*\+/gi,
64
66
  severity: 'critical',
65
67
  description: 'String concatenation in SQL queries can lead to SQL injection',
66
68
  suggestedFix: 'Use parameterized queries or prepared statements',
@@ -1,65 +1,20 @@
1
1
  /**
2
2
  * Control Flow Analysis Utilities
3
3
  *
4
- * Functions for analyzing code control flow, including try-catch detection
5
- * and function context extraction.
4
+ * Backward-compatible wrappers around shared code-analysis utilities.
5
+ * Existing callers pass (content: string, lineNumber: number) — these
6
+ * wrappers create a temporary ParsedFile to delegate to the shared implementation.
6
7
  */
7
8
 
8
- import { isComment } from '../../../utils/context-helpers'
9
+ import { ParsedFile } from '../../../utils/parsed-file'
10
+ import * as codeAnalysis from '../../../utils/code-analysis'
9
11
 
10
12
  /**
11
13
  * Check if a line is inside a try-catch block
12
14
  * Looks for enclosing try { ... } catch pattern
13
15
  */
14
16
  export function isInsideTryCatch(content: string, lineNumber: number): boolean {
15
- const lines = content.split('\n')
16
-
17
- // Track brace depth and whether we're in a try block
18
- let tryDepth = 0
19
- let inTryBlock = false
20
- const braceStack: Array<'try' | 'other'> = []
21
-
22
- // Scan from start to the target line
23
- for (let i = 0; i < lineNumber && i < lines.length; i++) {
24
- const line = lines[i]
25
-
26
- // Check for try keyword (not in a comment)
27
- if (/\btry\s*\{/.test(line) && !isComment(line)) {
28
- inTryBlock = true
29
- tryDepth++
30
- // Count opening braces on this line
31
- const openBraces = (line.match(/\{/g) || []).length
32
- const closeBraces = (line.match(/\}/g) || []).length
33
- for (let j = 0; j < openBraces - closeBraces; j++) {
34
- braceStack.push('try')
35
- }
36
- } else if (/\bcatch\s*\(/.test(line) && !isComment(line)) {
37
- // Entering catch block - still protected
38
- // Don't decrement tryDepth yet
39
- } else if (/\bfinally\s*\{/.test(line) && !isComment(line)) {
40
- // Entering finally block - still protected
41
- } else {
42
- // Track regular braces
43
- const openBraces = (line.match(/\{/g) || []).length
44
- const closeBraces = (line.match(/\}/g) || []).length
45
-
46
- for (let j = 0; j < openBraces; j++) {
47
- braceStack.push(inTryBlock && tryDepth > 0 ? 'try' : 'other')
48
- }
49
-
50
- for (let j = 0; j < closeBraces; j++) {
51
- const popped = braceStack.pop()
52
- if (popped === 'try') {
53
- tryDepth--
54
- if (tryDepth === 0) {
55
- inTryBlock = false
56
- }
57
- }
58
- }
59
- }
60
- }
61
-
62
- return tryDepth > 0
17
+ return codeAnalysis.isInsideTryCatch(new ParsedFile(content, ''), lineNumber)
63
18
  }
64
19
 
65
20
  /**
@@ -67,39 +22,7 @@ export function isInsideTryCatch(content: string, lineNumber: number): boolean {
67
22
  * Looks for try { before the line and } catch after, within reasonable bounds
68
23
  */
69
24
  export function hasTryCatchNearby(content: string, lineNumber: number, windowSize: number = 20): boolean {
70
- const lines = content.split('\n')
71
- const startLine = Math.max(0, lineNumber - windowSize)
72
- const endLine = Math.min(lines.length, lineNumber + windowSize)
73
-
74
- // Look backward for 'try {'
75
- let foundTry = false
76
- for (let i = lineNumber - 1; i >= startLine; i--) {
77
- const line = lines[i]
78
- if (/\btry\s*\{/.test(line) && !isComment(line)) {
79
- foundTry = true
80
- break
81
- }
82
- // Stop if we hit a function boundary
83
- if (/\b(function|async function|=>|class)\b/.test(line) && /\{/.test(line)) {
84
- break
85
- }
86
- }
87
-
88
- if (!foundTry) return false
89
-
90
- // Look forward for '} catch'
91
- for (let i = lineNumber; i < endLine; i++) {
92
- const line = lines[i]
93
- if (/\}\s*catch\s*\(/.test(line) && !isComment(line)) {
94
- return true
95
- }
96
- // Stop if we hit another function boundary
97
- if (i > lineNumber && /\b(function|async function|class)\b/.test(line) && /\{/.test(line)) {
98
- break
99
- }
100
- }
101
-
102
- return false
25
+ return codeAnalysis.hasTryCatchNearby(new ParsedFile(content, ''), lineNumber, windowSize)
103
26
  }
104
27
 
105
28
  /**
@@ -108,55 +31,5 @@ export function hasTryCatchNearby(content: string, lineNumber: number, windowSiz
108
31
  * Returns lowercase function name or null if not found
109
32
  */
110
33
  export function extractFunctionContext(content: string, lineNumber: number): string | null {
111
- const lines = content.split('\n')
112
- const start = Math.max(0, lineNumber - 20) // Increased from 10 to 20 for nested callbacks
113
-
114
- // Look backwards for function declaration
115
- for (let i = lineNumber; i >= start; i--) {
116
- const line = lines[i]
117
-
118
- // Skip anonymous arrow functions in callbacks (e.g., .map((x) => ...), .replace(/x/g, (c) => ...))
119
- // These are not the function context we're looking for
120
- // Look for pattern: .methodName(..., (param) => or .methodName(...(param) =>
121
- const hasMethodCallWithArrowCallback = /\.\w+\(.*\([^)]*\)\s*=>/.test(line)
122
-
123
- // Skip lines that only have arrow callbacks in method calls
124
- if (hasMethodCallWithArrowCallback && !/^(const|let|var|function|async|export)/.test(line.trim())) {
125
- continue
126
- }
127
-
128
- // Named function declaration: function funcName(
129
- const funcMatch = line.match(/function\s+(\w+)\s*\(/)
130
- if (funcMatch) {
131
- return funcMatch[1].toLowerCase()
132
- }
133
-
134
- // Arrow function with const/let/var: const funcName = () => | const funcName = async () =>
135
- // Must have => after the parameters to distinguish from const x = (expression)
136
- // Also handles TypeScript return type annotations: const funcName = (): string =>
137
- const arrowMatch = line.match(/(const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)(?:\s*:\s*\w+)?\s*=>/)
138
- if (arrowMatch) {
139
- return arrowMatch[2].toLowerCase()
140
- }
141
-
142
- // Method declaration: methodName() { or async methodName() {
143
- const methodMatch = line.match(/^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/)
144
- if (methodMatch) {
145
- return methodMatch[1].toLowerCase()
146
- }
147
-
148
- // Export function: export function funcName( or export const funcName = () =>
149
- const exportFuncMatch = line.match(/export\s+(?:async\s+)?function\s+(\w+)\s*\(/)
150
- if (exportFuncMatch) {
151
- return exportFuncMatch[1].toLowerCase()
152
- }
153
-
154
- // Also handles TypeScript return type annotations: export const funcName = (): string =>
155
- const exportConstMatch = line.match(/export\s+const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)(?:\s*:\s*\w+)?\s*=>/)
156
- if (exportConstMatch) {
157
- return exportConstMatch[1].toLowerCase()
158
- }
159
- }
160
-
161
- return null
34
+ return codeAnalysis.extractFunctionContext(new ParsedFile(content, ''), lineNumber)
162
35
  }
@@ -72,12 +72,13 @@ export function hasManualValidation(content: string): boolean {
72
72
  */
73
73
  export function hasSQLWhitelistValidation(content: string, lineNumber: number): boolean {
74
74
  const lines = content.split('\n')
75
- const contextStart = Math.max(0, lineNumber - 15)
75
+ const contextStart = Math.max(0, lineNumber - 20)
76
76
  const contextEnd = Math.min(lines.length, lineNumber + 5)
77
77
  const context = lines.slice(contextStart, contextEnd).join('\n')
78
78
 
79
79
  // Whitelist/allowlist validation patterns
80
80
  const whitelistPatterns = [
81
+ // Array-based whitelists
81
82
  /allowed\w*\s*=\s*\[/i, // allowedColumns = [...]
82
83
  /whitelist\w*\s*=\s*\[/i, // whitelistFields = [...]
83
84
  /valid\w*\s*=\s*\[/i, // validColumns = [...]
@@ -85,6 +86,20 @@ export function hasSQLWhitelistValidation(content: string, lineNumber: number):
85
86
  /\.includes\s*\([^)]*\)/i, // allowedColumns.includes(col)
86
87
  /\.every\s*\([^)]*\.includes/i, // columns.every(c => allowed.includes(c))
87
88
  /if\s*\(\s*!.*\.includes/i, // if (!allowed.includes(...))
89
+
90
+ // Object-based whitelists (Record<string, string>)
91
+ /\w+\s+in\s+\w*(?:sortable|allowed|valid|whitelist)\w*/i, // sorter in sortableFields
92
+ /\w+\s+in\s+\w+Fields/i, // key in someFields
93
+ /:\s*Record<string,\s*string>/i, // Type annotation: Record<string, string>
94
+ /const\s+\w+Fields\s*:\s*\{[^}]*\}\s*=/i, // const xyzFields: {...} = (inline type)
95
+ /const\s+\w+Fields\s*=\s*\{[^}]*\}/i, // const xyzFields = { ... }
96
+ /if\s*\([^)]*\s+in\s+\w+Fields\s*\)/i, // if (x in yFields)
97
+ /&&\s*\w+\s+in\s+\w+/i, // && sorter in sortableFields
98
+
99
+ // Enum-based validation (ASC/DESC, etc.)
100
+ /===?\s*['"](?:ASC|DESC)['"]/i, // === 'ASC' or === 'DESC'
101
+ /===?\s*\w+\.(?:Asc|Desc|ASC|DESC)/i, // === SortType.Asc
102
+ /toLowerCase\s*\(\s*\)\s*===?\s*\w+\.(?:asc|desc)/i, // .toLowerCase() === SortType.asc
88
103
  ]
89
104
 
90
105
  return whitelistPatterns.some(p => p.test(context))
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Vulnerability, VulnerabilitySeverity } from '../types'
8
+ import type { ParsedFile } from '../utils/parsed-file'
8
9
  import { isComment, isTestOrMockFile, isScannerOrFixtureFile } from '../utils/context-helpers'
9
10
 
10
11
  interface DataExposurePattern {
@@ -18,44 +19,17 @@ interface DataExposurePattern {
18
19
 
19
20
  const DATA_EXPOSURE_PATTERNS: DataExposurePattern[] = [
20
21
  // ============================================================================
21
- // LOG SINKS (generally lower severity - server-side only)
22
- // These are hygiene issues, not security vulnerabilities in most cases
22
+ // LOG SINKS - DISABLED
23
+ // Server logs are never exposed to users. After analysis of real codebases,
24
+ // 100% of console.error/warn findings were false positives.
25
+ // Logging is a standard debugging practice, not a security vulnerability.
23
26
  // ============================================================================
24
27
 
25
- // Logging sensitive variables - kept low/info
26
- // NOTE: Generic "query" or "search" logging is NOT flagged - only actual sensitive data
27
- {
28
- name: 'Logging user ID',
29
- pattern: /console\.(log|info|debug|warn|error)\s*\([^)]*\b(userId|user_id|user\.id)\b/gi,
30
- sink: 'log',
31
- severity: 'info',
32
- description: 'User ID logged to console. Common debugging practice - verify logs are secured.',
33
- suggestedFix: 'Use structured logging with appropriate access controls. Remove before production if not needed.',
34
- },
35
- {
36
- name: 'Logging error objects',
37
- pattern: /console\.(log|error|warn)\s*\(\s*(err|error|e)\s*\)/gi,
38
- sink: 'log',
39
- severity: 'info', // Downgraded from 'low' - this is standard practice
40
- description: 'Error object logged to console. Standard debugging practice, but may expose stack traces in logs.',
41
- suggestedFix: 'Consider logging only error.message in production. Use structured logging for better control.',
42
- },
43
- {
44
- name: 'Logging request body',
45
- pattern: /console\.(log|info|debug)\s*\([^)]*\b(req\.body|request\.body|body)\b/gi,
46
- sink: 'log',
47
- severity: 'low',
48
- description: 'Request body logged to console. May contain sensitive user input.',
49
- suggestedFix: 'Redact sensitive fields (passwords, tokens) before logging.',
50
- },
51
- {
52
- name: 'JSON.stringify error to log',
53
- pattern: /console\.(log|error|warn)\s*\([^)]*JSON\.stringify\s*\(\s*(err|error|e)\s*\)/gi,
54
- sink: 'log',
55
- severity: 'info',
56
- description: 'Error object serialized to JSON for logging. May include stack traces.',
57
- suggestedFix: 'Consider logging specific error properties instead of the full serialized object.',
58
- },
28
+ // NOTE: The following patterns have been REMOVED to eliminate false positives:
29
+ // - 'Logging user ID' - user IDs in logs are standard practice
30
+ // - 'Logging error objects' - console.error(err) is correct error handling
31
+ // - 'Logging request body' - server logs are not exposed to clients
32
+ // - 'JSON.stringify error to log' - serializing errors for logs is fine
59
33
 
60
34
  // ============================================================================
61
35
  // RESPONSE SINKS (higher severity - exposed to clients)
@@ -171,14 +145,15 @@ function isLowRiskLoggingFile(filePath: string): boolean {
171
145
  */
172
146
  export function detectDataExposure(
173
147
  content: string,
174
- filePath: string
148
+ filePath: string,
149
+ options?: { parsed?: ParsedFile }
175
150
  ): Vulnerability[] {
176
151
  const vulnerabilities: Vulnerability[] = []
177
152
 
178
153
  // Skip scanner/fixture files to avoid self-detection
179
154
  if (isScannerOrFixtureFile(filePath)) return vulnerabilities
180
155
 
181
- const lines = content.split('\n')
156
+ const lines = options?.parsed?.lines ?? content.split('\n')
182
157
  const isTestFile = isTestOrMockFile(filePath)
183
158
  const isLowRiskFile = isLowRiskLoggingFile(filePath)
184
159