@oculum/scanner 1.0.9 → 1.0.11

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