@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
@@ -54,21 +54,19 @@ function isUITemplateSuggestion(lineContent: string, surroundingContext: string)
54
54
  const fullContext = lineContent + '\n' + surroundingContext
55
55
 
56
56
  // UI suggestion object patterns (command palette, autocomplete suggestions)
57
+ // Note: Be careful not to match variable declarations like `const completion =`
57
58
  const uiSuggestionPatterns = [
58
- // Object property patterns for suggestion items
59
- /(?:id|key|label|title|name|description|completion|display|text|value|placeholder):\s*`[^`]*\$\{/i,
60
- // Common suggestion UI patterns
61
- /suggestions?\s*[=:]/i,
62
- /completions?\s*[=:]/i,
59
+ // Object property patterns for suggestion items (key: value in objects)
60
+ /(?:id|key|label|title|name|description|display|text|value|placeholder):\s*`[^`]*\$\{/i,
61
+ // Common suggestion UI patterns (arrays or objects, not variable declarations)
62
+ /(?:set)?suggestions\s*[=:]\s*\[/i, // suggestions: [...] or setSuggestions([])
63
63
  /autocomplete/i,
64
64
  /command\s*palette/i,
65
65
  /fuzzy\s*search/i,
66
66
  /search\s*result/i,
67
- // UI component context patterns
68
- /\.map\s*\(\s*\(?(?:item|result|suggestion|node|entry)/i,
69
- /\.filter\s*\(/i,
70
67
  // React/UI state patterns
71
- /useState|setItems|setResults|setSuggestions/i,
68
+ /useState.*suggestions|setSuggestions/i,
69
+ /setItems|setResults/i,
72
70
  // Template ID generation for UI
73
71
  /id:\s*`[a-z]+-\$\{/i, // id: `delete-${...}`, id: `edit-${...}`
74
72
  ]
@@ -87,6 +85,12 @@ function isUITemplateSuggestion(lineContent: string, surroundingContext: string)
87
85
  /exec\s*\(/i,
88
86
  /spawn\s*\(/i,
89
87
  /eval\s*\(/i,
88
+ /fetch\s*\(/i,
89
+ /axios\./i,
90
+ /\.redirect\s*\(/i,
91
+ /\.setHeader\s*\(/i,
92
+ /\.cookie\s*\(/i,
93
+ /location\./i,
90
94
  ]
91
95
 
92
96
  // Check if context matches UI pattern but NOT execution pattern
@@ -174,7 +178,7 @@ function hasOutputValidation(content: string, lineNumber: number): boolean {
174
178
  /validate/i,
175
179
  /sanitize/i,
176
180
  /escape/i,
177
- /filter/i,
181
+ /\.filter\s*\([^)]*(?:allowed|safe|valid)/i, // .filter(x => allowed.includes(x))
178
182
  /parse.*catch/i,
179
183
  /schema\./i,
180
184
  /\.parse\s*\(/i,
@@ -182,9 +186,19 @@ function hasOutputValidation(content: string, lineNumber: number): boolean {
182
186
  /whitelist/i,
183
187
  /blocklist/i,
184
188
  /blacklist/i,
189
+ /allowed(?:Columns|Tables|Hosts|Domains|Extensions|Types|Args|Paths)/i, // Allowlist variable names
185
190
  /JSON\.parse.*catch/i,
186
191
  /DOMPurify/i,
187
192
  /xss/i,
193
+ /encodeURIComponent/i,
194
+ /\.replace\s*\(\s*\/\[.*\]\/[gi]*/i, // Regex sanitization like .replace(/[^a-z0-9]/gi, '')
195
+ /textContent\s*=/i, // Using textContent (safe) instead of innerHTML
196
+ /ReactMarkdown/i, // React Markdown sanitizes by default
197
+ /ast\.literal_eval/i, // Python safe eval
198
+ /yaml\.(?:safe_load|SafeLoader)/i, // Safe YAML parsing
199
+ /\.startsWith\s*\(\s*['"]\/['"]?\)/i, // Relative URL check
200
+ /new\s+URL\s*\(.*\).*origin/i, // URL origin check
201
+ /path\.resolve.*startsWith/i, // Path validation
188
202
  ]
189
203
 
190
204
  return validationPatterns.some(p => p.test(context))
@@ -325,12 +339,28 @@ const EXECUTION_SINK_PATTERNS: ExecutionSinkPattern[] = [
325
339
  // ========== Template/DOM Sinks ==========
326
340
  {
327
341
  name: 'LLM output to innerHTML',
328
- pattern: /\.innerHTML\s*=\s*(?:response|result|output|completion|message|content)(?:\.|\.data\.|\.text|\.content)?/gi,
342
+ pattern: /\.innerHTML\s*=\s*(?:response|result|output|completion|message|content|generated)(?:\.|\.data\.|\.text|\.content)?/gi,
329
343
  sinkType: 'template_render',
330
344
  baseSeverity: 'high',
331
345
  description: 'LLM output assigned to innerHTML. If the model outputs malicious HTML/JS, it will execute (XSS).',
332
346
  suggestedFix: 'Use textContent for plain text. Sanitize HTML with DOMPurify before rendering. Use React/Vue which auto-escape by default.',
333
347
  },
348
+ {
349
+ name: 'LLM output to outerHTML',
350
+ pattern: /\.outerHTML\s*=\s*(?:response|result|output|completion|message|content|generated)(?:\.|\.data\.|\.text|\.content)?/gi,
351
+ sinkType: 'template_render',
352
+ baseSeverity: 'high',
353
+ description: 'LLM output assigned to outerHTML. This replaces the entire element and allows XSS.',
354
+ suggestedFix: 'Use textContent for plain text. Sanitize HTML with DOMPurify before rendering.',
355
+ },
356
+ {
357
+ name: 'LLM output to insertAdjacentHTML',
358
+ pattern: /\.insertAdjacentHTML\s*\([^,]+,\s*(?:response|result|output|completion|message|content|generated)/gi,
359
+ sinkType: 'template_render',
360
+ baseSeverity: 'high',
361
+ description: 'LLM output passed to insertAdjacentHTML. This allows XSS via injected HTML/JS.',
362
+ suggestedFix: 'Use insertAdjacentText for plain text. Sanitize HTML with DOMPurify: el.insertAdjacentHTML("beforeend", DOMPurify.sanitize(content))',
363
+ },
334
364
  {
335
365
  name: 'LLM output to dangerouslySetInnerHTML',
336
366
  pattern: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html:\s*(?:response|result|output|completion|message|content)/gi,
@@ -399,8 +429,405 @@ const EXECUTION_SINK_PATTERNS: ExecutionSinkPattern[] = [
399
429
  description: 'AI output used in module path resolution. Could leak information about file system or enable module confusion attacks.',
400
430
  suggestedFix: 'Validate module name against allowlist before resolution.',
401
431
  },
432
+
433
+ // ========== Phase 2: Network/SSRF Sinks ==========
434
+ {
435
+ name: 'LLM output in fetch URL',
436
+ pattern: /fetch\s*\(\s*(?:response|result|output|completion|aiUrl|generatedUrl|urlFromAi)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
437
+ sinkType: 'code_execution', // SSRF is code-level risk
438
+ baseSeverity: 'critical',
439
+ description: 'AI-generated URL passed to fetch(). Attackers can manipulate the model to make requests to internal services (SSRF), exfiltrate data, or access localhost services.',
440
+ suggestedFix: 'Validate URL against allowlist: const allowed = ["api.example.com"]; if (!allowed.includes(new URL(url).host)) throw. Block private IP ranges.',
441
+ },
442
+ {
443
+ name: 'LLM output in axios request',
444
+ pattern: /axios\.(?:get|post|put|delete|patch|request)\s*\(\s*(?:response|result|output|completion|aiUrl|generatedUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
445
+ sinkType: 'code_execution',
446
+ baseSeverity: 'critical',
447
+ description: 'AI-generated URL passed to axios. This enables SSRF attacks where the model is manipulated to make requests to internal services.',
448
+ suggestedFix: 'Validate URL host against allowlist. Use axios interceptors to block private IPs and internal hosts.',
449
+ },
450
+ {
451
+ name: 'LLM output in axios config',
452
+ pattern: /axios\s*\(\s*\{[^}]*url:\s*(?:response|result|output|completion|aiUrl|generatedUrl)/gi,
453
+ sinkType: 'code_execution',
454
+ baseSeverity: 'critical',
455
+ description: 'AI-generated URL passed to axios via config object. SSRF risk.',
456
+ suggestedFix: 'Validate URL host against allowlist before passing to axios.',
457
+ },
458
+ {
459
+ name: 'LLM output in HTTP client',
460
+ pattern: /(?:got|request|superagent|ky|undici\.fetch)\s*\(\s*(?:response|result|output|completion|aiUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
461
+ sinkType: 'code_execution',
462
+ baseSeverity: 'critical',
463
+ description: 'AI-generated URL passed to HTTP client. Server-Side Request Forgery (SSRF) risk.',
464
+ suggestedFix: 'Validate URLs against allowlist of permitted hosts. Block internal IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, localhost).',
465
+ },
466
+
467
+ // ========== Phase 2: Redirect Sinks ==========
468
+ {
469
+ name: 'LLM output in server redirect',
470
+ pattern: /(?:res|response)\.redirect\s*\(\s*(?:response|result|output|completion|aiUrl|generatedUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
471
+ sinkType: 'template_render', // Open redirect is similar to XSS
472
+ baseSeverity: 'high',
473
+ description: 'AI-generated URL used in HTTP redirect. Attackers can craft prompts to redirect users to phishing sites or malicious pages.',
474
+ suggestedFix: 'Validate redirect URL against allowlist. Only allow redirects to same-origin or known safe domains. Use relative URLs where possible.',
475
+ },
476
+ {
477
+ name: 'LLM output in client redirect assignment',
478
+ pattern: /(?:window\.)?location\.href\s*=\s*(?:response|result|output|completion|aiUrl|generatedUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
479
+ sinkType: 'template_render',
480
+ baseSeverity: 'high',
481
+ description: 'AI-generated URL assigned to location.href. Enables open redirect attacks.',
482
+ suggestedFix: 'Validate URL before assignment. Prefer relative URLs or validate against allowlist: if (!url.startsWith("/") && !allowedHosts.includes(new URL(url).host)) throw',
483
+ },
484
+ {
485
+ name: 'LLM output in location.assign',
486
+ pattern: /location\.assign\s*\(\s*(?:response|result|output|completion|aiUrl|generatedUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
487
+ sinkType: 'template_render',
488
+ baseSeverity: 'high',
489
+ description: 'AI-generated URL passed to location.assign(). Enables open redirect attacks.',
490
+ suggestedFix: 'Validate URL before assignment. Only allow same-origin or allowlisted domains.',
491
+ },
492
+ {
493
+ name: 'LLM output in location.replace',
494
+ pattern: /location\.replace\s*\(\s*(?:response|result|output|completion|aiUrl|generatedUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
495
+ sinkType: 'template_render',
496
+ baseSeverity: 'high',
497
+ description: 'AI-generated URL passed to location.replace(). Enables open redirect attacks.',
498
+ suggestedFix: 'Validate URL before assignment. Only allow same-origin or allowlisted domains.',
499
+ },
500
+ {
501
+ name: 'LLM output in Next.js redirect',
502
+ pattern: /redirect\s*\(\s*(?:response|result|output|completion|aiUrl|generatedUrl)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
503
+ sinkType: 'template_render',
504
+ baseSeverity: 'high',
505
+ description: 'AI-generated URL passed to Next.js redirect(). Enables open redirect attacks.',
506
+ suggestedFix: 'Validate URL before redirect. Only allow relative URLs or allowlisted domains.',
507
+ },
508
+ {
509
+ name: 'LLM output in meta refresh',
510
+ pattern: /<meta[^>]*http-equiv\s*=\s*['"`]refresh['"`][^>]*content\s*=\s*['"`][^'"]*url\s*=\s*(?:\$\{|<%=).*(?:response|output|completion)/gi,
511
+ sinkType: 'template_render',
512
+ baseSeverity: 'high',
513
+ description: 'AI-generated URL in meta refresh tag. Open redirect vulnerability.',
514
+ suggestedFix: 'Avoid meta refresh with dynamic URLs. Use server-side redirects with URL validation instead.',
515
+ },
516
+
517
+ // ========== Phase 2: Header Injection Sinks ==========
518
+ {
519
+ name: 'LLM output in response header',
520
+ pattern: /(?:res|response)\.(?:setHeader|set|header)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:response|result|output|completion|aiValue)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
521
+ sinkType: 'template_render',
522
+ baseSeverity: 'high',
523
+ description: 'AI-generated value used in HTTP response header. Enables header injection attacks (CRLF injection, cache poisoning).',
524
+ suggestedFix: 'Sanitize header values: remove CR/LF characters. Validate against expected format. Never use AI output directly in security-sensitive headers (Set-Cookie, Authorization).',
525
+ },
526
+ {
527
+ name: 'LLM output in cookie',
528
+ pattern: /(?:res|response)\.(?:cookie|setCookie)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:response|result|output|completion)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
529
+ sinkType: 'template_render',
530
+ baseSeverity: 'high',
531
+ description: 'AI-generated value set as cookie. Could enable session fixation or cookie injection attacks.',
532
+ suggestedFix: 'Never use AI output for cookie values. Generate tokens server-side with crypto.randomBytes(). Validate any user-facing values.',
533
+ },
534
+ {
535
+ name: 'LLM output in res.type',
536
+ pattern: /(?:res|response)\.type\s*\(\s*(?:response|result|output|completion)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
537
+ sinkType: 'template_render',
538
+ baseSeverity: 'high',
539
+ description: 'AI-generated value used to set Content-Type. Could enable MIME confusion attacks.',
540
+ suggestedFix: 'Use allowlist for content types: const allowed = ["json", "html", "text"]; if (!allowed.includes(type)) throw',
541
+ },
542
+
543
+ // ========== Phase 3: Additional Code Execution Sinks ==========
544
+ {
545
+ name: 'LLM output to setTimeout/setInterval string',
546
+ pattern: /(?:setTimeout|setInterval)\s*\(\s*(?:response|result|output|completion)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
547
+ sinkType: 'code_execution',
548
+ baseSeverity: 'high',
549
+ description: 'AI-generated string passed to setTimeout/setInterval. When passed a string, these functions act like eval().',
550
+ suggestedFix: 'Never pass strings to setTimeout/setInterval. Use arrow functions: setTimeout(() => doSomething(), 1000)',
551
+ },
552
+ {
553
+ name: 'LLM output to globalThis.eval',
554
+ pattern: /(?:globalThis|window)\[?['"]?eval['"]?\]?\s*\(\s*(?:response|result|output|completion)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
555
+ sinkType: 'code_execution',
556
+ baseSeverity: 'critical',
557
+ description: 'AI-generated code passed to eval via globalThis/window. This is indirect eval() that enables arbitrary code execution.',
558
+ suggestedFix: 'Never eval() LLM output. Use structured output and validation.',
559
+ },
560
+ {
561
+ name: 'LLM output to execa',
562
+ pattern: /execa\s*\(\s*(?:response|result|output|completion)(?:\.choices\[0\]\.message\.content|\.content|\.text)?/gi,
563
+ sinkType: 'shell_command',
564
+ baseSeverity: 'critical',
565
+ description: 'AI-generated command passed to execa. This enables command injection attacks.',
566
+ suggestedFix: 'Never pass LLM output directly to shell. Use allowlists for permitted commands.',
567
+ },
568
+
569
+ // ========== Phase 3: Python-Specific Sinks ==========
570
+ {
571
+ name: 'LLM output to Python eval',
572
+ pattern: /eval\s*\(\s*(?:response|result|output|completion|code)(?:\[['"]?choices['"]?\]\[0\]\[['"]?message['"]?\]\[['"]?content['"]?\]|\.content|\.text)?/gi,
573
+ sinkType: 'code_execution',
574
+ baseSeverity: 'critical',
575
+ description: 'AI-generated code passed to Python eval(). Enables arbitrary code execution.',
576
+ suggestedFix: 'Never eval() LLM output. Use ast.literal_eval() for safe literal evaluation, or JSON parsing with schema validation.',
577
+ },
578
+ {
579
+ name: 'LLM output to Python exec',
580
+ pattern: /exec\s*\(\s*(?:response|result|output|completion)(?:\[['"]?choices['"]?\]\[0\]\[['"]?message['"]?\]\[['"]?content['"]?\]|\.content|\.text)?/gi,
581
+ sinkType: 'code_execution',
582
+ baseSeverity: 'critical',
583
+ description: 'AI-generated code passed to Python exec(). Enables arbitrary code execution.',
584
+ suggestedFix: 'Never exec() LLM output. Use structured output and validation instead.',
585
+ },
586
+ {
587
+ name: 'LLM output to pickle.loads',
588
+ pattern: /pickle\.loads?\s*\(\s*(?:response|result|output|completion|serialized)(?:\.encode\(\)|\.content|\.text)?/gi,
589
+ sinkType: 'code_execution',
590
+ baseSeverity: 'critical',
591
+ description: 'AI-generated data passed to pickle.loads(). Pickle deserialization can execute arbitrary code.',
592
+ suggestedFix: 'Never unpickle untrusted data. Use JSON or other safe serialization formats.',
593
+ },
594
+ {
595
+ name: 'LLM output to subprocess with shell=True',
596
+ pattern: /subprocess\.(?:run|call|Popen)\s*\(\s*(?:response|result|output|completion|ai_command|generated_cmd)(?:\.content|\.text)?[^)]*shell\s*=\s*True/gi,
597
+ sinkType: 'shell_command',
598
+ baseSeverity: 'critical',
599
+ description: 'AI-generated command passed to subprocess with shell=True. Enables command injection.',
600
+ suggestedFix: 'Never use shell=True with user/AI input. Use subprocess.run(["cmd", "arg1", "arg2"]) without shell.',
601
+ },
602
+ {
603
+ name: 'LLM output to os.system',
604
+ pattern: /os\.system\s*\(\s*(?:response|result|output|completion|generated_cmd|ai_command)(?:\.content|\.text)?/gi,
605
+ sinkType: 'shell_command',
606
+ baseSeverity: 'critical',
607
+ description: 'AI-generated command passed to os.system(). Enables command injection.',
608
+ suggestedFix: 'Use subprocess.run() with list arguments instead of os.system(). Never pass AI output to shell.',
609
+ },
610
+ {
611
+ name: 'Python SQL f-string injection',
612
+ pattern: /cursor\.execute\s*\(\s*f["'].*\{.*(?:response|result|output|completion)/gi,
613
+ sinkType: 'sql_builder',
614
+ baseSeverity: 'critical',
615
+ description: 'AI-generated value interpolated into SQL query via f-string. Enables SQL injection.',
616
+ suggestedFix: 'Use parameterized queries: cursor.execute("SELECT * FROM users WHERE id = ?", [user_id])',
617
+ },
402
618
  ]
403
619
 
620
+ // ============================================================================
621
+ // Phase 2: URL/Network Validation Detection
622
+ // ============================================================================
623
+
624
+ /**
625
+ * Check if URL validation is present (returns 'strong', 'weak', or 'none')
626
+ * Strong validation = skip finding entirely
627
+ * Weak validation = downgrade severity
628
+ */
629
+ function getURLValidationLevel(content: string, lineNumber: number): 'strong' | 'weak' | 'none' {
630
+ const lines = content.split('\n')
631
+ const contextStart = Math.max(0, lineNumber - 15)
632
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
633
+ const context = lines.slice(contextStart, contextEnd).join('\n')
634
+
635
+ // Strong validation - skip entirely
636
+ const strongValidationPatterns = [
637
+ /allowedHosts\.includes\s*\(\s*(?:new\s+URL)?/i, // Explicit allowlist check
638
+ /safeDomains\.includes\s*\(/i, // Safe domain allowlist
639
+ /allowedDomains\.includes\s*\(/i, // Allowed domain check
640
+ /if\s*\(\s*allowedHosts/i, // Conditional on allowlist
641
+ /if\s*\(\s*safeDomains/i, // Conditional on safe domains
642
+ /url\.origin\s*===\s*(?:window\.)?(?:location\.)?origin/i, // Same-origin check
643
+ /\.origin\s*===\s*origin/i, // Same-origin check
644
+ /\.startsWith\s*\(\s*['"]\/['"]?\s*\)\s*&&\s*!\s*\w+\.startsWith\s*\(\s*['"]\/\//i, // Relative URL with protocol-relative check
645
+ /if\s*\(\s*\w+\.startsWith\s*\(\s*['"]\/['"]?\s*\)\s*&&\s*!/i, // Relative URL validation
646
+ /blockedHosts\.includes\s*\(/i, // Block list check
647
+ /privateIpPatterns\.some\s*\(/i, // Private IP blocking
648
+ ]
649
+
650
+ if (strongValidationPatterns.some(p => p.test(context))) {
651
+ return 'strong'
652
+ }
653
+
654
+ // Weak validation - downgrade severity
655
+ const weakValidationPatterns = [
656
+ /isValidUrl|validateUrl|isAllowedUrl/i,
657
+ /new\s+URL\s*\(.*\).*(?:host|hostname|origin)/i,
658
+ /allowedUrls|allowedHosts|allowedDomains|safeDomains/i,
659
+ /url\.startsWith\s*\(\s*['"`](?:https?:\/\/|\/[^\/])/i,
660
+ /sanitizeUrl|encodeURIComponent/i,
661
+ /blockedHosts|blockedDomains|privateIp/i,
662
+ /\.includes\s*\(\s*(?:new\s+URL\s*\()?.*\.host/i,
663
+ ]
664
+
665
+ if (weakValidationPatterns.some(p => p.test(context))) {
666
+ return 'weak'
667
+ }
668
+
669
+ return 'none'
670
+ }
671
+
672
+ /**
673
+ * Legacy function for backward compatibility
674
+ */
675
+ function hasURLValidation(content: string, lineNumber: number): boolean {
676
+ return getURLValidationLevel(content, lineNumber) !== 'none'
677
+ }
678
+
679
+ /**
680
+ * Check if DOM content is sanitized (e.g., DOMPurify)
681
+ */
682
+ function isDOMSanitized(lineContent: string, surroundingContext: string): boolean {
683
+ const fullContext = lineContent + '\n' + surroundingContext
684
+
685
+ const sanitizationPatterns = [
686
+ /DOMPurify\.sanitize\s*\(/i,
687
+ /sanitizeHtml\s*\(/i,
688
+ /xss\s*\(/i,
689
+ /escapeHtml\s*\(/i,
690
+ /textContent\s*=/i, // textContent is safe
691
+ /innerText\s*=/i, // innerText is safe
692
+ /ReactMarkdown/i, // ReactMarkdown sanitizes by default
693
+ /<ReactMarkdown>/i, // JSX ReactMarkdown
694
+ ]
695
+
696
+ return sanitizationPatterns.some(p => p.test(fullContext))
697
+ }
698
+
699
+ /**
700
+ * Check if file path is properly validated
701
+ */
702
+ function isPathValidated(content: string, lineNumber: number): boolean {
703
+ const lines = content.split('\n')
704
+ const contextStart = Math.max(0, lineNumber - 15)
705
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
706
+ const context = lines.slice(contextStart, contextEnd).join('\n')
707
+
708
+ const pathValidationPatterns = [
709
+ /path\.resolve\s*\([^)]*\).*startsWith/i, // Resolved path + startsWith check
710
+ /resolved\.startsWith\s*\(/i, // Common pattern: resolved.startsWith(baseDir)
711
+ /!.*startsWith.*throw/i, // Validation with throw on failure
712
+ /if\s*\(\s*!?\s*resolved\.startsWith/i, // Conditional path check
713
+ /allowedExtensions\.includes\s*\(/i, // Extension allowlist
714
+ /allowedPaths/i, // Path allowlist
715
+ /SAFE_BASE_DIR/i, // Common safe directory constant
716
+ /baseDir|safeDir|allowedDir/i, // Directory restriction variables
717
+ /path\.basename\s*\(/i, // Only using basename (no traversal)
718
+ /\.replace\s*\(/i, // Generic replace (likely sanitization)
719
+ ]
720
+
721
+ return pathValidationPatterns.some(p => p.test(context))
722
+ }
723
+
724
+ /**
725
+ * Check if header value is sanitized
726
+ */
727
+ function isHeaderSanitized(content: string, lineNumber: number): boolean {
728
+ const lines = content.split('\n')
729
+ const contextStart = Math.max(0, lineNumber - 15)
730
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
731
+ const context = lines.slice(contextStart, contextEnd).join('\n')
732
+
733
+ const headerSanitizationPatterns = [
734
+ /\.replace\s*\(\s*\/\[\\r\\n\]/i, // CRLF removal
735
+ /\.replace\s*\(\s*\/\[\\\\r\\\\n\]/i, // CRLF removal (escaped)
736
+ /allowedTypes\.includes\s*\(/i, // Content-type allowlist
737
+ /allowed(?:Headers|Types|Values)\.includes\s*\(/i, // Generic allowlist
738
+ /if\s*\(\s*allowed\w*\.includes\s*\(/i, // Conditional allowlist
739
+ /crypto\.random/i, // Server-generated value (not AI)
740
+ /randomUUID/i, // UUID generation
741
+ /safeValue|sanitized/i, // Variable indicating sanitization
742
+ /\/\^?\[a-zA-Z0-9\-_\.\s\]\+\$?\/.*\.test\s*\(/i, // Regex validation (alphanumeric chars only)
743
+ /if\s*\(\/\^?\[a-zA-Z0-9/i, // Conditional with alphanumeric regex
744
+ ]
745
+
746
+ return headerSanitizationPatterns.some(p => p.test(context))
747
+ }
748
+
749
+ /**
750
+ * Check for Python-specific safe patterns
751
+ */
752
+ function isPythonSafe(lineContent: string, surroundingContext: string): boolean {
753
+ const fullContext = lineContent + '\n' + surroundingContext
754
+
755
+ const pythonSafePatterns = [
756
+ /ast\.literal_eval\s*\(/i, // Safe literal evaluation
757
+ /yaml\.(?:safe_load|SafeLoader)/i, // Safe YAML
758
+ /yaml\.load\s*\([^)]*Loader\s*=\s*yaml\.SafeLoader/i, // Explicit SafeLoader
759
+ /cursor\.execute\s*\([^,]+,\s*\[/i, // Parameterized query with list
760
+ /\?\s*,\s*\[/i, // SQL placeholder with params
761
+ /%s.*,\s*\[/i, // Python %s placeholder with list
762
+ /subprocess\.run\s*\(\s*\[/i, // subprocess with list (no shell)
763
+ /shell\s*=\s*False/i, // Explicit shell=False
764
+ ]
765
+
766
+ return pythonSafePatterns.some(p => p.test(fullContext))
767
+ }
768
+
769
+ /**
770
+ * Check if SQL is using parameterized queries or ORM
771
+ */
772
+ function isSQLParameterized(lineContent: string, surroundingContext: string): boolean {
773
+ const fullContext = lineContent + '\n' + surroundingContext
774
+
775
+ const parameterizedPatterns = [
776
+ /allowedColumns\.filter\s*\(/i, // Column allowlist
777
+ /safeColumns/i, // Safe column variable
778
+ /allowedColumns\.includes\s*\(/i, // Column allowlist check
779
+ /\.filter\s*\(\s*\w+\s*=>\s*allowed\w*\.includes/i, // Filter with allowlist
780
+ /schema\.parse\s*\(/i, // Zod schema validation
781
+ /z\.enum\s*\(\s*\[/i, // Zod enum (allowlist)
782
+ /prisma\.\w+\.(?:findMany|findUnique|create|update)/i, // Prisma ORM methods (not raw)
783
+ /\$\{.*\}.*WHERE.*=\s*\$\d/i, // Dynamic column but parameterized value
784
+ ]
785
+
786
+ return parameterizedPatterns.some(p => p.test(fullContext))
787
+ }
788
+
789
+ /**
790
+ * Check if shell execution uses allowlist
791
+ */
792
+ function isShellAllowlisted(content: string, lineNumber: number): boolean {
793
+ const lines = content.split('\n')
794
+ const contextStart = Math.max(0, lineNumber - 15)
795
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
796
+ const context = lines.slice(contextStart, contextEnd).join('\n')
797
+
798
+ const shellAllowlistPatterns = [
799
+ /allowedArgs\.includes\s*\(/i, // Argument allowlist
800
+ /if\s*\(\s*allowedArgs\.includes/i, // Conditional on allowlist
801
+ /allowedCommands\.includes\s*\(/i, // Command allowlist
802
+ /execFile\s*\(\s*['"][^'"]+['"]/i, // execFile with hardcoded command (safe)
803
+ /\.replace\s*\(\s*\/\[^a-z0-9\]/gi, // Strict sanitization
804
+ /sanitized\s*=/i, // Sanitization variable
805
+ ]
806
+
807
+ return shellAllowlistPatterns.some(p => p.test(context))
808
+ }
809
+
810
+ /**
811
+ * Check if dynamic import uses allowlist
812
+ */
813
+ function isImportAllowlisted(content: string, lineNumber: number): boolean {
814
+ const lines = content.split('\n')
815
+ const contextStart = Math.max(0, lineNumber - 15)
816
+ const contextEnd = Math.min(lines.length, lineNumber + 5)
817
+ const context = lines.slice(contextStart, contextEnd).join('\n')
818
+
819
+ const importAllowlistPatterns = [
820
+ /ALLOWED_PLUGINS\s*[=:]/i, // Plugin allowlist
821
+ /importMap\s*[=:]/i, // Import map object
822
+ /allowedModules/i, // Module allowlist
823
+ /if\s*\(\s*\w+\s+in\s+importMap\)/i, // Key in import map
824
+ /if\s*\(\s*loader\)/i, // Loader function check (from allowlist)
825
+ /\[aiModule\]\s*$/i, // Array access into known object (allowlist lookup)
826
+ ]
827
+
828
+ return importAllowlistPatterns.some(p => p.test(context))
829
+ }
830
+
404
831
  // ============================================================================
405
832
  // Main Detection Function
406
833
  // ============================================================================
@@ -529,12 +956,86 @@ export function detectAIExecutionSinks(
529
956
  const isSandboxed = isSandboxedExecution(content, lineNumber)
530
957
  const hasValidation = hasOutputValidation(content, lineNumber)
531
958
 
959
+ // ===== SINK-SPECIFIC VALIDATION CHECKS =====
960
+
961
+ // Phase 2: Check for URL validation on network/redirect sinks (SSRF, Open Redirect)
962
+ const isNetworkSink = pattern.name.includes('fetch') || pattern.name.includes('axios') ||
963
+ pattern.name.includes('HTTP') || pattern.name.includes('redirect') ||
964
+ pattern.name.includes('location') || pattern.name.includes('got')
965
+ if (isNetworkSink) {
966
+ const urlValidLevel = getURLValidationLevel(content, lineNumber)
967
+ if (urlValidLevel === 'strong') {
968
+ continue // Skip - strong URL validation present
969
+ }
970
+ }
971
+
972
+ // Phase 3: Check for DOM sanitization on template_render sinks
973
+ const hasDOMSanitization = pattern.sinkType === 'template_render'
974
+ ? isDOMSanitized(lineContent, surroundingContext)
975
+ : false
976
+
977
+ // Skip DOM findings if sanitized
978
+ if (hasDOMSanitization && pattern.sinkType === 'template_render') {
979
+ continue
980
+ }
981
+
982
+ // Check for header sanitization
983
+ const isHeaderSink = pattern.name.includes('header') || pattern.name.includes('cookie') ||
984
+ pattern.name.includes('res.type')
985
+ if (isHeaderSink && isHeaderSanitized(content, lineNumber)) {
986
+ continue // Skip - header value is sanitized
987
+ }
988
+
989
+ // Check for path validation on file system sinks
990
+ const isFileSink = pattern.name.includes('file path') || pattern.name.includes('fs operation') ||
991
+ pattern.name.includes('path.join')
992
+ if (isFileSink && isPathValidated(content, lineNumber)) {
993
+ continue // Skip - path is validated
994
+ }
995
+
996
+ // Check for SQL parameterization
997
+ const isSQLSink = pattern.sinkType === 'sql_builder'
998
+ if (isSQLSink && isSQLParameterized(lineContent, surroundingContext)) {
999
+ continue // Skip - SQL is parameterized or uses allowlist
1000
+ }
1001
+
1002
+ // Check for shell allowlist
1003
+ const isShellSink = pattern.sinkType === 'shell_command'
1004
+ if (isShellSink && isShellAllowlisted(content, lineNumber)) {
1005
+ continue // Skip - shell command uses allowlist
1006
+ }
1007
+
1008
+ // Check for import allowlist
1009
+ const isImportSink = pattern.name.includes('import') || pattern.name.includes('require')
1010
+ if (isImportSink && isImportAllowlisted(content, lineNumber)) {
1011
+ continue // Skip - import uses allowlist
1012
+ }
1013
+
1014
+ // Check for Python-specific safe patterns
1015
+ const isPythonSink = pattern.name.includes('Python') || pattern.name.includes('pickle') ||
1016
+ pattern.name.includes('subprocess') || pattern.name.includes('os.system')
1017
+ if (isPythonSink && isPythonSafe(lineContent, surroundingContext)) {
1018
+ continue // Skip - Python code uses safe patterns
1019
+ }
1020
+
1021
+ // Check for ast.literal_eval (Python safe eval) - this is a safe alternative to eval()
1022
+ // It matches the eval pattern because literal_eval contains "eval("
1023
+ if (pattern.name.includes('eval') && /ast\.literal_eval\s*\(/i.test(lineContent)) {
1024
+ continue // Skip - ast.literal_eval is safe, only evaluates literals
1025
+ }
1026
+
1027
+ // Check URL validation level for severity adjustment
1028
+ const hasURLValid = isNetworkSink ? getURLValidationLevel(content, lineNumber) !== 'none' : false
1029
+
1030
+ // Combine validation checks (URL validation counts as validation for network sinks)
1031
+ const effectiveValidation = hasValidation || hasURLValid
1032
+
532
1033
  // Calculate final severity
533
1034
  const severity = calculateSeverity(
534
1035
  pattern.baseSeverity,
535
1036
  pattern.sinkType,
536
1037
  isSandboxed,
537
- hasValidation,
1038
+ effectiveValidation,
538
1039
  isTestFile,
539
1040
  isExample,
540
1041
  isLibrary
@@ -548,6 +1049,9 @@ export function detectAIExecutionSinks(
548
1049
  if (hasValidation) {
549
1050
  description += ' (Some validation detected nearby.)'
550
1051
  }
1052
+ if (hasURLValid && !hasValidation) {
1053
+ description += ' (URL validation detected nearby.)'
1054
+ }
551
1055
  if (isTestFile) {
552
1056
  description += ' (In test file.)'
553
1057
  } else if (isExample) {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { Vulnerability, VulnerabilitySeverity } from '../types'
7
- import { isExampleFile, isTestOrMockFile, isPlaceholderValue } from '../utils/context-helpers'
7
+ import { isExampleFile, isTestOrMockFile, isPlaceholderValue, isScannerOrFixtureFile } from '../utils/context-helpers'
8
8
 
9
9
  interface AIFingerprint {
10
10
  name: string
@@ -635,6 +635,10 @@ export function detectAIFingerprints(
635
635
  filePath: string
636
636
  ): Vulnerability[] {
637
637
  const vulnerabilities: Vulnerability[] = []
638
+
639
+ // Skip scanner/fixture files to avoid self-detection
640
+ if (isScannerOrFixtureFile(filePath)) return vulnerabilities
641
+
638
642
  const lines = content.split('\n')
639
643
 
640
644
  // Skip example/demo files entirely - they contain placeholder code by design