@oculum/scanner 1.0.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 (281) hide show
  1. package/dist/formatters/cli-terminal.d.ts +27 -0
  2. package/dist/formatters/cli-terminal.d.ts.map +1 -0
  3. package/dist/formatters/cli-terminal.js +412 -0
  4. package/dist/formatters/cli-terminal.js.map +1 -0
  5. package/dist/formatters/github-comment.d.ts +41 -0
  6. package/dist/formatters/github-comment.d.ts.map +1 -0
  7. package/dist/formatters/github-comment.js +306 -0
  8. package/dist/formatters/github-comment.js.map +1 -0
  9. package/dist/formatters/grouping.d.ts +52 -0
  10. package/dist/formatters/grouping.d.ts.map +1 -0
  11. package/dist/formatters/grouping.js +152 -0
  12. package/dist/formatters/grouping.js.map +1 -0
  13. package/dist/formatters/index.d.ts +9 -0
  14. package/dist/formatters/index.d.ts.map +1 -0
  15. package/dist/formatters/index.js +35 -0
  16. package/dist/formatters/index.js.map +1 -0
  17. package/dist/formatters/vscode-diagnostic.d.ts +103 -0
  18. package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
  19. package/dist/formatters/vscode-diagnostic.js +151 -0
  20. package/dist/formatters/vscode-diagnostic.js.map +1 -0
  21. package/dist/index.d.ts +52 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +648 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/layer1/comments.d.ts +8 -0
  26. package/dist/layer1/comments.d.ts.map +1 -0
  27. package/dist/layer1/comments.js +203 -0
  28. package/dist/layer1/comments.js.map +1 -0
  29. package/dist/layer1/config-audit.d.ts +8 -0
  30. package/dist/layer1/config-audit.d.ts.map +1 -0
  31. package/dist/layer1/config-audit.js +252 -0
  32. package/dist/layer1/config-audit.js.map +1 -0
  33. package/dist/layer1/entropy.d.ts +8 -0
  34. package/dist/layer1/entropy.d.ts.map +1 -0
  35. package/dist/layer1/entropy.js +500 -0
  36. package/dist/layer1/entropy.js.map +1 -0
  37. package/dist/layer1/file-flags.d.ts +7 -0
  38. package/dist/layer1/file-flags.d.ts.map +1 -0
  39. package/dist/layer1/file-flags.js +112 -0
  40. package/dist/layer1/file-flags.js.map +1 -0
  41. package/dist/layer1/index.d.ts +36 -0
  42. package/dist/layer1/index.d.ts.map +1 -0
  43. package/dist/layer1/index.js +132 -0
  44. package/dist/layer1/index.js.map +1 -0
  45. package/dist/layer1/patterns.d.ts +8 -0
  46. package/dist/layer1/patterns.d.ts.map +1 -0
  47. package/dist/layer1/patterns.js +482 -0
  48. package/dist/layer1/patterns.js.map +1 -0
  49. package/dist/layer1/urls.d.ts +8 -0
  50. package/dist/layer1/urls.d.ts.map +1 -0
  51. package/dist/layer1/urls.js +296 -0
  52. package/dist/layer1/urls.js.map +1 -0
  53. package/dist/layer1/weak-crypto.d.ts +7 -0
  54. package/dist/layer1/weak-crypto.d.ts.map +1 -0
  55. package/dist/layer1/weak-crypto.js +291 -0
  56. package/dist/layer1/weak-crypto.js.map +1 -0
  57. package/dist/layer2/ai-agent-tools.d.ts +19 -0
  58. package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
  59. package/dist/layer2/ai-agent-tools.js +528 -0
  60. package/dist/layer2/ai-agent-tools.js.map +1 -0
  61. package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
  62. package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
  63. package/dist/layer2/ai-endpoint-protection.js +332 -0
  64. package/dist/layer2/ai-endpoint-protection.js.map +1 -0
  65. package/dist/layer2/ai-execution-sinks.d.ts +18 -0
  66. package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
  67. package/dist/layer2/ai-execution-sinks.js +496 -0
  68. package/dist/layer2/ai-execution-sinks.js.map +1 -0
  69. package/dist/layer2/ai-fingerprinting.d.ts +7 -0
  70. package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
  71. package/dist/layer2/ai-fingerprinting.js +654 -0
  72. package/dist/layer2/ai-fingerprinting.js.map +1 -0
  73. package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
  74. package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
  75. package/dist/layer2/ai-prompt-hygiene.js +356 -0
  76. package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
  77. package/dist/layer2/ai-rag-safety.d.ts +21 -0
  78. package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
  79. package/dist/layer2/ai-rag-safety.js +459 -0
  80. package/dist/layer2/ai-rag-safety.js.map +1 -0
  81. package/dist/layer2/ai-schema-validation.d.ts +25 -0
  82. package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
  83. package/dist/layer2/ai-schema-validation.js +375 -0
  84. package/dist/layer2/ai-schema-validation.js.map +1 -0
  85. package/dist/layer2/auth-antipatterns.d.ts +20 -0
  86. package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
  87. package/dist/layer2/auth-antipatterns.js +333 -0
  88. package/dist/layer2/auth-antipatterns.js.map +1 -0
  89. package/dist/layer2/byok-patterns.d.ts +12 -0
  90. package/dist/layer2/byok-patterns.d.ts.map +1 -0
  91. package/dist/layer2/byok-patterns.js +299 -0
  92. package/dist/layer2/byok-patterns.js.map +1 -0
  93. package/dist/layer2/dangerous-functions.d.ts +7 -0
  94. package/dist/layer2/dangerous-functions.d.ts.map +1 -0
  95. package/dist/layer2/dangerous-functions.js +1375 -0
  96. package/dist/layer2/dangerous-functions.js.map +1 -0
  97. package/dist/layer2/data-exposure.d.ts +16 -0
  98. package/dist/layer2/data-exposure.d.ts.map +1 -0
  99. package/dist/layer2/data-exposure.js +279 -0
  100. package/dist/layer2/data-exposure.js.map +1 -0
  101. package/dist/layer2/framework-checks.d.ts +7 -0
  102. package/dist/layer2/framework-checks.d.ts.map +1 -0
  103. package/dist/layer2/framework-checks.js +388 -0
  104. package/dist/layer2/framework-checks.js.map +1 -0
  105. package/dist/layer2/index.d.ts +58 -0
  106. package/dist/layer2/index.d.ts.map +1 -0
  107. package/dist/layer2/index.js +380 -0
  108. package/dist/layer2/index.js.map +1 -0
  109. package/dist/layer2/logic-gates.d.ts +7 -0
  110. package/dist/layer2/logic-gates.d.ts.map +1 -0
  111. package/dist/layer2/logic-gates.js +182 -0
  112. package/dist/layer2/logic-gates.js.map +1 -0
  113. package/dist/layer2/risky-imports.d.ts +7 -0
  114. package/dist/layer2/risky-imports.d.ts.map +1 -0
  115. package/dist/layer2/risky-imports.js +161 -0
  116. package/dist/layer2/risky-imports.js.map +1 -0
  117. package/dist/layer2/variables.d.ts +8 -0
  118. package/dist/layer2/variables.d.ts.map +1 -0
  119. package/dist/layer2/variables.js +152 -0
  120. package/dist/layer2/variables.js.map +1 -0
  121. package/dist/layer3/anthropic.d.ts +83 -0
  122. package/dist/layer3/anthropic.d.ts.map +1 -0
  123. package/dist/layer3/anthropic.js +1745 -0
  124. package/dist/layer3/anthropic.js.map +1 -0
  125. package/dist/layer3/index.d.ts +24 -0
  126. package/dist/layer3/index.d.ts.map +1 -0
  127. package/dist/layer3/index.js +119 -0
  128. package/dist/layer3/index.js.map +1 -0
  129. package/dist/layer3/openai.d.ts +25 -0
  130. package/dist/layer3/openai.d.ts.map +1 -0
  131. package/dist/layer3/openai.js +238 -0
  132. package/dist/layer3/openai.js.map +1 -0
  133. package/dist/layer3/package-check.d.ts +63 -0
  134. package/dist/layer3/package-check.d.ts.map +1 -0
  135. package/dist/layer3/package-check.js +508 -0
  136. package/dist/layer3/package-check.js.map +1 -0
  137. package/dist/modes/incremental.d.ts +66 -0
  138. package/dist/modes/incremental.d.ts.map +1 -0
  139. package/dist/modes/incremental.js +200 -0
  140. package/dist/modes/incremental.js.map +1 -0
  141. package/dist/tiers.d.ts +125 -0
  142. package/dist/tiers.d.ts.map +1 -0
  143. package/dist/tiers.js +234 -0
  144. package/dist/tiers.js.map +1 -0
  145. package/dist/types.d.ts +175 -0
  146. package/dist/types.d.ts.map +1 -0
  147. package/dist/types.js +50 -0
  148. package/dist/types.js.map +1 -0
  149. package/dist/utils/auth-helper-detector.d.ts +56 -0
  150. package/dist/utils/auth-helper-detector.d.ts.map +1 -0
  151. package/dist/utils/auth-helper-detector.js +360 -0
  152. package/dist/utils/auth-helper-detector.js.map +1 -0
  153. package/dist/utils/context-helpers.d.ts +96 -0
  154. package/dist/utils/context-helpers.d.ts.map +1 -0
  155. package/dist/utils/context-helpers.js +493 -0
  156. package/dist/utils/context-helpers.js.map +1 -0
  157. package/dist/utils/diff-detector.d.ts +53 -0
  158. package/dist/utils/diff-detector.d.ts.map +1 -0
  159. package/dist/utils/diff-detector.js +104 -0
  160. package/dist/utils/diff-detector.js.map +1 -0
  161. package/dist/utils/diff-parser.d.ts +80 -0
  162. package/dist/utils/diff-parser.d.ts.map +1 -0
  163. package/dist/utils/diff-parser.js +202 -0
  164. package/dist/utils/diff-parser.js.map +1 -0
  165. package/dist/utils/imported-auth-detector.d.ts +37 -0
  166. package/dist/utils/imported-auth-detector.d.ts.map +1 -0
  167. package/dist/utils/imported-auth-detector.js +251 -0
  168. package/dist/utils/imported-auth-detector.js.map +1 -0
  169. package/dist/utils/middleware-detector.d.ts +55 -0
  170. package/dist/utils/middleware-detector.d.ts.map +1 -0
  171. package/dist/utils/middleware-detector.js +260 -0
  172. package/dist/utils/middleware-detector.js.map +1 -0
  173. package/dist/utils/oauth-flow-detector.d.ts +41 -0
  174. package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
  175. package/dist/utils/oauth-flow-detector.js +202 -0
  176. package/dist/utils/oauth-flow-detector.js.map +1 -0
  177. package/dist/utils/path-exclusions.d.ts +55 -0
  178. package/dist/utils/path-exclusions.d.ts.map +1 -0
  179. package/dist/utils/path-exclusions.js +222 -0
  180. package/dist/utils/path-exclusions.js.map +1 -0
  181. package/dist/utils/project-context-builder.d.ts +119 -0
  182. package/dist/utils/project-context-builder.d.ts.map +1 -0
  183. package/dist/utils/project-context-builder.js +534 -0
  184. package/dist/utils/project-context-builder.js.map +1 -0
  185. package/dist/utils/registry-clients.d.ts +93 -0
  186. package/dist/utils/registry-clients.d.ts.map +1 -0
  187. package/dist/utils/registry-clients.js +273 -0
  188. package/dist/utils/registry-clients.js.map +1 -0
  189. package/dist/utils/trpc-analyzer.d.ts +78 -0
  190. package/dist/utils/trpc-analyzer.d.ts.map +1 -0
  191. package/dist/utils/trpc-analyzer.js +297 -0
  192. package/dist/utils/trpc-analyzer.js.map +1 -0
  193. package/package.json +45 -0
  194. package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
  195. package/src/__tests__/benchmark/fixtures/index.ts +68 -0
  196. package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
  197. package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
  198. package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
  199. package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
  200. package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
  201. package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
  202. package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
  203. package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
  204. package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
  205. package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
  206. package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
  207. package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
  208. package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
  209. package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
  210. package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
  211. package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
  212. package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
  213. package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
  214. package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
  215. package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
  216. package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
  217. package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
  218. package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
  219. package/src/__tests__/benchmark/index.ts +29 -0
  220. package/src/__tests__/benchmark/run-benchmark.ts +144 -0
  221. package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
  222. package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
  223. package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
  224. package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
  225. package/src/__tests__/benchmark/types.ts +144 -0
  226. package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
  227. package/src/__tests__/regression/known-false-positives.test.ts +467 -0
  228. package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
  229. package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
  230. package/src/__tests__/validation/analyze-results.ts +542 -0
  231. package/src/__tests__/validation/extract-for-triage.ts +146 -0
  232. package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
  233. package/src/__tests__/validation/run-validation.ts +364 -0
  234. package/src/__tests__/validation/triage-template.md +132 -0
  235. package/src/formatters/cli-terminal.ts +446 -0
  236. package/src/formatters/github-comment.ts +382 -0
  237. package/src/formatters/grouping.ts +190 -0
  238. package/src/formatters/index.ts +47 -0
  239. package/src/formatters/vscode-diagnostic.ts +243 -0
  240. package/src/index.ts +823 -0
  241. package/src/layer1/comments.ts +218 -0
  242. package/src/layer1/config-audit.ts +289 -0
  243. package/src/layer1/entropy.ts +583 -0
  244. package/src/layer1/file-flags.ts +127 -0
  245. package/src/layer1/index.ts +181 -0
  246. package/src/layer1/patterns.ts +516 -0
  247. package/src/layer1/urls.ts +334 -0
  248. package/src/layer1/weak-crypto.ts +328 -0
  249. package/src/layer2/ai-agent-tools.ts +601 -0
  250. package/src/layer2/ai-endpoint-protection.ts +387 -0
  251. package/src/layer2/ai-execution-sinks.ts +580 -0
  252. package/src/layer2/ai-fingerprinting.ts +758 -0
  253. package/src/layer2/ai-prompt-hygiene.ts +411 -0
  254. package/src/layer2/ai-rag-safety.ts +511 -0
  255. package/src/layer2/ai-schema-validation.ts +421 -0
  256. package/src/layer2/auth-antipatterns.ts +394 -0
  257. package/src/layer2/byok-patterns.ts +336 -0
  258. package/src/layer2/dangerous-functions.ts +1563 -0
  259. package/src/layer2/data-exposure.ts +315 -0
  260. package/src/layer2/framework-checks.ts +433 -0
  261. package/src/layer2/index.ts +473 -0
  262. package/src/layer2/logic-gates.ts +206 -0
  263. package/src/layer2/risky-imports.ts +186 -0
  264. package/src/layer2/variables.ts +166 -0
  265. package/src/layer3/anthropic.ts +2030 -0
  266. package/src/layer3/index.ts +130 -0
  267. package/src/layer3/package-check.ts +604 -0
  268. package/src/modes/incremental.ts +293 -0
  269. package/src/tiers.ts +318 -0
  270. package/src/types.ts +284 -0
  271. package/src/utils/auth-helper-detector.ts +443 -0
  272. package/src/utils/context-helpers.ts +535 -0
  273. package/src/utils/diff-detector.ts +135 -0
  274. package/src/utils/diff-parser.ts +272 -0
  275. package/src/utils/imported-auth-detector.ts +320 -0
  276. package/src/utils/middleware-detector.ts +333 -0
  277. package/src/utils/oauth-flow-detector.ts +246 -0
  278. package/src/utils/path-exclusions.ts +266 -0
  279. package/src/utils/project-context-builder.ts +707 -0
  280. package/src/utils/registry-clients.ts +351 -0
  281. package/src/utils/trpc-analyzer.ts +382 -0
@@ -0,0 +1,1563 @@
1
+ /**
2
+ * Layer 2: Dangerous Function Call Analysis
3
+ * Detects usage of dangerous functions that can lead to security vulnerabilities
4
+ */
5
+
6
+ import type { Vulnerability, VulnerabilitySeverity } from '../types'
7
+ import {
8
+ isComment,
9
+ isTestOrMockFile,
10
+ isScannerOrFixtureFile,
11
+ } from '../utils/context-helpers'
12
+
13
+ /**
14
+ * Check if exec() call is from child_process (dangerous) vs RegExp.exec (safe)
15
+ * Returns true if this is a child_process exec call that should be flagged
16
+ */
17
+ function isChildProcessExec(content: string, lineContent: string): boolean {
18
+ // Check for child_process import
19
+ const hasChildProcessImport =
20
+ /require\s*\(\s*['"]child_process['"]\s*\)/.test(content) ||
21
+ /from\s+['"]child_process['"]/.test(content) ||
22
+ /import\s+.*child_process/.test(content) ||
23
+ /require\s*\(\s*['"]node:child_process['"]\s*\)/.test(content) ||
24
+ /from\s+['"]node:child_process['"]/.test(content)
25
+
26
+ // If no child_process import, this is likely RegExp.exec or similar
27
+ if (!hasChildProcessImport) {
28
+ return false
29
+ }
30
+
31
+ // Check if this specific line is RegExp.exec pattern
32
+ // RegExp.exec is called as: regex.exec(string) or /pattern/.exec(string)
33
+ const isRegExpExec =
34
+ /\.\s*exec\s*\(/.test(lineContent) && // Method call on an object
35
+ !/\bexec\s*\(/.test(lineContent.replace(/\.\s*exec\s*\(/, '')) // Not a standalone exec()
36
+
37
+ // Also check for common RegExp patterns
38
+ const isRegExpPattern =
39
+ /\/[^/]+\/[gimsuy]*\.exec\s*\(/.test(lineContent) || // /pattern/.exec()
40
+ /new\s+RegExp\s*\([^)]+\)\.exec\s*\(/.test(lineContent) || // new RegExp().exec()
41
+ /regex\.exec\s*\(/i.test(lineContent) || // regex.exec()
42
+ /pattern\.exec\s*\(/i.test(lineContent) || // pattern.exec()
43
+ /match\.exec\s*\(/i.test(lineContent) || // match.exec()
44
+ /re\.exec\s*\(/i.test(lineContent) // re.exec()
45
+
46
+ if (isRegExpExec || isRegExpPattern) {
47
+ return false
48
+ }
49
+
50
+ // Check if exec is imported/destructured from child_process
51
+ const execImported =
52
+ /\{\s*[^}]*\bexec\b[^}]*\}\s*=\s*require\s*\(\s*['"]child_process['"]/.test(content) ||
53
+ /\{\s*[^}]*\bexec\b[^}]*\}\s*=\s*require\s*\(\s*['"]node:child_process['"]/.test(content) ||
54
+ /import\s+\{\s*[^}]*\bexec\b[^}]*\}\s+from\s+['"]child_process['"]/.test(content) ||
55
+ /import\s+\{\s*[^}]*\bexec\b[^}]*\}\s+from\s+['"]node:child_process['"]/.test(content)
56
+
57
+ // If exec is directly imported from child_process, standalone exec() is dangerous
58
+ if (execImported && /\bexec\s*\(/.test(lineContent)) {
59
+ return true
60
+ }
61
+
62
+ // Check for child_process.exec() pattern
63
+ if (/child_process\.exec\s*\(/.test(lineContent) ||
64
+ /cp\.exec\s*\(/.test(lineContent) ||
65
+ /childProcess\.exec\s*\(/.test(lineContent)) {
66
+ return true
67
+ }
68
+
69
+ // If we have child_process import but can't determine usage, be conservative
70
+ // Only flag if it looks like a standalone exec() call
71
+ return /\bexec\s*\(/.test(lineContent) && !/\.\s*exec\s*\(/.test(lineContent)
72
+ }
73
+
74
+ /**
75
+ * Check if schema validation is applied near a JSON.parse call
76
+ * Looks for zod, yup, joi, or similar validation patterns
77
+ */
78
+ function hasSchemaValidationNearby(content: string, lineNumber: number): boolean {
79
+ const lines = content.split('\n')
80
+ const start = Math.max(0, lineNumber - 5)
81
+ const end = Math.min(lines.length, lineNumber + 10)
82
+ const context = lines.slice(start, end).join('\n')
83
+
84
+ const schemaValidationPatterns = [
85
+ // Zod patterns
86
+ /z\.(object|string|number|array|boolean)\s*\(/i,
87
+ /\.parse\s*\(/i,
88
+ /\.safeParse\s*\(/i,
89
+ /schema\.parse/i,
90
+ /Schema\.parse/i,
91
+ // Yup patterns
92
+ /yup\.(object|string|number|array|boolean)\s*\(/i,
93
+ /\.validate\s*\(/i,
94
+ /\.validateSync\s*\(/i,
95
+ // Joi patterns
96
+ /Joi\.(object|string|number|array|boolean)\s*\(/i,
97
+ /\.validateAsync\s*\(/i,
98
+ // Valibot patterns
99
+ /v\.(object|string|number|array|boolean)\s*\(/i,
100
+ // AJV patterns
101
+ /ajv\.compile/i,
102
+ /validate\s*\(\s*schema/i,
103
+ // TypeBox patterns
104
+ /Type\.(Object|String|Number|Array|Boolean)\s*\(/i,
105
+ // Generic validation patterns
106
+ /validateSchema/i,
107
+ /schemaValidator/i,
108
+ /parseAndValidate/i,
109
+ ]
110
+
111
+ return schemaValidationPatterns.some(p => p.test(context))
112
+ }
113
+
114
+ /**
115
+ * Check if path traversal protection is in place
116
+ * Looks for common sanitization patterns that prevent directory traversal attacks
117
+ */
118
+ function hasPathTraversalProtection(context: string, lineContent: string): boolean {
119
+ const protectionPatterns = [
120
+ // Path normalization with base directory check
121
+ /path\.resolve\s*\([^)]+\).*\.startsWith\s*\(/i,
122
+ /\.startsWith\s*\([^)]*(?:baseDir|basePath|rootDir|uploadDir|allowedDir)/i,
123
+ // Explicit ".." rejection
124
+ /\.includes\s*\(\s*['"`]\.\.['"`]\s*\)/i,
125
+ /\.indexOf\s*\(\s*['"`]\.\.['"`]\s*\)/i,
126
+ /['"`]\.\.['"`].*(?:throw|reject|return|error)/i,
127
+ // Path sanitization libraries
128
+ /sanitizePath|sanitizeFilename|sanitize-filename/i,
129
+ /path-sanitizer|secure-path/i,
130
+ // Explicit path validation
131
+ /validatePath|isValidPath|checkPath|verifyPath/i,
132
+ /isPathAllowed|isAllowedPath|pathIsAllowed/i,
133
+ // Normalize and check pattern
134
+ /path\.normalize\s*\([^)]+\).*(?:startsWith|includes|indexOf)/i,
135
+ // Regex validation for safe characters only
136
+ /\/\^?\[a-zA-Z0-9_\-\.\\\/\]\+\$?\//, // Only alphanumeric, dash, underscore, dot
137
+ // Allowlist/whitelist patterns
138
+ /allowedExtensions|allowedTypes|whitelist/i,
139
+ /\.endsWith\s*\(\s*['"`]\.\w+['"`]\s*\)/i, // Extension check
140
+ // Path.basename to strip directory
141
+ /path\.basename\s*\(/i,
142
+ // Zod/validation for filename patterns
143
+ /z\.string\s*\(\s*\)\.regex\s*\(/i,
144
+ ]
145
+
146
+ return protectionPatterns.some(p => p.test(context) || p.test(lineContent))
147
+ }
148
+
149
+ /**
150
+ * Check if spawn/execFile/execSync is from child_process
151
+ */
152
+ function isChildProcessSpawn(content: string, lineContent: string): boolean {
153
+ // Check for child_process import
154
+ const hasChildProcessImport =
155
+ /require\s*\(\s*['"]child_process['"]\s*\)/.test(content) ||
156
+ /from\s+['"]child_process['"]/.test(content) ||
157
+ /require\s*\(\s*['"]node:child_process['"]\s*\)/.test(content) ||
158
+ /from\s+['"]node:child_process['"]/.test(content)
159
+
160
+ if (!hasChildProcessImport) {
161
+ return false
162
+ }
163
+
164
+ // These functions are always from child_process when that module is imported
165
+ return /\b(spawn|spawnSync|execSync|execFile|execFileSync)\s*\(/.test(lineContent)
166
+ }
167
+
168
+ /**
169
+ * Check if a line is inside a try-catch block
170
+ * Looks for enclosing try { ... } catch pattern
171
+ */
172
+ function isInsideTryCatch(content: string, lineNumber: number): boolean {
173
+ const lines = content.split('\n')
174
+
175
+ // Track brace depth and whether we're in a try block
176
+ let tryDepth = 0
177
+ let inTryBlock = false
178
+ let braceStack: Array<'try' | 'other'> = []
179
+
180
+ // Scan from start to the target line
181
+ for (let i = 0; i < lineNumber && i < lines.length; i++) {
182
+ const line = lines[i]
183
+
184
+ // Check for try keyword (not in a comment)
185
+ if (/\btry\s*\{/.test(line) && !isComment(line)) {
186
+ inTryBlock = true
187
+ tryDepth++
188
+ // Count opening braces on this line
189
+ const openBraces = (line.match(/\{/g) || []).length
190
+ const closeBraces = (line.match(/\}/g) || []).length
191
+ for (let j = 0; j < openBraces - closeBraces; j++) {
192
+ braceStack.push('try')
193
+ }
194
+ } else if (/\bcatch\s*\(/.test(line) && !isComment(line)) {
195
+ // Entering catch block - still protected
196
+ // Don't decrement tryDepth yet
197
+ } else if (/\bfinally\s*\{/.test(line) && !isComment(line)) {
198
+ // Entering finally block - still protected
199
+ } else {
200
+ // Track regular braces
201
+ const openBraces = (line.match(/\{/g) || []).length
202
+ const closeBraces = (line.match(/\}/g) || []).length
203
+
204
+ for (let j = 0; j < openBraces; j++) {
205
+ braceStack.push(inTryBlock && tryDepth > 0 ? 'try' : 'other')
206
+ }
207
+
208
+ for (let j = 0; j < closeBraces; j++) {
209
+ const popped = braceStack.pop()
210
+ if (popped === 'try') {
211
+ tryDepth--
212
+ if (tryDepth === 0) {
213
+ inTryBlock = false
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ return tryDepth > 0
221
+ }
222
+
223
+ /**
224
+ * Simpler heuristic: check if there's a try-catch in the same function scope
225
+ * Looks for try { before the line and } catch after, within reasonable bounds
226
+ */
227
+ function hasTryCatchNearby(content: string, lineNumber: number, windowSize: number = 20): boolean {
228
+ const lines = content.split('\n')
229
+ const startLine = Math.max(0, lineNumber - windowSize)
230
+ const endLine = Math.min(lines.length, lineNumber + windowSize)
231
+
232
+ // Look backward for 'try {'
233
+ let foundTry = false
234
+ for (let i = lineNumber - 1; i >= startLine; i--) {
235
+ const line = lines[i]
236
+ if (/\btry\s*\{/.test(line) && !isComment(line)) {
237
+ foundTry = true
238
+ break
239
+ }
240
+ // Stop if we hit a function boundary
241
+ if (/\b(function|async function|=>|class)\b/.test(line) && /\{/.test(line)) {
242
+ break
243
+ }
244
+ }
245
+
246
+ if (!foundTry) return false
247
+
248
+ // Look forward for '} catch'
249
+ for (let i = lineNumber; i < endLine; i++) {
250
+ const line = lines[i]
251
+ if (/\}\s*catch\s*\(/.test(line) && !isComment(line)) {
252
+ return true
253
+ }
254
+ // Stop if we hit another function boundary
255
+ if (i > lineNumber && /\b(function|async function|class)\b/.test(line) && /\{/.test(line)) {
256
+ break
257
+ }
258
+ }
259
+
260
+ return false
261
+ }
262
+
263
+ /**
264
+ * JSON.parse source classification
265
+ * Determines if the input is user-controlled or internal data
266
+ */
267
+ type JSONParseSource = 'user_input' | 'local_storage' | 'database' | 'config' | 'migration' | 'internal' | 'test_fixture' | 'ui_state' | 'unknown'
268
+
269
+ /**
270
+ * Check if file path indicates a low-risk context for JSON.parse
271
+ */
272
+ function isLowRiskJSONParseFile(filePath: string): JSONParseSource | null {
273
+ // Test/mock files - skip or info only
274
+ if (isTestOrMockFile(filePath)) {
275
+ return 'test_fixture'
276
+ }
277
+
278
+ // Settings/preferences components - internal UI state
279
+ if (/\/(components|pages)\/(settings|preferences|config)/i.test(filePath)) {
280
+ return 'ui_state'
281
+ }
282
+
283
+ // Provider/context files - typically storing state in localStorage
284
+ if (/Provider\.(ts|tsx|js|jsx)$/i.test(filePath)) {
285
+ return 'ui_state'
286
+ }
287
+
288
+ // Modal/Dialog components - typically internal state
289
+ if (/(Modal|Dialog|Settings|Preferences)\.(ts|tsx|js|jsx)$/i.test(filePath)) {
290
+ return 'ui_state'
291
+ }
292
+
293
+ // __mocks__ directory
294
+ if (/__mocks__/i.test(filePath)) {
295
+ return 'test_fixture'
296
+ }
297
+
298
+ // fixtures directory
299
+ if (/\/(fixtures?|stubs?|mocks?)\//i.test(filePath)) {
300
+ return 'test_fixture'
301
+ }
302
+
303
+ // scripts/tools directories (internal tooling)
304
+ if (/\/(scripts?|tools?|cli)\//i.test(filePath)) {
305
+ return 'internal'
306
+ }
307
+
308
+ // Migration files
309
+ if (/migration/i.test(filePath)) {
310
+ return 'migration'
311
+ }
312
+
313
+ // Config files
314
+ if (/\/(config|settings|constants)\.(ts|js)/i.test(filePath)) {
315
+ return 'config'
316
+ }
317
+
318
+ return null
319
+ }
320
+
321
+ /**
322
+ * Check if JSON.parse is parsing a trusted SDK response
323
+ * These are well-defined responses from known APIs and are safe to parse
324
+ */
325
+ function isTrustedSDKResponse(lineContent: string, content: string): boolean {
326
+ const trustedPatterns = [
327
+ // OpenAI SDK responses
328
+ /JSON\.parse\s*\(\s*(?:response|completion|result|message)\.(?:content|text|data)/i,
329
+ /JSON\.parse\s*\(\s*(?:openai|anthropic|client)\./i,
330
+ // Fetch response.json() result (already parsed by fetch)
331
+ /JSON\.parse\s*\(\s*await\s+.*\.json\s*\(\s*\)\s*\)/i,
332
+ // SDK method results
333
+ /JSON\.parse\s*\(\s*(?:result|response)\.(?:choices|content|data|body)\[/i,
334
+ // AI SDK streaming results
335
+ /JSON\.parse\s*\(\s*(?:chunk|delta|part)\.(?:content|text)/i,
336
+ ]
337
+
338
+ if (trustedPatterns.some(p => p.test(lineContent))) {
339
+ return true
340
+ }
341
+
342
+ // Check surrounding context for SDK usage
343
+ const sdkContextPatterns = [
344
+ /openai\..*\.create/i,
345
+ /anthropic\..*\.create/i,
346
+ /\.chat\.completions/i,
347
+ /\.messages\.create/i,
348
+ ]
349
+
350
+ return sdkContextPatterns.some(p => p.test(content))
351
+ }
352
+
353
+ function classifyJSONParseSource(lineContent: string, filePath: string): JSONParseSource {
354
+ // First check file path for low-risk contexts
355
+ const fileBasedSource = isLowRiskJSONParseFile(filePath)
356
+ if (fileBasedSource) {
357
+ return fileBasedSource
358
+ }
359
+
360
+ // User input - potentially dangerous
361
+ const userInputPatterns = [
362
+ /JSON\.parse\s*\(\s*(req|request)\.(body|query|params)/i,
363
+ /JSON\.parse\s*\(\s*event\.(body|queryStringParameters)/i, // AWS Lambda
364
+ /JSON\.parse\s*\(\s*ctx\.(request|body|query)/i, // Koa
365
+ /JSON\.parse\s*\(\s*(input|userInput|rawInput|payload)/i,
366
+ /JSON\.parse\s*\(\s*body\b/i, // Generic 'body' often means request body
367
+ ]
368
+ if (userInputPatterns.some(p => p.test(lineContent))) {
369
+ return 'user_input'
370
+ }
371
+
372
+ // localStorage/sessionStorage - client-side storage
373
+ const storagePatterns = [
374
+ /JSON\.parse\s*\(\s*localStorage\.getItem/i,
375
+ /JSON\.parse\s*\(\s*sessionStorage\.getItem/i,
376
+ /JSON\.parse\s*\(\s*window\.localStorage/i,
377
+ /JSON\.parse\s*\(\s*storage\.get/i,
378
+ /JSON\.parse\s*\(\s*saved\b/i, // Common pattern: const saved = localStorage.getItem(...); JSON.parse(saved)
379
+ /JSON\.parse\s*\(\s*stored\b/i,
380
+ ]
381
+ if (storagePatterns.some(p => p.test(lineContent))) {
382
+ return 'local_storage'
383
+ }
384
+
385
+ // Database results - internal data
386
+ const databasePatterns = [
387
+ /JSON\.parse\s*\(\s*(row|result|record|doc|document)\./i,
388
+ /JSON\.parse\s*\(\s*\w+\.(data|json|metadata|embedding)\)/i,
389
+ /JSON\.parse\s*\(\s*\w+\[['"]?\w+['"]?\]\.(data|json|embedding)/i,
390
+ /JSON\.parse\s*\(\s*item\.\w+\)/i, // ORM iteration: items.map(item => JSON.parse(item.field))
391
+ /JSON\.parse\s*\(\s*\w+\.content\)/i, // Parsing content field from DB
392
+ ]
393
+ if (databasePatterns.some(p => p.test(lineContent))) {
394
+ return 'database'
395
+ }
396
+
397
+ // Editor state, internal caches, UI state
398
+ const internalPatterns = [
399
+ /JSON\.parse\s*\(\s*(state|cache|stored|saved|cached)/i,
400
+ /JSON\.parse\s*\(\s*this\.(state|cache|data)/i,
401
+ /JSON\.parse\s*\(\s*\w+State\)/i,
402
+ /JSON\.parse\s*\(\s*editorState/i,
403
+ /JSON\.parse\s*\(\s*parsed\b/i, // JSON.parse(parsed) - likely already validated
404
+ /JSON\.parse\s*\(\s*settings\b/i, // Settings data
405
+ /JSON\.parse\s*\(\s*preferences\b/i,
406
+ ]
407
+ if (internalPatterns.some(p => p.test(lineContent))) {
408
+ return 'internal'
409
+ }
410
+
411
+ // Node content in editor apps (e.g., noda-os nodes have JSON content)
412
+ if (/JSON\.parse\s*\(\s*(node|note|document|entry)\.(content|body|data)\)/i.test(lineContent)) {
413
+ return 'database'
414
+ }
415
+
416
+ return 'unknown'
417
+ }
418
+
419
+ interface DangerousFunctionPattern {
420
+ name: string
421
+ pattern: RegExp
422
+ severity: VulnerabilitySeverity
423
+ description: string
424
+ suggestedFix: string
425
+ languages?: string[] // Optional: restrict to specific languages
426
+ }
427
+
428
+ const DANGEROUS_FUNCTIONS: DangerousFunctionPattern[] = [
429
+ // Code execution
430
+ {
431
+ name: 'eval() usage',
432
+ pattern: /\beval\s*\(/gi,
433
+ severity: 'critical',
434
+ description: 'eval() executes arbitrary code and is a major security risk',
435
+ suggestedFix: 'Use JSON.parse() for JSON data, or refactor to avoid dynamic code execution',
436
+ },
437
+ {
438
+ name: 'Function constructor',
439
+ pattern: /new\s+Function\s*\(/gi,
440
+ severity: 'critical',
441
+ description: 'Function constructor can execute arbitrary code like eval()',
442
+ suggestedFix: 'Refactor to use static functions or safe alternatives',
443
+ },
444
+ {
445
+ name: 'setTimeout/setInterval with string',
446
+ pattern: /set(Timeout|Interval)\s*\(\s*['"`]/gi,
447
+ severity: 'high',
448
+ description: 'setTimeout/setInterval with string argument acts like eval()',
449
+ suggestedFix: 'Pass a function reference instead of a string',
450
+ },
451
+
452
+ // Command injection
453
+ {
454
+ name: 'child_process exec',
455
+ pattern: /\b(exec|execSync|spawn|spawnSync|execFile)\s*\(/gi,
456
+ severity: 'high',
457
+ description: 'Shell command execution can lead to command injection',
458
+ suggestedFix: 'Validate and sanitize all inputs, prefer execFile over exec',
459
+ },
460
+ {
461
+ name: 'os.system/subprocess (Python)',
462
+ pattern: /\b(os\.system|subprocess\.(call|run|Popen|check_output))\s*\(/gi,
463
+ severity: 'high',
464
+ description: 'Shell command execution can lead to command injection',
465
+ suggestedFix: 'Use subprocess with shell=False and pass arguments as a list',
466
+ languages: ['py'],
467
+ },
468
+
469
+ // SQL injection risks
470
+ {
471
+ name: 'Raw SQL query construction',
472
+ pattern: /\.(query|execute|raw)\s*\(\s*[`'"].*\$\{|\.query\s*\(\s*['"].*\+/gi,
473
+ severity: 'critical',
474
+ description: 'String concatenation in SQL queries can lead to SQL injection',
475
+ suggestedFix: 'Use parameterized queries or prepared statements',
476
+ },
477
+ {
478
+ name: 'SQL template literal',
479
+ pattern: /`SELECT.*FROM.*WHERE.*\$\{|`INSERT.*INTO.*VALUES.*\$\{|`UPDATE.*SET.*\$\{|`DELETE.*FROM.*WHERE.*\$\{/gi,
480
+ severity: 'critical',
481
+ description: 'Template literals in SQL queries can lead to SQL injection',
482
+ suggestedFix: 'Use parameterized queries with placeholders (?, $1, etc.)',
483
+ },
484
+
485
+ // XSS risks
486
+ {
487
+ name: 'innerHTML assignment',
488
+ pattern: /\.innerHTML\s*=|\.outerHTML\s*=/gi,
489
+ severity: 'high',
490
+ description: 'Direct innerHTML assignment can lead to XSS vulnerabilities',
491
+ suggestedFix: 'Use textContent for text, or sanitize HTML with DOMPurify',
492
+ },
493
+ {
494
+ name: 'document.write',
495
+ pattern: /document\.write\s*\(/gi,
496
+ severity: 'high',
497
+ description: 'document.write can introduce XSS vulnerabilities',
498
+ suggestedFix: 'Use DOM manipulation methods instead',
499
+ },
500
+ {
501
+ name: 'dangerouslySetInnerHTML',
502
+ pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/gi,
503
+ severity: 'high',
504
+ description: 'dangerouslySetInnerHTML can lead to XSS if content is not sanitized',
505
+ suggestedFix: 'Sanitize HTML content with DOMPurify before rendering',
506
+ },
507
+
508
+ // Deserialization
509
+ {
510
+ name: 'Unsafe deserialization',
511
+ pattern: /\b(pickle\.loads?|yaml\.load\s*\((?!.*Loader)|unserialize|Marshal\.load)\s*\(/gi,
512
+ severity: 'critical',
513
+ description: 'Unsafe deserialization can lead to remote code execution',
514
+ suggestedFix: 'Use safe loaders (yaml.safe_load) or validate input before deserializing',
515
+ },
516
+ // Note: JSON.parse is handled specially with source-aware severity - see below
517
+ // Note: request.json() is NOT a dangerous function - see schema validation rules
518
+
519
+ // File system risks
520
+ {
521
+ name: 'Dynamic file path',
522
+ pattern: /\b(readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream)\s*\(\s*[^'"]/gi,
523
+ severity: 'medium',
524
+ description: 'Dynamic file paths can lead to path traversal attacks',
525
+ suggestedFix: 'Validate and sanitize file paths, use path.resolve with a base directory',
526
+ },
527
+ {
528
+ name: 'Path traversal risk',
529
+ pattern: /path\.(join|resolve)\s*\([^)]*req\.(params|query|body)/gi,
530
+ severity: 'high',
531
+ description: 'User input in file paths can lead to path traversal attacks',
532
+ suggestedFix: 'Validate paths and ensure they stay within allowed directories',
533
+ },
534
+
535
+ // Crypto weaknesses
536
+ {
537
+ name: 'Math.random for security',
538
+ pattern: /Math\.random\s*\(\s*\)/gi,
539
+ severity: 'medium',
540
+ description: 'Math.random() is not cryptographically secure',
541
+ suggestedFix: 'Use crypto.randomBytes() or crypto.getRandomValues() for security-sensitive operations',
542
+ },
543
+
544
+ // Regex DoS
545
+ {
546
+ name: 'Potentially unsafe regex',
547
+ pattern: /new\s+RegExp\s*\(\s*[^'"]/gi,
548
+ severity: 'medium',
549
+ description: 'Dynamic regex construction can lead to ReDoS attacks',
550
+ suggestedFix: 'Validate regex patterns and consider using safe-regex library',
551
+ },
552
+
553
+ // Prototype pollution
554
+ {
555
+ name: 'Object.assign with user input',
556
+ pattern: /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(req\.|request\.|body|params|query)/gi,
557
+ severity: 'high',
558
+ description: 'Object.assign with user input can lead to prototype pollution',
559
+ suggestedFix: 'Validate and sanitize input, or use a safe merge function',
560
+ },
561
+ {
562
+ name: 'Spread operator with user input',
563
+ pattern: /\{\s*\.\.\.req\.(body|params|query)|\.\.\.request\.(body|params|query)/gi,
564
+ severity: 'medium',
565
+ description: 'Spreading user input can lead to mass assignment vulnerabilities',
566
+ suggestedFix: 'Explicitly pick allowed properties instead of spreading all input',
567
+ },
568
+ ]
569
+
570
+ // Check if file matches language filter
571
+ function matchesLanguage(filePath: string, languages?: string[]): boolean {
572
+ if (!languages || languages.length === 0) return true
573
+
574
+ const ext = filePath.split('.').pop()?.toLowerCase() || ''
575
+ return languages.some(lang => {
576
+ if (lang === 'py') return ext === 'py'
577
+ if (lang === 'js') return ['js', 'jsx', 'mjs', 'cjs'].includes(ext)
578
+ if (lang === 'ts') return ['ts', 'tsx'].includes(ext)
579
+ return ext === lang
580
+ })
581
+ }
582
+
583
+ // Check if innerHTML/dangerouslySetInnerHTML uses static content only
584
+ function isStaticHTMLContent(lineContent: string, content: string, lineNumber: number): boolean {
585
+ const lines = content.split('\n')
586
+
587
+ // Get surrounding context (5 lines before and after)
588
+ const contextStart = Math.max(0, lineNumber - 6)
589
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
590
+ const context = lines.slice(contextStart, contextEnd).join('\n')
591
+
592
+ // Static HTML indicators - string literals only
593
+ const staticIndicators = [
594
+ /innerHTML\s*=\s*['"`][^'"`]*['"`]/, // innerHTML = "static string"
595
+ /innerHTML\s*=\s*`[^$]*`/, // innerHTML = `static template without ${}`
596
+ /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html:\s*['"`]/, // React static string
597
+ ]
598
+
599
+ // Dynamic content indicators (red flags)
600
+ const dynamicIndicators = [
601
+ /\$\{[^}]+\}/, // Template interpolation ${...}
602
+ /innerHTML\s*=.*\+/, // String concatenation with +
603
+ /innerHTML\s*\+=\s*/, // Append operation
604
+ /\breq\.|\.params|\.query|\.body/, // User input (req.params, req.query, req.body)
605
+ /\bprops\./, // Component props
606
+ /\bstate\./, // Component state
607
+ /\.value\b/, // Input value
608
+ /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html:\s*[^'"`]/, // React dynamic
609
+ ]
610
+
611
+ const isStatic = staticIndicators.some(p => p.test(lineContent))
612
+ const isDynamic = dynamicIndicators.some(p => p.test(context))
613
+
614
+ return isStatic && !isDynamic
615
+ }
616
+
617
+ /**
618
+ * Check if eval/exec/Function has only static literal inputs (no user data)
619
+ * Static inputs like eval('({ mode: "production" })') are low risk
620
+ */
621
+ function hasOnlyStaticInputs(lineContent: string, content: string, lineNumber: number): boolean {
622
+ const lines = content.split('\n')
623
+
624
+ // Check if the argument to eval/exec/Function is a string literal only
625
+ const staticPatterns = [
626
+ /eval\s*\(\s*['"`][^'"`$]*['"`]\s*\)/, // eval('static string')
627
+ /eval\s*\(\s*`[^$`]*`\s*\)/, // eval(`static template without ${}`)
628
+ /new\s+Function\s*\(\s*['"`][^'"`$]*['"`]\s*\)/, // new Function('static')
629
+ /execSync\s*\(\s*['"`][^'"`$]*['"`]\s*\)/, // execSync('static command')
630
+ /exec\s*\(\s*['"`][^'"`$]*['"`]/, // exec('static command'
631
+ ]
632
+
633
+ if (staticPatterns.some(p => p.test(lineContent))) {
634
+ return true
635
+ }
636
+
637
+ // Check surrounding context for user input flowing in
638
+ const userInputIndicators = [
639
+ /\$\{/, // Template interpolation
640
+ /\+\s*\w+/, // String concatenation with variable
641
+ /req\.|request\.|body\.|params\.|query\./i, // Request data
642
+ /user[Ii]nput|userCode|userCommand/, // User input variables
643
+ /args\[|argv\[/, // Command line args
644
+ ]
645
+
646
+ const contextStart = Math.max(0, lineNumber - 3)
647
+ const contextEnd = Math.min(lines.length, lineNumber + 1)
648
+ const context = lines.slice(contextStart, contextEnd).join('\n')
649
+
650
+ // If no user input indicators found, likely static
651
+ return !userInputIndicators.some(p => p.test(context))
652
+ }
653
+
654
+ /**
655
+ * Check if SQL query uses whitelist validation pattern
656
+ * e.g., columns validated against allowedColumns array before use
657
+ */
658
+ function hasSQLWhitelistValidation(content: string, lineNumber: number): boolean {
659
+ const lines = content.split('\n')
660
+ const contextStart = Math.max(0, lineNumber - 15)
661
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
662
+ const context = lines.slice(contextStart, contextEnd).join('\n')
663
+
664
+ // Whitelist/allowlist validation patterns
665
+ const whitelistPatterns = [
666
+ /allowed\w*\s*=\s*\[/i, // allowedColumns = [...]
667
+ /whitelist\w*\s*=\s*\[/i, // whitelistFields = [...]
668
+ /valid\w*\s*=\s*\[/i, // validColumns = [...]
669
+ /\.filter\s*\([^)]*\.includes\s*\(/i, // .filter(c => allowed.includes(c))
670
+ /\.includes\s*\([^)]*\)/i, // allowedColumns.includes(col)
671
+ /\.every\s*\([^)]*\.includes/i, // columns.every(c => allowed.includes(c))
672
+ /if\s*\(\s*!.*\.includes/i, // if (!allowed.includes(...))
673
+ ]
674
+
675
+ return whitelistPatterns.some(p => p.test(context))
676
+ }
677
+
678
+ /**
679
+ * Check if dangerouslySetInnerHTML is used with DOMPurify sanitization
680
+ */
681
+ function hasDOMPurifySanitization(lineContent: string, content: string, lineNumber: number): boolean {
682
+ const lines = content.split('\n')
683
+ const contextStart = Math.max(0, lineNumber - 10)
684
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
685
+ const context = lines.slice(contextStart, contextEnd).join('\n')
686
+
687
+ // DOMPurify sanitization patterns
688
+ const sanitizationPatterns = [
689
+ /DOMPurify\.sanitize/i,
690
+ /sanitize\s*\(/i,
691
+ /purify\s*\(/i,
692
+ /xss\s*\(/i,
693
+ /clean\s*\(/i,
694
+ /sanitizeHtml/i,
695
+ /escapeHtml/i,
696
+ /sanitized/i,
697
+ /purified/i,
698
+ ]
699
+
700
+ return sanitizationPatterns.some(p => p.test(context))
701
+ }
702
+
703
+ /**
704
+ * Check if data flows to an LLM prompt rather than a DOM sink
705
+ * LLM prompts are NOT XSS - they're prompt injection (different risk profile)
706
+ */
707
+ function isLLMPromptContext(lineContent: string, content: string, filePath: string): boolean {
708
+ // File path indicators of AI/LLM code
709
+ const aiFilePatterns = [
710
+ /\/(ai|llm|chat|openai|anthropic|gpt|claude)\//i,
711
+ /\/(assistants?|agents?|prompts?)\//i,
712
+ /(chat|ai|llm|prompt|assistant).*\.(ts|js|tsx|jsx)$/i,
713
+ ]
714
+
715
+ if (aiFilePatterns.some(p => p.test(filePath))) {
716
+ return true
717
+ }
718
+
719
+ // Content patterns suggesting LLM API usage
720
+ const llmApiPatterns = [
721
+ /\.create\s*\(\s*\{[^}]*messages\s*:/i, // OpenAI/Anthropic SDK
722
+ /openai|anthropic|claude|gpt-4|gpt-3/i, // AI service mentions
723
+ /\bprompt\s*[=:+]/i, // prompt assignment
724
+ /\bsystemPrompt|userPrompt|assistantPrompt/i, // Prompt variables
725
+ /completion|chat\.create|messages\.create/i, // API calls
726
+ /\bmessages\s*:\s*\[/i, // Messages array
727
+ /role:\s*['"`](user|assistant|system)['"`]/i, // Message roles
728
+ ]
729
+
730
+ // Check the line and surrounding context
731
+ const lines = content.split('\n')
732
+ const lineIndex = lines.findIndex(l => l === lineContent || l.includes(lineContent.trim()))
733
+ const startLine = Math.max(0, lineIndex - 10)
734
+ const endLine = Math.min(lines.length, lineIndex + 10)
735
+ const context = lines.slice(startLine, endLine).join('\n')
736
+
737
+ return llmApiPatterns.some(p => p.test(lineContent) || p.test(context))
738
+ }
739
+
740
+ /**
741
+ * Check if this is a static bootstrap script (e.g., localStorage theme reader)
742
+ * These are very low risk even with dangerouslySetInnerHTML
743
+ */
744
+ function isStaticBootstrapScript(_lineContent: string, content: string, lineNumber: number): boolean {
745
+ const lines = content.split('\n')
746
+ const contextStart = Math.max(0, lineNumber - 10)
747
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
748
+ const context = lines.slice(contextStart, contextEnd).join('\n')
749
+
750
+ // Bootstrap script indicators (reading from localStorage, setting attributes)
751
+ const bootstrapPatterns = [
752
+ /localStorage\.getItem/i,
753
+ /document\.documentElement\.setAttribute/i,
754
+ /data-(theme|font|mode)/i,
755
+ /classList\.(add|remove|toggle)/i,
756
+ /\.dataset\./i,
757
+ ]
758
+
759
+ // Dangerous patterns that disqualify as safe bootstrap
760
+ const dangerousPatterns = [
761
+ /\$\{.*\}/, // Template interpolation
762
+ /\+\s*[a-zA-Z]/, // String concatenation with variable
763
+ /innerHTML\s*=\s*[a-zA-Z]/, // innerHTML set to variable directly
764
+ /fetch\s*\(/, // Network requests
765
+ /\.(query|params|body)/, // User input
766
+ /location\.(search|hash)/, // URL parameters
767
+ /document\.cookie/, // Cookie access
768
+ ]
769
+
770
+ const hasBootstrapPatterns = bootstrapPatterns.some(p => p.test(context))
771
+ const hasDangerousPatterns = dangerousPatterns.some(p => p.test(context))
772
+
773
+ return hasBootstrapPatterns && !hasDangerousPatterns
774
+ }
775
+
776
+ /**
777
+ * Check if Math.random() is used for cosmetic/UI purposes (not security)
778
+ * Cosmetic uses: CSS values, animations, UI variations, demo data
779
+ * Security uses: tokens, IDs, cryptographic operations, session management
780
+ */
781
+ function isCosmeticMathRandom(lineContent: string, content: string, lineNumber: number): boolean {
782
+ const lines = content.split('\n')
783
+
784
+ // Check the line itself for cosmetic indicators
785
+ const cosmeticLinePatterns = [
786
+ // CSS/style values
787
+ /['"`]\s*\$\{.*Math\.random.*\}\s*%['"`]/, // `${Math.random() * 40 + 50}%`
788
+ /Math\.random.*\s*\+\s*['"`]%['"`]/, // Math.random() * 40 + '%'
789
+ /Math\.random.*\)\s*\*\s*\d+\s*\+\s*\d+\s*\}\s*%/, // }) * 40 + 50}%
790
+ /return\s+`.*Math\.random.*%`/, // return `${...}%`
791
+ /width:\s*['"`].*Math\.random/i, // width: `${Math.random()...}%`
792
+ /height:\s*['"`].*Math\.random/i, // height: `${Math.random()...}%`
793
+ /opacity:\s*['"`]?.*Math\.random/i, // opacity: Math.random()
794
+ /transform:\s*['"`]?.*Math\.random/i, // transform: translate(...)
795
+ /rotate\(.*Math\.random/i, // rotate(Math.random() * 360)
796
+ /translate\(.*Math\.random/i, // translate(Math.random() * 100)
797
+ /scale\(.*Math\.random/i, // scale(Math.random() * 2)
798
+ // Color/animation values
799
+ /rgba?\(.*Math\.random/i, // rgb(Math.random() * 255, ...)
800
+ /hsl\(.*Math\.random/i, // hsl(Math.random() * 360, ...)
801
+ /Math\.random.*\*\s*360/, // Math.random() * 360 (degrees/hue)
802
+ /Math\.random.*\*\s*255/, // Math.random() * 255 (RGB values)
803
+ // Array/list randomization for UI
804
+ /Math\.floor\(Math\.random.*\.length\)/, // Math.floor(Math.random() * array.length)
805
+ /\[Math\.floor\(Math\.random/, // array[Math.floor(Math.random()...)]
806
+ // Demo/placeholder data
807
+ /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bpx\b/i, // Math.random() * 100 + 50 + 'px'
808
+ /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bms\b/i, // Math.random() * 1000 + 500 + 'ms'
809
+ /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i, // Math.random() * 5 + 2 + 's'
810
+ // UI identifier generation (short strings for element IDs, keys, etc.)
811
+ /Math\.random\(\)\.toString\(36\)\.substring\(/, // .toString(36).substring(2, 9) - short UI IDs
812
+ /Math\.random\(\)\.toString\(36\)\.substr\(/, // .substr() variant
813
+ /Math\.random\(\)\.toString\(36\)\.slice\(/, // .slice() variant
814
+ /Math\.random\(\)\.toString\(16\)\.substring\(/, // .toString(16).substring() - hex UI IDs
815
+ /Math\.random\(\)\.toString\(16\)\.slice\(/, // hex slice variant
816
+ ]
817
+
818
+ if (cosmeticLinePatterns.some(p => p.test(lineContent))) {
819
+ return true
820
+ }
821
+
822
+ // Check surrounding context (5 lines before and after)
823
+ const contextStart = Math.max(0, lineNumber - 5)
824
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
825
+ const context = lines.slice(contextStart, contextEnd).join('\n')
826
+
827
+ // Context indicators of cosmetic use
828
+ const cosmeticContextPatterns = [
829
+ // UI component files
830
+ /\/(components?|ui|widgets?|animations?|contexts?)\//i,
831
+ // Style-related variables/functions
832
+ /\b(style|styles|css|className|animation|transition)/i,
833
+ /\b(width|height|opacity|color|transform|rotate|scale|translate)/i,
834
+ // Demo/example data
835
+ /\b(demo|example|placeholder|mock|fake|sample|test)Data/i,
836
+ /\b(random|shuffle|pick|choose).*\b(color|item|element|option)/i,
837
+ // Animation/timing
838
+ /setTimeout.*Math\.random/i,
839
+ /setInterval.*Math\.random/i,
840
+ /delay.*Math\.random/i,
841
+ /duration.*Math\.random/i,
842
+ // UI state variations
843
+ /\b(variant|theme|layout|position).*Math\.random/i,
844
+ // UI identifier variable names (toast, notification, element, component IDs)
845
+ /\b(toast|notification|element|component|widget|modal|dialog|popup).*id\b/i,
846
+ /\bid\s*=.*Math\.random/i,
847
+ /\bkey\s*=.*Math\.random/i, // React keys
848
+ /\btempId|temporaryId|uniqueId\b/i,
849
+ ]
850
+
851
+ if (cosmeticContextPatterns.some(p => p.test(context))) {
852
+ return true
853
+ }
854
+
855
+ // Security-sensitive patterns that override cosmetic detection
856
+ const securityPatterns = [
857
+ /\b(token|secret|key|password|credential|signature)/i,
858
+ /\b(auth|crypto|encrypt|decrypt|hash)/i,
859
+ /\b(session|nonce|salt)\b/i,
860
+ /Math\.random.*\*\s*1e\d+/, // Math.random() * 1e16 (large numbers for IDs)
861
+ ]
862
+
863
+ if (securityPatterns.some(p => p.test(lineContent) || p.test(context))) {
864
+ return false // Not cosmetic - this is security-sensitive
865
+ }
866
+
867
+ // Check for .toString(36) WITHOUT substring/slice/substr (security token pattern)
868
+ // If it has substring/slice/substr, it's already caught by cosmeticLinePatterns above
869
+ const hasToString36WithoutTruncation = /Math\.random\(\)\.toString\(36\)/.test(lineContent) &&
870
+ !/\.(substring|substr|slice)\(/.test(lineContent)
871
+
872
+ const hasToString16WithoutTruncation = /Math\.random\(\)\.toString\(16\)/.test(lineContent) &&
873
+ !/\.(substring|substr|slice)\(/.test(lineContent)
874
+
875
+ if (hasToString36WithoutTruncation || hasToString16WithoutTruncation) {
876
+ return false // Full-length toString() without truncation - likely security token
877
+ }
878
+
879
+ return false // Default to flagging if unclear
880
+ }
881
+
882
+ export function detectDangerousFunctions(
883
+ content: string,
884
+ filePath: string
885
+ ): Vulnerability[] {
886
+ const vulnerabilities: Vulnerability[] = []
887
+
888
+ // Skip scanner/fixture files to avoid self-detection
889
+ if (isScannerOrFixtureFile(filePath)) {
890
+ return vulnerabilities
891
+ }
892
+
893
+ const lines = content.split('\n')
894
+ const isTestFile = isTestOrMockFile(filePath)
895
+
896
+ lines.forEach((line, index) => {
897
+ // Skip comment lines
898
+ if (isComment(line)) return
899
+
900
+ for (const funcPattern of DANGEROUS_FUNCTIONS) {
901
+ // Check language filter
902
+ if (!matchesLanguage(filePath, funcPattern.languages)) continue
903
+
904
+ const regex = new RegExp(funcPattern.pattern.source, funcPattern.pattern.flags)
905
+
906
+ if (regex.test(line)) {
907
+ // Special handling for innerHTML patterns
908
+ if (funcPattern.name === 'innerHTML assignment' ||
909
+ funcPattern.name === 'dangerouslySetInnerHTML') {
910
+
911
+ // Check if this uses static content only
912
+ if (isStaticHTMLContent(line, content, index)) {
913
+ vulnerabilities.push({
914
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
915
+ filePath,
916
+ lineNumber: index + 1,
917
+ lineContent: line.trim(),
918
+ severity: 'info',
919
+ category: 'dangerous_function',
920
+ title: funcPattern.name + ' (static content)',
921
+ description: 'Static HTML assignment detected. Generally safe for hardcoded content, but consider using textContent for plain text or proper DOM methods for dynamic content.',
922
+ suggestedFix: 'If this is plain text, use textContent instead. If HTML must be used, ensure it is static and does not come from user input.',
923
+ confidence: 'low',
924
+ layer: 2,
925
+ })
926
+ break // Only report once per line
927
+ }
928
+
929
+ // Check if DOMPurify or similar sanitization is used
930
+ if (hasDOMPurifySanitization(line, content, index)) {
931
+ vulnerabilities.push({
932
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
933
+ filePath,
934
+ lineNumber: index + 1,
935
+ lineContent: line.trim(),
936
+ severity: 'info',
937
+ category: 'dangerous_function',
938
+ title: funcPattern.name + ' (sanitized)',
939
+ description: 'HTML is sanitized before rendering (DOMPurify or similar detected). This is the recommended pattern for rendering user-generated HTML.',
940
+ suggestedFix: 'Ensure DOMPurify is configured correctly and kept up to date.',
941
+ confidence: 'low',
942
+ layer: 2,
943
+ })
944
+ break // Only report once per line
945
+ }
946
+
947
+ // Check if this is a static bootstrap script (e.g., theme/font loader)
948
+ if (isStaticBootstrapScript(line, content, index)) {
949
+ vulnerabilities.push({
950
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
951
+ filePath,
952
+ lineNumber: index + 1,
953
+ lineContent: line.trim(),
954
+ severity: 'info',
955
+ category: 'dangerous_function',
956
+ title: funcPattern.name + ' (static bootstrap script)',
957
+ description: 'This appears to be a static bootstrap script (e.g., reading localStorage for theme/font preferences). Low risk as no untrusted data is interpolated into the HTML/JS.',
958
+ suggestedFix: 'Verify no user-controlled data is interpolated into the script content.',
959
+ confidence: 'low',
960
+ layer: 2,
961
+ })
962
+ break // Only report once per line
963
+ }
964
+
965
+ // Check if this is in LLM prompt context (not XSS - it's prompt injection)
966
+ if (isLLMPromptContext(line, content, filePath)) {
967
+ vulnerabilities.push({
968
+ id: `dangerous-func-${filePath}-${index + 1}-prompt-injection`,
969
+ filePath,
970
+ lineNumber: index + 1,
971
+ lineContent: line.trim(),
972
+ severity: 'info',
973
+ category: 'ai_pattern',
974
+ title: 'Potential prompt injection risk',
975
+ description: 'User content is being used in an LLM prompt context. This is NOT XSS (the content goes to an AI, not a DOM). However, untrusted content in prompts may lead to prompt injection attacks.',
976
+ suggestedFix: 'Consider input validation, content filtering, or structured prompts to limit prompt injection risk.',
977
+ confidence: 'low',
978
+ layer: 2,
979
+ })
980
+ break // Only report once per line
981
+ }
982
+
983
+ // Dynamic content - full severity, needs AI validation
984
+ let severity = funcPattern.severity
985
+ if (isTestFile) {
986
+ severity = 'low'
987
+ }
988
+
989
+ vulnerabilities.push({
990
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
991
+ filePath,
992
+ lineNumber: index + 1,
993
+ lineContent: line.trim(),
994
+ severity,
995
+ category: 'dangerous_function',
996
+ title: funcPattern.name,
997
+ description: funcPattern.description + ' This appears to use dynamic content which increases XSS risk.' + (isTestFile ? ' (in test file)' : ''),
998
+ suggestedFix: funcPattern.suggestedFix,
999
+ confidence: isTestFile ? 'low' : 'high',
1000
+ layer: 2,
1001
+ requiresAIValidation: true, // Dynamic HTML needs validation
1002
+ })
1003
+ break // Only report once per line
1004
+ }
1005
+
1006
+ // Note: JSON.parse is now handled by standalone detectJSONParseSafe() function
1007
+ // which provides better source-aware severity classification
1008
+
1009
+ // Special handling for eval and Function constructor
1010
+ if (funcPattern.name === 'eval() usage' || funcPattern.name === 'Function constructor') {
1011
+ // Suppress entirely in test files - test files legitimately test eval behavior
1012
+ if (isTestFile) {
1013
+ break // Skip reporting entirely
1014
+ }
1015
+
1016
+ // Check if eval is inside a test assertion (expect(), test(), it(), describe())
1017
+ const testAssertionPattern = /\b(expect|test|it|describe)\s*\(/
1018
+ if (testAssertionPattern.test(line)) {
1019
+ break // Skip reporting - this is testing eval behavior
1020
+ }
1021
+
1022
+ // Check if inputs are static literals (low risk)
1023
+ if (hasOnlyStaticInputs(line, content, index)) {
1024
+ vulnerabilities.push({
1025
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1026
+ filePath,
1027
+ lineNumber: index + 1,
1028
+ lineContent: line.trim(),
1029
+ severity: 'info',
1030
+ category: 'dangerous_function',
1031
+ title: funcPattern.name + ' (static input)',
1032
+ description: 'eval/Function with static string literal input. Lower risk than dynamic input, but consider refactoring to avoid eval entirely.',
1033
+ suggestedFix: 'Consider using JSON.parse() for JSON data or refactoring to avoid eval.',
1034
+ confidence: 'low',
1035
+ layer: 2,
1036
+ })
1037
+ break
1038
+ }
1039
+
1040
+ vulnerabilities.push({
1041
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1042
+ filePath,
1043
+ lineNumber: index + 1,
1044
+ lineContent: line.trim(),
1045
+ severity: funcPattern.severity,
1046
+ category: 'dangerous_function',
1047
+ title: funcPattern.name,
1048
+ description: funcPattern.description,
1049
+ suggestedFix: funcPattern.suggestedFix,
1050
+ confidence: 'high',
1051
+ layer: 2,
1052
+ requiresAIValidation: true, // Code execution patterns need validation
1053
+ })
1054
+ break
1055
+ }
1056
+
1057
+ // Special handling for child_process exec - verify it's not RegExp.exec
1058
+ if (funcPattern.name === 'child_process exec') {
1059
+ // First check if this is actually from child_process (not RegExp.exec)
1060
+ const isExecMatch = /\bexec\s*\(/.test(line)
1061
+ const isOtherMatch = /\b(execSync|spawn|spawnSync|execFile)\s*\(/.test(line)
1062
+
1063
+ if (isExecMatch && !isOtherMatch) {
1064
+ // This matched 'exec(' - verify it's from child_process
1065
+ if (!isChildProcessExec(content, line)) {
1066
+ // This is RegExp.exec or similar - skip
1067
+ break
1068
+ }
1069
+ } else if (isOtherMatch) {
1070
+ // This matched spawn/execSync/etc - verify child_process import
1071
+ if (!isChildProcessSpawn(content, line)) {
1072
+ // No child_process import - skip
1073
+ break
1074
+ }
1075
+ }
1076
+
1077
+ if (hasOnlyStaticInputs(line, content, index)) {
1078
+ vulnerabilities.push({
1079
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1080
+ filePath,
1081
+ lineNumber: index + 1,
1082
+ lineContent: line.trim(),
1083
+ severity: 'info',
1084
+ category: 'dangerous_function',
1085
+ title: funcPattern.name + ' (static command)',
1086
+ description: 'exec/execSync with hardcoded command. Lower risk than dynamic commands, but ensure command does not change based on user input.',
1087
+ suggestedFix: 'If command is truly static, this is generally acceptable. For dynamic commands, validate and sanitize inputs.',
1088
+ confidence: 'low',
1089
+ layer: 2,
1090
+ })
1091
+ break
1092
+ }
1093
+ }
1094
+
1095
+ // Special handling for SQL patterns - check for whitelist validation
1096
+ if (funcPattern.name === 'Raw SQL query construction' || funcPattern.name === 'SQL template literal') {
1097
+ if (hasSQLWhitelistValidation(content, index)) {
1098
+ vulnerabilities.push({
1099
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1100
+ filePath,
1101
+ lineNumber: index + 1,
1102
+ lineContent: line.trim(),
1103
+ severity: 'info',
1104
+ category: 'dangerous_function',
1105
+ title: funcPattern.name + ' (whitelist validated)',
1106
+ description: 'SQL query with dynamic content, but whitelist/allowlist validation detected. This is a safer pattern that limits injection risk.',
1107
+ suggestedFix: 'Ensure the whitelist is comprehensive and cannot be bypassed. Consider using parameterized queries for additional safety.',
1108
+ confidence: 'low',
1109
+ layer: 2,
1110
+ })
1111
+ break
1112
+ }
1113
+ }
1114
+
1115
+ // Special handling for Dynamic file path - skip for utility files
1116
+ // Utility functions designed to work with file paths are expected to take path parameters
1117
+ if (funcPattern.name === 'Dynamic file path') {
1118
+ // Skip for utility/lib/helper files - these are internal functions, not API handlers
1119
+ const isUtilityFile = /\/(utils?|lib|helpers?|services?|modules?)\//i.test(filePath)
1120
+ // Skip if function name suggests it's designed for file operations
1121
+ const isFileOperationFunction = /\b(checksum|hash|digest|fingerprint|read|write|load|save|get|set|copy|move|delete)File/i.test(content.slice(Math.max(0, index - 200), index + 100))
1122
+
1123
+ // Skip CLI command files - these take paths from command-line args (controlled inputs)
1124
+ const isCLIFile = /\/(cli|commands?|bin)\//i.test(filePath) ||
1125
+ /\/src\/(index|main|cli)\.(ts|js)$/i.test(filePath)
1126
+
1127
+ // Skip GitHub Action files - these process repo files (controlled environment)
1128
+ const isGitHubAction = /github-action/i.test(filePath) ||
1129
+ /action\.(ts|js|yml|yaml)$/i.test(filePath)
1130
+
1131
+ // Check for schema validation patterns in the surrounding context
1132
+ // Zod, Yup, Joi, or regex validation on the input
1133
+ const contextWindow = content.slice(Math.max(0, content.indexOf(line) - 500), content.indexOf(line) + line.length)
1134
+ const hasSchemaValidation = /z\.(string|object)\s*\(\s*\)\.regex\s*\(/i.test(contextWindow) ||
1135
+ /z\.enum\s*\(/i.test(contextWindow) ||
1136
+ /\.regex\s*\(\s*\/.*\/\s*\)/i.test(contextWindow) || // .regex(/.../)
1137
+ /\.match\s*\(\s*\/.*\/\s*\)/i.test(contextWindow) || // .match(/.../)
1138
+ /\.(schema|validate)\s*\(/i.test(contextWindow) ||
1139
+ /joi\./i.test(contextWindow) ||
1140
+ /yup\./i.test(contextWindow)
1141
+
1142
+ // Check for path sanitization patterns
1143
+ const hasPathSanitization = hasPathTraversalProtection(contextWindow, line)
1144
+
1145
+ if (isUtilityFile || isFileOperationFunction || isTestFile || isCLIFile || isGitHubAction || hasSchemaValidation || hasPathSanitization) {
1146
+ // Skip entirely for utility functions or when schema validation is present
1147
+ break
1148
+ }
1149
+ }
1150
+
1151
+ // Special handling for Path traversal risk - check for sanitization
1152
+ if (funcPattern.name === 'Path traversal risk') {
1153
+ const contextWindow = content.slice(Math.max(0, content.indexOf(line) - 500), content.indexOf(line) + line.length + 200)
1154
+
1155
+ // Check for path sanitization patterns
1156
+ if (hasPathTraversalProtection(contextWindow, line)) {
1157
+ vulnerabilities.push({
1158
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1159
+ filePath,
1160
+ lineNumber: index + 1,
1161
+ lineContent: line.trim(),
1162
+ severity: 'info',
1163
+ category: 'dangerous_function',
1164
+ title: funcPattern.name + ' (sanitized)',
1165
+ description: 'User input in file path, but path traversal protection detected. Verify sanitization is comprehensive.',
1166
+ suggestedFix: 'Ensure path.resolve() result is checked against base directory and ".." sequences are rejected.',
1167
+ confidence: 'low',
1168
+ layer: 2,
1169
+ })
1170
+ break
1171
+ }
1172
+ }
1173
+
1174
+ // Special handling for Math.random() - skip cosmetic/UI uses
1175
+ if (funcPattern.name === 'Math.random for security') {
1176
+ // Check if this is cosmetic use (CSS, animations, UI variations)
1177
+ if (isCosmeticMathRandom(line, content, index)) {
1178
+ // Skip entirely - this is not a security concern
1179
+ break
1180
+ }
1181
+ }
1182
+
1183
+ // Standard handling for all other patterns
1184
+ let severity = funcPattern.severity
1185
+ let confidence: 'high' | 'medium' | 'low' = 'high'
1186
+
1187
+ if (isTestFile) {
1188
+ if (severity === 'critical') {
1189
+ severity = 'medium'
1190
+ } else if (severity === 'high') {
1191
+ severity = 'low'
1192
+ } else {
1193
+ severity = 'info'
1194
+ }
1195
+ confidence = 'low'
1196
+ }
1197
+
1198
+ vulnerabilities.push({
1199
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1200
+ filePath,
1201
+ lineNumber: index + 1,
1202
+ lineContent: line.trim(),
1203
+ severity,
1204
+ category: 'dangerous_function',
1205
+ title: funcPattern.name,
1206
+ description: funcPattern.description + (isTestFile ? ' (in test file)' : ''),
1207
+ suggestedFix: funcPattern.suggestedFix,
1208
+ confidence,
1209
+ layer: 2,
1210
+ })
1211
+ break // Only report once per line
1212
+ }
1213
+ }
1214
+ })
1215
+
1216
+ // Additional standalone checks (not in DANGEROUS_FUNCTIONS array)
1217
+
1218
+ // JSON.parse source-aware detection
1219
+ detectJSONParseSafe(content, filePath, isTestFile, vulnerabilities)
1220
+
1221
+ // request.json() / req.json() schema validation suggestion
1222
+ detectRequestJsonValidation(content, filePath, isTestFile, vulnerabilities)
1223
+
1224
+ return vulnerabilities
1225
+ }
1226
+
1227
+ /**
1228
+ * Detect JSON.parse usage with source-aware severity
1229
+ * Much smarter than simple pattern matching - considers try/catch and data source
1230
+ */
1231
+ function detectJSONParseSafe(
1232
+ content: string,
1233
+ filePath: string,
1234
+ isTestFile: boolean,
1235
+ vulnerabilities: Vulnerability[]
1236
+ ): void {
1237
+ const lines = content.split('\n')
1238
+ const jsonParsePattern = /JSON\.parse\s*\(/gi
1239
+
1240
+ // Track instances per file to aggregate noisy patterns
1241
+ const instances: { lineNumber: number; lineContent: string; source: JSONParseSource }[] = []
1242
+
1243
+ lines.forEach((line, index) => {
1244
+ if (isComment(line)) return
1245
+
1246
+ jsonParsePattern.lastIndex = 0
1247
+ if (!jsonParsePattern.test(line)) return
1248
+
1249
+ const jsonSource = classifyJSONParseSource(line, filePath)
1250
+
1251
+ // Skip migration files entirely - they're internal tooling
1252
+ if (jsonSource === 'migration') return
1253
+
1254
+ // Skip test fixtures entirely - they're intentionally parsing test data
1255
+ if (jsonSource === 'test_fixture') return
1256
+
1257
+ // Skip trusted SDK responses - these are well-defined and safe to parse
1258
+ if (isTrustedSDKResponse(line, content)) return
1259
+
1260
+ // Check if JSON.parse is inside a try-catch block
1261
+ const insideTryCatch = isInsideTryCatch(content, index) || hasTryCatchNearby(content, index)
1262
+
1263
+ // Check if schema validation is applied after JSON.parse
1264
+ const hasSchemaValidation = hasSchemaValidationNearby(content, index)
1265
+
1266
+ // If inside try-catch with safe source, suppress entirely - this is perfectly fine
1267
+ if (insideTryCatch && ['local_storage', 'database', 'config', 'internal', 'ui_state'].includes(jsonSource)) {
1268
+ return
1269
+ }
1270
+
1271
+ // If schema validation is present, this is properly handled
1272
+ if (hasSchemaValidation) {
1273
+ return
1274
+ }
1275
+
1276
+ // UI state (settings, providers, modals) - very low risk, aggregate or skip
1277
+ if (jsonSource === 'ui_state') {
1278
+ // Only track for aggregation, don't report individually
1279
+ instances.push({ lineNumber: index + 1, lineContent: line.trim(), source: jsonSource })
1280
+ return
1281
+ }
1282
+
1283
+ // Determine severity based on source and error handling
1284
+ let severity: VulnerabilitySeverity
1285
+ let description: string
1286
+ let suggestedFix: string
1287
+ let confidence: 'high' | 'medium' | 'low' = 'medium'
1288
+
1289
+ if (insideTryCatch) {
1290
+ // Already has error handling
1291
+ switch (jsonSource) {
1292
+ case 'user_input':
1293
+ severity = 'low'
1294
+ description = 'JSON.parse on user input is wrapped in try-catch. Consider adding schema validation (zod/yup) to validate the parsed structure.'
1295
+ suggestedFix = 'Add schema validation after parsing: const validated = schema.parse(JSON.parse(input))'
1296
+ confidence = 'low'
1297
+ break
1298
+ default:
1299
+ // With try-catch and non-user source, this is fine - don't report
1300
+ return
1301
+ }
1302
+ } else {
1303
+ // No try-catch
1304
+ switch (jsonSource) {
1305
+ case 'user_input':
1306
+ severity = 'medium'
1307
+ description = 'JSON.parse on user input without schema validation. Malformed input will crash; malicious input may have unexpected shape.'
1308
+ suggestedFix = 'Use a schema validation library (zod, yup, joi): try { const data = schema.parse(JSON.parse(body)) } catch (e) { return 400 }'
1309
+ confidence = 'high'
1310
+ break
1311
+ case 'local_storage':
1312
+ severity = 'info'
1313
+ description = 'JSON.parse on localStorage data. Consider adding try-catch for robustness against corrupted data.'
1314
+ suggestedFix = 'Wrap in try-catch to handle corrupted localStorage gracefully.'
1315
+ confidence = 'low'
1316
+ break
1317
+ case 'database':
1318
+ // Database content parsing is very common and low-risk
1319
+ instances.push({ lineNumber: index + 1, lineContent: line.trim(), source: jsonSource })
1320
+ return // Will be aggregated below
1321
+ case 'config':
1322
+ case 'internal':
1323
+ severity = 'info'
1324
+ description = `JSON.parse on ${jsonSource.replace('_', ' ')} data without error handling. Low risk but consider defensive coding.`
1325
+ suggestedFix = 'Consider adding try-catch for robustness.'
1326
+ confidence = 'low'
1327
+ break
1328
+ default:
1329
+ // Unknown source - track for potential aggregation
1330
+ instances.push({ lineNumber: index + 1, lineContent: line.trim(), source: jsonSource })
1331
+ return // Will be evaluated below based on aggregation
1332
+ }
1333
+ }
1334
+
1335
+ // Downgrade test files
1336
+ if (isTestFile) {
1337
+ severity = 'info'
1338
+ confidence = 'low'
1339
+ description += ' (in test file)'
1340
+ }
1341
+
1342
+ vulnerabilities.push({
1343
+ id: `json-parse-${filePath}-${index + 1}`,
1344
+ filePath,
1345
+ lineNumber: index + 1,
1346
+ lineContent: line.trim(),
1347
+ severity,
1348
+ category: 'dangerous_function',
1349
+ title: 'JSON.parse usage',
1350
+ description,
1351
+ suggestedFix,
1352
+ confidence,
1353
+ layer: 2,
1354
+ })
1355
+ })
1356
+
1357
+ // Aggregate low-risk JSON.parse instances if there are many
1358
+ if (instances.length >= 3) {
1359
+ // Create single aggregated finding instead of N individual findings
1360
+ const lineNumbers = instances.map(i => i.lineNumber).slice(0, 5)
1361
+ const moreText = instances.length > 5 ? `... (${instances.length} total)` : ''
1362
+
1363
+ vulnerabilities.push({
1364
+ id: `json-parse-aggregated-${filePath}`,
1365
+ filePath,
1366
+ lineNumber: instances[0].lineNumber,
1367
+ lineContent: `${instances.length} instances across this file`,
1368
+ severity: 'info',
1369
+ category: 'dangerous_function',
1370
+ title: `JSON.parse usage (${instances.length} instances)`,
1371
+ description: `JSON.parse detected. Consider adding error handling and schema validation if parsing user input.${isTestFile ? ' (in test file)' : ''}\n\nFound ${instances.length} occurrences at lines: ${lineNumbers.join(', ')}${moreText}`,
1372
+ suggestedFix: 'Add try-catch for error handling. If parsing user input, add schema validation.',
1373
+ confidence: 'low',
1374
+ layer: 2,
1375
+ })
1376
+ } else if (instances.length > 0 && instances.length < 3) {
1377
+ // Report individually for small counts
1378
+ for (const instance of instances) {
1379
+ vulnerabilities.push({
1380
+ id: `json-parse-${filePath}-${instance.lineNumber}`,
1381
+ filePath,
1382
+ lineNumber: instance.lineNumber,
1383
+ lineContent: instance.lineContent,
1384
+ severity: 'info',
1385
+ category: 'dangerous_function',
1386
+ title: 'JSON.parse usage',
1387
+ description: `JSON.parse on ${instance.source.replace('_', ' ')} data without error handling. Low risk but consider defensive coding.${isTestFile ? ' (in test file)' : ''}`,
1388
+ suggestedFix: 'Consider adding try-catch for robustness.',
1389
+ confidence: 'low',
1390
+ layer: 2,
1391
+ })
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ /**
1397
+ * Check if this file appears to have form/input validation elsewhere
1398
+ * (manual checks on body fields, type guards, etc.)
1399
+ */
1400
+ function hasManualValidation(content: string): boolean {
1401
+ const manualValidationPatterns = [
1402
+ // Type checking / type guards
1403
+ /typeof\s+\w+\s*[!=]==?\s*['"](?:string|number|boolean|object)['"]|Array\.isArray\s*\(/i,
1404
+ // Field existence checks followed by throws/returns
1405
+ /if\s*\(\s*!(?:body|data|input)\.\w+\s*\)\s*\{?\s*(throw|return)/i,
1406
+ // Property access with type assertion comments or inline validation
1407
+ /\b(body|data|input)\s*as\s+\w+/i, // Type assertion
1408
+ // Manual validation with error handling
1409
+ /if\s*\(\s*![\w.]+\s*\|\|\s*typeof\s+[\w.]+/i,
1410
+ // Using type predicates
1411
+ /is\w+\s*\([\w.]+\)/i, // isFoo(bar) pattern
1412
+ ]
1413
+
1414
+ return manualValidationPatterns.some(p => p.test(content))
1415
+ }
1416
+
1417
+ /**
1418
+ * Check if route has throwing auth helper (getCurrentUserId, requireAuth, etc.)
1419
+ * Routes with throwing auth helpers are already protected
1420
+ */
1421
+ function hasThrowingAuthHelper(content: string): boolean {
1422
+ const throwingAuthPatterns = [
1423
+ /\bgetCurrentUserId\s*\(/i,
1424
+ /\brequireAuth\s*\(/i,
1425
+ /\bensureAuth\s*\(/i,
1426
+ /\bauth\s*\(\s*\)\s*\.protect\s*\(/i, // Clerk: auth().protect()
1427
+ /\bcurrentUser\s*\(\s*\)/i, // Clerk: currentUser()
1428
+ /\bgetServerSession\s*\([^)]*\)/i, // NextAuth
1429
+ /\bauth\s*\(\s*\)/i, // Generic auth() call
1430
+ /\bcheckAuth\s*\(/i,
1431
+ /\bverifyAuth\s*\(/i,
1432
+ /\bvalidateAuth\s*\(/i,
1433
+ /\bassertAuth\s*\(/i,
1434
+ /\bgetAuth\s*\(/i,
1435
+ /\brequireUser\s*\(/i,
1436
+ /\bgetUser\s*\(\s*\)/i, // supabase.auth.getUser()
1437
+ /const\s+\{\s*user\s*\}\s*=\s*await/i, // Destructuring pattern
1438
+ ]
1439
+ return throwingAuthPatterns.some(p => p.test(content))
1440
+ }
1441
+
1442
+ /**
1443
+ * Detect request.json() / req.json() and suggest schema validation
1444
+ * This is NOT a dangerous function - it's a prompt for best practices
1445
+ */
1446
+ function detectRequestJsonValidation(
1447
+ content: string,
1448
+ filePath: string,
1449
+ isTestFile: boolean,
1450
+ vulnerabilities: Vulnerability[]
1451
+ ): void {
1452
+ // Only check API route files
1453
+ if (!/\/(api|routes?|handlers?|controllers?)\//i.test(filePath) &&
1454
+ !/route\.(ts|js)$/i.test(filePath)) {
1455
+ return
1456
+ }
1457
+
1458
+ // Skip if route has throwing auth helper - these are already protected routes
1459
+ // and the schema validation suggestion is lower priority
1460
+ if (hasThrowingAuthHelper(content)) {
1461
+ return
1462
+ }
1463
+
1464
+ const lines = content.split('\n')
1465
+ // Matches: request.json(), req.json(), await request.json(), etc.
1466
+ const requestJsonPattern = /\b(request|req)\.json\s*\(\s*\)/gi
1467
+
1468
+ // Check if file has schema validation (library-based)
1469
+ const hasSchemaLibrary = /\b(zod|yup|joi|ajv|superstruct|valibot|typebox)\b/i.test(content) ||
1470
+ /\.parse\s*\(|\.validate\s*\(|\.safeParse\s*\(/i.test(content)
1471
+
1472
+ // If file has schema library validation, don't report
1473
+ if (hasSchemaLibrary) return
1474
+
1475
+ // Check for manual validation patterns (less robust but still indicates intent)
1476
+ const hasManualCheck = hasManualValidation(content)
1477
+
1478
+ // Track instances for potential aggregation
1479
+ const instances: { lineNumber: number; lineContent: string }[] = []
1480
+
1481
+ lines.forEach((line, index) => {
1482
+ if (isComment(line)) return
1483
+
1484
+ requestJsonPattern.lastIndex = 0
1485
+ if (!requestJsonPattern.test(line)) return
1486
+
1487
+ // Check if there's validation nearby (within 10 lines after)
1488
+ const startCheck = index
1489
+ const endCheck = Math.min(lines.length, index + 10)
1490
+ const nearbyContent = lines.slice(startCheck, endCheck).join('\n')
1491
+
1492
+ // If there's validation in the nearby lines, skip
1493
+ if (/\.parse\s*\(|\.validate\s*\(|\.safeParse\s*\(|schema\./i.test(nearbyContent)) {
1494
+ return
1495
+ }
1496
+
1497
+ // If manual validation is present, skip individual reporting but track for aggregate
1498
+ if (hasManualCheck) {
1499
+ instances.push({ lineNumber: index + 1, lineContent: line.trim() })
1500
+ return
1501
+ }
1502
+
1503
+ if (isTestFile) {
1504
+ return // Don't report in test files
1505
+ }
1506
+
1507
+ instances.push({ lineNumber: index + 1, lineContent: line.trim() })
1508
+ })
1509
+
1510
+ // Don't report if no instances found
1511
+ if (instances.length === 0) return
1512
+
1513
+ // If manual validation exists, create a single info-level note
1514
+ if (hasManualCheck && instances.length > 0) {
1515
+ vulnerabilities.push({
1516
+ id: `request-json-manual-${filePath}`,
1517
+ filePath,
1518
+ lineNumber: instances[0].lineNumber,
1519
+ lineContent: instances[0].lineContent,
1520
+ severity: 'info',
1521
+ category: 'dangerous_function',
1522
+ title: 'Request body with manual validation',
1523
+ description: `API endpoint parses request body with manual validation patterns detected. Consider using a schema library (zod, yup) for more robust type-safe validation.`,
1524
+ suggestedFix: 'While manual validation works, schema libraries provide better TypeScript integration and error messages.',
1525
+ confidence: 'low',
1526
+ layer: 2,
1527
+ })
1528
+ return
1529
+ }
1530
+
1531
+ // Aggregate if multiple instances without validation
1532
+ if (instances.length >= 2) {
1533
+ const lineNumbers = instances.map(i => i.lineNumber).slice(0, 5)
1534
+ vulnerabilities.push({
1535
+ id: `request-json-aggregated-${filePath}`,
1536
+ filePath,
1537
+ lineNumber: instances[0].lineNumber,
1538
+ lineContent: `${instances.length} instances`,
1539
+ severity: 'info',
1540
+ category: 'dangerous_function',
1541
+ title: `Request body without schema validation (${instances.length} instances)`,
1542
+ description: `API endpoint parses request body without visible schema validation at lines: ${lineNumbers.join(', ')}. Consider validating the shape of incoming data.`,
1543
+ suggestedFix: 'Add schema validation (e.g., zod): const body = await request.json(); const data = schema.parse(body);',
1544
+ confidence: 'low',
1545
+ layer: 2,
1546
+ })
1547
+ } else {
1548
+ // Single instance
1549
+ vulnerabilities.push({
1550
+ id: `request-json-${filePath}-${instances[0].lineNumber}`,
1551
+ filePath,
1552
+ lineNumber: instances[0].lineNumber,
1553
+ lineContent: instances[0].lineContent,
1554
+ severity: 'info',
1555
+ category: 'dangerous_function',
1556
+ title: 'Request body without schema validation',
1557
+ description: 'API endpoint parses request body without visible schema validation. Consider validating the shape of incoming data.',
1558
+ suggestedFix: 'Add schema validation (e.g., zod): const body = await request.json(); const data = schema.parse(body);',
1559
+ confidence: 'low',
1560
+ layer: 2,
1561
+ })
1562
+ }
1563
+ }