@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,1948 +0,0 @@
1
- "use strict";
2
- /**
3
- * Layer 3: AI Semantic Analysis
4
- * Uses Claude to perform deep security analysis including:
5
- * - Taint analysis (data flow from sources to sinks)
6
- * - Business logic flaw detection
7
- * - Missing authorization checks
8
- * - Cryptography validation
9
- * - Data exposure detection
10
- * - Framework-specific deep analysis
11
- */
12
- var __importDefault = (this && this.__importDefault) || function (mod) {
13
- return (mod && mod.__esModule) ? mod : { "default": mod };
14
- };
15
- Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.applyAutoDismissRules = applyAutoDismissRules;
17
- exports.analyzeWithAI = analyzeWithAI;
18
- exports.batchAnalyzeWithAI = batchAnalyzeWithAI;
19
- exports.validateFindingsWithAI = validateFindingsWithAI;
20
- const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
21
- const openai_1 = __importDefault(require("openai"));
22
- const context_helpers_1 = require("../utils/context-helpers");
23
- const project_context_builder_1 = require("../utils/project-context-builder");
24
- // Import tier system for tier-aware auto-dismiss
25
- const tiers_1 = require("../tiers");
26
- // ============================================================================
27
- // Path Normalization Helpers (for AI response path matching)
28
- // ============================================================================
29
- /**
30
- * Normalize a file path for comparison purposes.
31
- * Handles common variations: ./src/file.ts, src/file.ts, /src/file.ts
32
- */
33
- function normalizePathForComparison(path) {
34
- return path
35
- .replace(/^\.\//, '') // Remove leading ./
36
- .replace(/^\//, '') // Remove leading /
37
- .replace(/\\/g, '/'); // Normalize Windows backslashes
38
- }
39
- /**
40
- * Find a matching file path from expected paths, handling path format variations.
41
- * AI responses may use different path formats than what we sent.
42
- */
43
- function findMatchingFilePath(responsePath, expectedPaths) {
44
- // Exact match first
45
- if (expectedPaths.includes(responsePath))
46
- return responsePath;
47
- // Normalized match
48
- const normalized = normalizePathForComparison(responsePath);
49
- for (const expected of expectedPaths) {
50
- if (normalizePathForComparison(expected) === normalized) {
51
- console.log(`[AI Validation] Path fuzzy matched: "${responsePath}" -> "${expected}"`);
52
- return expected;
53
- }
54
- }
55
- // Basename match (only if unique) - handles cases like "file.ts" matching "src/api/file.ts"
56
- const basename = responsePath.split('/').pop() || responsePath;
57
- const matches = expectedPaths.filter(p => (p.split('/').pop() || p) === basename);
58
- if (matches.length === 1) {
59
- console.log(`[AI Validation] Path basename matched: "${responsePath}" -> "${matches[0]}"`);
60
- return matches[0];
61
- }
62
- return null;
63
- }
64
- // ============================================================================
65
- // Phase 2: Multi-File Batching Configuration
66
- // ============================================================================
67
- // Number of files to include in each API call (Phase 2 optimization)
68
- // Batching multiple files reduces API overhead and leverages prompt caching better
69
- const FILES_PER_API_BATCH = 8;
70
- // Number of API batches to process in parallel (Phase 3 optimization)
71
- // Higher values = faster scans but more API load; OpenAI/GPT-5-mini handles this well
72
- // Increased from 4 to 6 for better throughput on large codebases
73
- const PARALLEL_API_BATCHES = 6;
74
- // Initialize Anthropic client
75
- function getAnthropicClient() {
76
- const apiKey = process.env.ANTHROPIC_API_KEY;
77
- if (!apiKey) {
78
- throw new Error('ANTHROPIC_API_KEY environment variable is not set');
79
- }
80
- return new sdk_1.default({ apiKey });
81
- }
82
- // Initialize OpenAI client
83
- let openaiClient = null;
84
- function getOpenAIClient() {
85
- if (!openaiClient) {
86
- const apiKey = process.env.OPENAI_API_KEY;
87
- if (!apiKey) {
88
- throw new Error('OPENAI_API_KEY environment variable is not set');
89
- }
90
- openaiClient = new openai_1.default({ apiKey });
91
- }
92
- return openaiClient;
93
- }
94
- // GPT-5-mini pricing constants (per 1M tokens)
95
- const GPT5_MINI_PRICING = {
96
- input: 0.25, // $0.25 per 1M tokens
97
- cached: 0.025, // $0.025 per 1M tokens (10% of input)
98
- output: 2.00, // $2.00 per 1M tokens
99
- };
100
- const AUTO_DISMISS_RULES = [
101
- // Test files - often contain intentional "vulnerable" patterns for testing
102
- {
103
- name: 'test_file',
104
- check: (finding) => (0, context_helpers_1.isTestOrMockFile)(finding.filePath),
105
- reason: 'Finding in test/mock file',
106
- },
107
- // Example/demo code - not production code
108
- {
109
- name: 'example_file',
110
- check: (finding) => (0, context_helpers_1.isExampleFile)(finding.filePath),
111
- reason: 'Finding in example/demo file',
112
- },
113
- // Documentation files
114
- {
115
- name: 'documentation_file',
116
- check: (finding) => /\.(md|mdx|txt|rst)$/i.test(finding.filePath),
117
- reason: 'Finding in documentation file',
118
- },
119
- // Scanner/security tool code itself
120
- {
121
- name: 'scanner_code',
122
- check: (finding) => (0, context_helpers_1.isScannerOrFixtureFile)(finding.filePath),
123
- reason: 'Finding in scanner/fixture code',
124
- },
125
- // Environment variable references (not hardcoded secrets)
126
- {
127
- name: 'env_var_reference',
128
- check: (finding) => {
129
- if (finding.category !== 'hardcoded_secret' && finding.category !== 'high_entropy_string') {
130
- return false;
131
- }
132
- return (0, context_helpers_1.isEnvVarReference)(finding.lineContent);
133
- },
134
- reason: 'Uses environment variable (not hardcoded)',
135
- },
136
- // Public health check endpoints don't need auth
137
- {
138
- name: 'health_check_endpoint',
139
- check: (finding) => {
140
- if (finding.category !== 'missing_auth')
141
- return false;
142
- return (0, context_helpers_1.isPublicEndpoint)(finding.lineContent, finding.filePath);
143
- },
144
- reason: 'Public health check endpoint (auth not required)',
145
- },
146
- // CSS/Tailwind classes flagged as high entropy
147
- {
148
- name: 'css_classes',
149
- check: (finding) => {
150
- if (finding.category !== 'high_entropy_string')
151
- return false;
152
- const cssIndicators = ['flex', 'grid', 'text-', 'bg-', 'px-', 'py-', 'rounded', 'shadow', 'hover:', 'dark:'];
153
- const lowerLine = finding.lineContent.toLowerCase();
154
- const matchCount = cssIndicators.filter(ind => lowerLine.includes(ind)).length;
155
- return matchCount >= 2;
156
- },
157
- reason: 'CSS/Tailwind classes (not a secret)',
158
- },
159
- // Comment lines shouldn't be flagged for most categories
160
- {
161
- name: 'comment_line',
162
- check: (finding) => {
163
- // Some categories are valid in comments (e.g., TODO security)
164
- if (finding.category === 'ai_pattern')
165
- return false;
166
- return (0, context_helpers_1.isComment)(finding.lineContent);
167
- },
168
- reason: 'Code comment (not executable)',
169
- },
170
- // Info severity already - no need to validate
171
- // BUT: Only auto-dismiss info-severity for Tier A (core) findings
172
- // Tier B (ai_assisted) findings MUST go through AI validation even at info severity
173
- // because detectors may have pre-downgraded them based on partial context
174
- {
175
- name: 'info_severity_core_only',
176
- check: (finding) => {
177
- if (finding.severity !== 'info')
178
- return false;
179
- // Only auto-dismiss info-severity for Tier A (core) findings
180
- // Tier B should always go through AI for proper validation
181
- const tier = (0, tiers_1.getTierForCategory)(finding.category, finding.layer);
182
- return tier === 'core';
183
- },
184
- reason: 'Already info severity for core detector (low priority)',
185
- },
186
- // Generic success/error messages in ai_pattern
187
- {
188
- name: 'generic_message',
189
- check: (finding) => {
190
- if (finding.category !== 'ai_pattern')
191
- return false;
192
- const genericPatterns = [
193
- /['"`](success|done|ok|completed|finished|saved|updated|deleted|created)['"`]/i,
194
- /['"`]something went wrong['"`]/i,
195
- /['"`]an error occurred['"`]/i,
196
- /console\.(log|info|debug)\s*\(\s*['"`][^'"]+['"`]\s*\)/i,
197
- ];
198
- return genericPatterns.some(p => p.test(finding.lineContent));
199
- },
200
- reason: 'Generic UI message (not security-relevant)',
201
- },
202
- // Type definitions with 'any' - often necessary for third-party libs
203
- {
204
- name: 'type_definition_any',
205
- check: (finding) => {
206
- if (finding.category !== 'ai_pattern')
207
- return false;
208
- if (!finding.title.toLowerCase().includes('any'))
209
- return false;
210
- // Check if it's in a .d.ts file or type definition context
211
- if (finding.filePath.includes('.d.ts'))
212
- return true;
213
- const typeDefPatterns = [/^type\s+\w+\s*=/, /^interface\s+\w+/, /declare\s+(const|let|var|function|class)/];
214
- return typeDefPatterns.some(p => p.test(finding.lineContent.trim()));
215
- },
216
- reason: 'Type definition (not runtime code)',
217
- },
218
- // setTimeout/setInterval magic numbers - code style, not security
219
- {
220
- name: 'timeout_magic_number',
221
- check: (finding) => {
222
- if (finding.category !== 'ai_pattern')
223
- return false;
224
- return /set(Timeout|Interval)\s*\([^,]+,\s*\d+\s*\)/.test(finding.lineContent);
225
- },
226
- reason: 'Timeout value (code style, not security)',
227
- },
228
- ];
229
- /**
230
- * Apply smart auto-dismiss rules to filter obvious false positives
231
- * Returns findings that should be sent to AI validation
232
- */
233
- function applyAutoDismissRules(findings) {
234
- const toValidate = [];
235
- const dismissed = [];
236
- for (const finding of findings) {
237
- let wasDismissed = false;
238
- for (const rule of AUTO_DISMISS_RULES) {
239
- if (rule.check(finding)) {
240
- dismissed.push({
241
- finding,
242
- rule: rule.name,
243
- reason: rule.reason,
244
- });
245
- wasDismissed = true;
246
- break;
247
- }
248
- }
249
- if (!wasDismissed) {
250
- toValidate.push(finding);
251
- }
252
- }
253
- return { toValidate, dismissed };
254
- }
255
- // ============================================================================
256
- // Security Analysis Prompt (Layer 3)
257
- // ============================================================================
258
- // System prompt for security analysis
259
- const SECURITY_ANALYSIS_PROMPT = `You are an expert security code reviewer. Analyze the provided code for security vulnerabilities.
260
-
261
- Focus on these specific vulnerability types:
262
-
263
- 1. **Taint Analysis (Data Flow)**
264
- - Track user input from sources (req.query, req.params, req.body, searchParams, URL parameters)
265
- - To dangerous sinks (eval, dangerouslySetInnerHTML, exec, SQL queries, file operations)
266
- - Flag any path where untrusted data reaches a dangerous function without sanitization
267
-
268
- 2. **SQL Injection**
269
- - String concatenation in SQL queries
270
- - Template literals with user input in queries
271
- - Missing parameterized queries
272
-
273
- 3. **XSS (Cross-Site Scripting)**
274
- - User input rendered without escaping
275
- - dangerouslySetInnerHTML with user data
276
- - innerHTML assignments
277
- - NOTE: React/Next.js JSX automatically escapes content, so {variable} in JSX is NOT XSS
278
-
279
- 4. **Command Injection**
280
- - exec, spawn, execSync with user input
281
- - Shell command construction with variables
282
-
283
- 5. **Missing Authorization**
284
- - API routes that modify data without auth checks
285
- - Database writes in GET handlers
286
- - Missing permission checks before sensitive operations
287
-
288
- 6. **Insecure Deserialization**
289
- - JSON.parse on untrusted data without validation
290
- - eval of serialized data
291
-
292
- 7. **Cryptography Validation**
293
- - Weak algorithms: MD5 (for security), SHA1 (for security), DES, RC4
294
- - Insecure random: Math.random() for tokens/keys/secrets
295
- - Hardcoded encryption keys or IVs (not from env vars)
296
- - ECB mode usage (patterns indicate cipher mode)
297
- - Low iteration counts for PBKDF2 (< 10000)
298
- - Short key lengths (< 256 bits for symmetric)
299
- - Missing salt for password hashing
300
- - createCipher() instead of createCipheriv()
301
-
302
- 8. **Data Exposure Detection**
303
- - Logging sensitive data: console.log with passwords, tokens, secrets, API keys
304
- - Stack traces exposed to clients: err.stack in response
305
- - Returning entire user objects (may include password hash)
306
- - Debug endpoints left in code: /debug, /test, /_internal routes
307
- - Verbose error messages exposing internal details
308
- - Sensitive data in error responses
309
-
310
- 9. **Framework-Specific Security**
311
-
312
- **Next.js:**
313
- - Server actions ('use server') without authentication
314
- - Client components ('use client') accessing non-NEXT_PUBLIC_ env vars
315
- - Middleware that returns NextResponse.next() without auth checks
316
- - getServerSideProps without session validation
317
- - Exposed API routes without rate limiting
318
-
319
- **React:**
320
- - Sensitive data stored in useState (visible in devtools)
321
- - dangerouslySetInnerHTML with props/state
322
- - useEffect making authenticated API calls without token validation
323
-
324
- **Express:**
325
- - Missing helmet() middleware for security headers
326
- - CORS with origin: "*" in production
327
- - Missing body-parser limits (DoS risk)
328
- - Trust proxy without verification
329
- - Error handlers exposing stack traces
330
-
331
- IMPORTANT - DO NOT FLAG THESE AS VULNERABILITIES (common false positives):
332
-
333
- **Framework Patterns (Safe by Design):**
334
- - Next.js middleware using request.url for redirects (standard pattern)
335
- - React/Next.js JSX rendering variables like {user.name} (auto-escaped by React)
336
- - Supabase/Firebase client creation with NEXT_PUBLIC_ environment variables
337
- - Using headers().get('host') in Next.js server actions
338
-
339
- **Data Handling (Low Risk):**
340
- - JSON.parse on data from YOUR OWN database (the app wrote it, it's trusted). Do NOT report this as a vulnerability. At most, you may mention an info-level robustness note if there is no error handling, but generally you should omit it.
341
- - JSON.parse on localStorage data (same-origin, XSS is a separate issue). This is also not a security vulnerability. At most, you may suggest an info-level robustness improvement, and usually it is not worth mentioning.
342
- - Passing user's own data to external APIs (user embedding their own content).
343
- - Error messages that use error.message in catch blocks or are returned to the client as a generic error string are standard error handling. Treat them as LOW/INFO hardening at most, and DO NOT mark them as medium/high unless the message clearly includes credentials, secrets, or full stack traces.
344
- - Generic configuration or feature messages like "OpenAI API key not configured" or "service disabled" are operational information, not security vulnerabilities. Treat them as info at most, or ignore them.
345
-
346
- **Authentication Patterns (Context Matters):**
347
- - Internal server-side functions only called from trusted code paths (OAuth callbacks, etc.)
348
- - Functions with userId parameters called with session.user.id from authenticated contexts
349
- - Service role keys used in server-side code with proper auth checks elsewhere
350
- - API routes that call getCurrentUserId() and use the result (the auth check IS the userId call)
351
-
352
- **BYOK (Bring Your Own Key) Patterns:**
353
- - User-provided API keys in BYOK mode are INTENTIONAL - the user wants to use their own key
354
- - This is a feature, not a vulnerability - don't flag it unless there's actual abuse potential
355
- - When a BYOK key is only used TRANSIENTLY in memory for a single provider call (and is never logged or stored), and the route is authenticated, do NOT report this as a medium/high vulnerability. At most, you may surface a low/info note reminding the developer not to log or persist keys.
356
- - Frontend components sending a BYOK key to an authenticated backend endpoint for one-shot use are expected behavior, not a vulnerability. Do NOT flag these as data_exposure or dangerous_function unless the key is logged, stored, or echoed back to the client.
357
- - Only raise medium/high BYOK findings when keys are clearly stored (e.g., written to a database or long-term logs), logged in plaintext, or accepted by unauthenticated endpoints that attackers could abuse at scale.
358
-
359
- **What TO Flag (Real Vulnerabilities):**
360
- - SQL string concatenation with user input
361
- - eval() or Function() with user-controlled strings
362
- - Missing auth checks where sensitive data could be accessed by wrong user
363
- - Actual hardcoded secrets (real API keys, not env var references)
364
- - Command injection (exec/spawn with user input)
365
-
366
- Respond ONLY with a JSON array of findings. Each finding must have:
367
- {
368
- "lineNumber": <number>,
369
- "severity": "critical" | "high" | "medium" | "low",
370
- "category": "sql_injection" | "xss" | "command_injection" | "missing_auth" | "dangerous_function",
371
- "title": "<short title>",
372
- "description": "<detailed explanation of the vulnerability>",
373
- "suggestedFix": "<how to fix it>"
374
- }
375
-
376
- If no vulnerabilities are found, return an empty array: []
377
-
378
- CRITICAL: Only report REAL vulnerabilities with HIGH confidence. Be conservative - it's better to miss a low-confidence issue than to report false positives. The code is likely using modern frameworks with built-in protections.`;
379
- /**
380
- * Build auth context string for AI prompt
381
- */
382
- function buildAuthContextForPrompt(ctx) {
383
- if (!ctx)
384
- return '';
385
- const parts = [];
386
- if (ctx.middlewareConfig?.hasAuthMiddleware) {
387
- parts.push(`**IMPORTANT AUTH CONTEXT**: This project uses ${ctx.middlewareConfig.authType || 'auth'} middleware.`);
388
- if (ctx.middlewareConfig.protectedPaths.length > 0) {
389
- parts.push(`Protected paths: ${ctx.middlewareConfig.protectedPaths.join(', ')}`);
390
- }
391
- else {
392
- parts.push('All /api/** routes are protected by default.');
393
- }
394
- parts.push('Routes under these paths are ALREADY AUTHENTICATED - do NOT flag them as "missing auth".');
395
- parts.push('Client components calling these protected API routes are also safe - the backend handles auth.');
396
- }
397
- if (ctx.authHelpers?.hasThrowingHelpers) {
398
- parts.push('');
399
- parts.push('**AUTH HELPER FUNCTIONS**: This project uses throwing auth helpers that guarantee authenticated context:');
400
- parts.push(ctx.authHelpers.summary);
401
- parts.push('Code after these helper calls is GUARANTEED to be authenticated. Do NOT flag "missing auth" after these calls.');
402
- }
403
- if (ctx.additionalContext) {
404
- parts.push('');
405
- parts.push(ctx.additionalContext);
406
- }
407
- return parts.length > 0 ? '\n\n' + parts.join('\n') : '';
408
- }
409
- async function analyzeWithAI(file, context) {
410
- const client = getAnthropicClient();
411
- // Prepare the code with line numbers for reference
412
- const numberedCode = file.content
413
- .split('\n')
414
- .map((line, i) => `${i + 1}: ${line}`)
415
- .join('\n');
416
- // Build auth context for the prompt
417
- const authContext = buildAuthContextForPrompt(context);
418
- const userMessage = `Analyze this ${file.language} file for security vulnerabilities:
419
-
420
- File: ${file.path}${authContext}
421
-
422
- \`\`\`${file.language}
423
- ${numberedCode}
424
- \`\`\`
425
-
426
- Return ONLY a JSON array of findings.`;
427
- try {
428
- const response = await client.messages.create({
429
- model: 'claude-3-5-haiku-20241022',
430
- max_tokens: 4096,
431
- system: SECURITY_ANALYSIS_PROMPT,
432
- messages: [
433
- {
434
- role: 'user',
435
- content: userMessage,
436
- },
437
- ],
438
- });
439
- // Extract text content from response
440
- const textContent = response.content.find((block) => block.type === 'text');
441
- if (!textContent || textContent.type !== 'text') {
442
- console.error('No text content in AI response');
443
- return [];
444
- }
445
- // Parse the JSON response
446
- const findings = parseAIResponse(textContent.text);
447
- // Convert to Vulnerability format
448
- return findings.map((finding, index) => ({
449
- id: `ai-${file.path}-${finding.lineNumber}-${index}`,
450
- filePath: file.path,
451
- lineNumber: finding.lineNumber,
452
- lineContent: getLineContent(file.content, finding.lineNumber),
453
- severity: finding.severity,
454
- category: finding.category,
455
- title: finding.title,
456
- description: finding.description,
457
- suggestedFix: finding.suggestedFix,
458
- confidence: 'high',
459
- layer: 3,
460
- }));
461
- }
462
- catch (error) {
463
- console.error('AI analysis error:', error);
464
- return [];
465
- }
466
- }
467
- // Parse the AI response JSON
468
- function parseAIResponse(response) {
469
- try {
470
- // Try to extract JSON from the response
471
- const jsonMatch = response.match(/\[[\s\S]*\]/);
472
- if (!jsonMatch) {
473
- return [];
474
- }
475
- const parsed = JSON.parse(jsonMatch[0]);
476
- // Validate the structure
477
- if (!Array.isArray(parsed)) {
478
- return [];
479
- }
480
- return parsed.filter(item => typeof item.lineNumber === 'number' &&
481
- typeof item.severity === 'string' &&
482
- typeof item.category === 'string' &&
483
- typeof item.title === 'string' &&
484
- typeof item.description === 'string').map(item => ({
485
- lineNumber: item.lineNumber,
486
- severity: validateSeverity(item.severity),
487
- category: validateCategory(item.category),
488
- title: item.title,
489
- description: item.description,
490
- suggestedFix: item.suggestedFix || 'Review and fix the security issue',
491
- }));
492
- }
493
- catch (error) {
494
- console.error('Failed to parse AI response:', error);
495
- return [];
496
- }
497
- }
498
- function validateSeverity(severity) {
499
- const valid = ['critical', 'high', 'medium', 'low'];
500
- return valid.includes(severity)
501
- ? severity
502
- : 'medium';
503
- }
504
- function validateCategory(category) {
505
- const valid = [
506
- 'sql_injection', 'xss', 'command_injection', 'missing_auth',
507
- 'dangerous_function', 'hardcoded_secret', 'high_entropy_string',
508
- 'sensitive_variable', 'security_bypass', 'insecure_config',
509
- 'suspicious_package', 'cors_misconfiguration', 'root_container',
510
- 'weak_crypto', 'sensitive_url', 'ai_pattern', 'dangerous_file',
511
- 'data_exposure', // NEW: for logging/exposing sensitive data
512
- ];
513
- return valid.includes(category)
514
- ? category
515
- : 'dangerous_function';
516
- }
517
- function getLineContent(content, lineNumber) {
518
- const lines = content.split('\n');
519
- return lines[lineNumber - 1]?.trim() || '';
520
- }
521
- // Batch analyze multiple files (with rate limiting)
522
- async function batchAnalyzeWithAI(files, context, maxConcurrent = 3) {
523
- const vulnerabilities = [];
524
- // Process files in batches to avoid rate limits
525
- for (let i = 0; i < files.length; i += maxConcurrent) {
526
- const batch = files.slice(i, i + maxConcurrent);
527
- const results = await Promise.all(batch.map(file => analyzeWithAI(file, context).catch(err => {
528
- console.error(`AI analysis failed for ${file.path}:`, err);
529
- return [];
530
- })));
531
- vulnerabilities.push(...results.flat());
532
- // Small delay between batches to avoid rate limits
533
- if (i + maxConcurrent < files.length) {
534
- await new Promise(resolve => setTimeout(resolve, 500));
535
- }
536
- }
537
- return vulnerabilities;
538
- }
539
- // ============================================================================
540
- // High-Context Validation Prompt (Section 3 Generalised Rules)
541
- // ============================================================================
542
- /**
543
- * This prompt encodes the generalised security rules from CURRENTTASK.md Section 3.
544
- * It is designed to work with full-file content and project context.
545
- */
546
- const HIGH_CONTEXT_VALIDATION_PROMPT = `You are an expert security code reviewer acting as a "Second-opinion AI Reviewer" for vulnerability findings from an automated scanner.
547
-
548
- Your PRIMARY task: AGGRESSIVELY REJECT false positives and marginal findings. Only keep findings that are clearly exploitable or represent real security risk.
549
-
550
- **CORE PHILOSOPHY**: A professional scanner should surface very few, high-confidence findings. When in doubt, REJECT the finding or downgrade to info.
551
-
552
- ## Input Format
553
- You will receive:
554
- 1. **Project Context** - Architectural information about auth, data access, and secrets handling
555
- 2. **Full File Content** - The entire file with line numbers
556
- 3. **Candidate Findings** - List of potential vulnerabilities to validate
557
-
558
- ## Core Validation Principles
559
-
560
- ### 3.1 Authentication & Access Control
561
- Recognise these SAFE patterns (downgrade to info or REJECT entirely):
562
- - **Middleware-protected routes**: If project context shows auth middleware (Clerk, NextAuth, Auth0, custom), routes under protected paths are ALREADY GUARDED - do NOT flag as missing auth
563
- - **Auth helper functions that THROW**: Functions like getCurrentUserId(), getSession(), auth() that throw/abort on missing auth guarantee authenticated context. Code AFTER these calls is authenticated.
564
- - Do NOT suggest "if (!userId)" checks after calling throwing helpers - the check is redundant
565
- - If helper throws, it returns Promise<string> not Promise<string|null> - userId is guaranteed non-null
566
- - Common throwing helpers: getCurrentUserId(), requireAuth(), getUser(), auth().protect(), getSession() with throw
567
- - **User-scoped queries**: Database queries filtered by user_id/tenant_id from authenticated session
568
- - **Guard patterns**: Early returns or throws when auth fails (if (!user) return/throw)
569
-
570
- Flag as REAL vulnerability (keep high severity) ONLY when:
571
- - Route has no visible auth check AND is NOT covered by middleware AND has no throwing auth helper
572
- - Sensitive operations without user scoping (cross-tenant access possible)
573
- - Auth checks that can be bypassed (e.g., checking wrong variable)
574
-
575
- **CRITICAL CONTRADICTION HANDLING**:
576
- - If we detect both "protected by middleware" and "missing auth" on the same route - REJECT the "missing auth" finding
577
- - If we detect both "uses throwing auth helper" and "missing auth" - REJECT the "missing auth" finding
578
- - Client components calling these protected API routes should NOT be flagged for "missing auth"
579
- - Adding "if (!userId)" after a throwing helper is a FALSE POSITIVE - reject it
580
-
581
- ### 3.2 Deserialization & Unsafe Parsing
582
- Distinguish by INPUT ORIGIN and error handling:
583
- - **Application-controlled data** (database, config, localStorage): Low risk - downgrade to info
584
- - JSON.parse on data YOUR app wrote is trusted
585
- - Failures affect robustness, not security
586
- - If ALSO wrapped in try-catch: REJECT the finding entirely
587
- - **External/untrusted data** (HTTP request body, URL params): Higher risk
588
- - With try-catch: downgrade to low, suggest SCHEMA VALIDATION (zod/joi/yup) not more try-catch
589
- - Without try-catch: keep as medium, suggest both try-catch AND schema validation
590
- - **request.json() / req.json()**: NOT a dangerous function
591
- - This is the standard way to parse request bodies in modern frameworks
592
- - Only suggest schema validation if none is visible nearby
593
- - Severity: info at most
594
-
595
- **CRITICAL JSON.parse RULES**:
596
- - Do NOT suggest "add try/catch" when JSON.parse is ALREADY inside a try-catch block - this creates contradictory advice
597
- - If JSON.parse is in try-catch with app-controlled data: REJECT the finding
598
- - Prefer suggesting schema validation over generic try-catch for user input
599
- - For sensitive sinks (DB writes, code execution): medium severity
600
- - For display-only uses: low/info severity
601
-
602
- ### 3.3 Logging & Error Handling
603
- Distinguish LOGS vs RESPONSES with this severity ladder:
604
-
605
- **Response Sinks (res.json, NextResponse.json, return) - Higher Risk:**
606
- - Full error object or stack trace in response → **HIGH severity**
607
- - Detailed internal fields (debug, trace, internal) → **MEDIUM severity**
608
- - error.message only or static error strings → **LOW/INFO severity** (this is the RECOMMENDED pattern)
609
-
610
- **Log Sinks (console.log, logger.info) - Lower Risk:**
611
- - Logging error objects for debugging → **INFO severity** (hygiene, not security)
612
- - Logging userId, query strings → **INFO severity** (privacy note)
613
- - Logging passwords/secrets → **MEDIUM+ severity**
614
- - JSON.stringify(error) in logs → **INFO severity**
615
-
616
- **CRITICAL ERROR HANDLING RULES**:
617
- - "error.message" in responses is usually SAFE and should NOT be HIGH severity
618
- - HIGH severity is ONLY for responses that expose stacks, internal fields, or raw error objects
619
- - Logging errors is STANDARD PRACTICE - don't flag it as a security issue unless it logs secrets
620
-
621
- ### 3.4 XSS vs Prompt Injection
622
- Keep these SEPARATE:
623
- - **XSS**: Writing untrusted data into DOM/HTML sinks without escaping
624
- - innerHTML with dynamic user data: flag as XSS
625
- - React JSX {variable}: NOT XSS (auto-escaped)
626
- - dangerouslySetInnerHTML with static content: info severity
627
- - **Prompt Injection**: User content in LLM prompts
628
- - NOT XSS - different threat model
629
- - Downgrade to low/info unless clear path to high-impact actions
630
- - Never label prompt issues as XSS
631
-
632
- ### 3.5 Secrets, BYOK, and External Services
633
- Distinguish these patterns:
634
- - **Hardcoded secrets**: Real API keys in code = critical/high
635
- - **Environment variables**: process.env.SECRET = safe (REJECT finding)
636
- - **BYOK (Bring Your Own Key)**: User provides their own key for AI services
637
- - This is a FEATURE, not a vulnerability
638
- - Distinguish TRANSIENT USE vs STORAGE:
639
- - Transient use (key in request body → API call → discarded): info severity, this is the IDEAL pattern
640
- - Storage (key saved to database): check for user-scoping and encryption
641
- - Severity ladder:
642
- - Authenticated + transient use: info (feature, not vuln)
643
- - Authenticated + user-scoped storage: low (suggest encryption at rest)
644
- - Unauthenticated: medium (cost/abuse risk)
645
- - Cross-tenant storage: medium (data isolation risk)
646
- - Do NOT describe transient BYOK keys as "stored without encryption" - they are NOT stored
647
-
648
- **Math.random() for Security:**
649
- Distinguish legitimate uses from security-critical misuse:
650
- - **Seed/Data Generation Files**: Files in /seed/, /fixtures/, /factories/, datacreator.ts, *.fixture.* are for test data generation
651
- - Math.random() in seed files is acceptable - these are never production security code
652
- - REJECT findings from seed/data generation files entirely
653
- - **Educational Vulnerability Files**: Files named insecurity.ts, vulnerable.ts, or in /intentionally-vulnerable/ paths
654
- - These are OWASP Juice Shop challenges or security training examples
655
- - REJECT entirely - they're intentionally vulnerable for educational purposes
656
- - **UUID/Identifier Generation**: Functions named generateUUID(), createId(), correlationId(), etc.
657
- - Use Math.random() for UI correlation, React keys, element IDs
658
- - Short toString(36).substring(2, 9) patterns are for UI correlation, NOT security tokens
659
- - REJECT unless function name explicitly indicates security (generateToken, createSessionId, generateSecret)
660
- - **CAPTCHA/Puzzle Generation**: Math.random() for CAPTCHA questions, puzzle difficulty, game mechanics
661
- - These don't need cryptographic randomness - legitimate non-security use
662
- - REJECT findings in CAPTCHA/puzzle generation functions
663
- - **Security-Sensitive Context**: Only keep as HIGH/CRITICAL when:
664
- - Variable names indicate security: token, secret, key, auth, session, password
665
- - Function names indicate security: generateToken, createSession, makeSecret
666
- - Used in security-critical files: auth.ts, crypto.ts, session.ts
667
- - Long toString() patterns without truncation (potential token generation)
668
-
669
- **Severity Ladder for Math.random():**
670
- - Seed/educational files: REJECT (not production code)
671
- - UUID/CAPTCHA functions: REJECT (legitimate use)
672
- - Short UI IDs (toString(36).substring(2, 9)): INFO (UI correlation, suggest crypto.randomUUID())
673
- - Business IDs: LOW (suggest crypto.randomUUID() for collision resistance)
674
- - Security contexts (tokens/secrets/keys): HIGH (cryptographic weakness)
675
- - Unknown context: MEDIUM (needs manual review)
676
-
677
- ### 3.6 DOM Sinks and Bootstrap Scripts
678
- Recognise LOW-RISK patterns:
679
- - Static scripts reading localStorage for theme/preferences
680
- - Setting attributes from config without user input
681
- - innerHTML with string literals only (no interpolation)
682
-
683
- Flag as REAL when:
684
- - User input flows to innerHTML/eval without sanitization
685
- - Template literals with \${userInput} in DOM sinks
686
-
687
- ### 3.7 AI/LLM-Specific Patterns
688
-
689
- **Prompt Injection (ai_prompt_injection):**
690
- - User input in system prompt WITHOUT delimiters (code fences, XML tags, separators) -> **HIGH** (real risk)
691
- - User input in system prompt WITH clear delimiters -> **INFO** (properly fenced)
692
- - Static prompts with no user interpolation -> **REJECT** (false positive)
693
- - Prompt templates using proper parameterization/placeholders -> **REJECT**
694
-
695
- **LLM Output Execution (ai_unsafe_execution):**
696
- - LLM output fed to eval()/Function()/exec() WITHOUT sandbox -> **CRITICAL** (arbitrary code execution)
697
- - LLM output to execution WITH sandbox (vm2, isolated-vm) -> **MEDIUM** (risk mitigated)
698
- - LLM output to execution WITH validation AND sandbox -> **LOW** (well-protected)
699
- - LLM output used for display only (console.log, UI) -> **REJECT** (not execution)
700
- - Generated SQL from LLM without parameterization -> **CRITICAL** (SQL injection)
701
- - Generated SQL with parameterized queries -> **MEDIUM** (logic may still be wrong)
702
-
703
- **Agent Tool Permissions (ai_overpermissive_tool):**
704
- - Tool with unrestricted file/network/exec access -> **HIGH** (overpermissive)
705
- - Tool without user context verification -> **MEDIUM** (missing authorization)
706
- - Tool with proper scoping, allowlists, and user verification -> **LOW** or **REJECT**
707
- - Test files with tool definitions -> **INFO** or **REJECT**
708
-
709
- **Hallucinated Dependencies (suspicious_package):**
710
- - Package not found in registry -> **CRITICAL** (likely AI-hallucinated name)
711
- - Very new package (less than 7 days old) with low downloads and typosquat pattern -> **HIGH**
712
- - Legitimate looking package with source/repo but low popularity -> **MEDIUM** (needs review)
713
- - Known legitimate package with unusual name (in allowlist) -> **REJECT**
714
-
715
- **CRITICAL AI PATTERN RULES**:
716
- - AI code generation often produces non-existent package names - flag these prominently
717
- - Prompt injection is NOT the same as XSS - different threat model and severity
718
- - Sandboxed code execution (vm2, isolated-vm) significantly reduces risk
719
- - Agent tools need both access restrictions AND user context verification
720
-
721
- ### 3.8 RAG Data Exfiltration (ai_rag_exfiltration)
722
- Retrieval Augmented Generation systems can leak sensitive data across tenant boundaries.
723
-
724
- **Unscoped Retrieval Queries:**
725
- - Vector store query WITHOUT user/tenant filter -> **HIGH** (cross-tenant data access)
726
- - .query(), .search(), .similaritySearch() without filter/where/userId/tenantId parameter
727
- - LangChain retriever.invoke() without metadata filter
728
- - Pinecone/Chroma/Weaviate query without namespace or metadata filter
729
- - Query WITH proper scoping (filter by userId/tenantId) -> **REJECT** (properly scoped)
730
- - Query with RLS-enabled Supabase tables -> **LOW/INFO** (verify RLS policy)
731
-
732
- **Raw Context Exposure:**
733
- - Raw sourceDocuments/chunks returned in API response -> **MEDIUM** (data leak to client)
734
- - Raw context returned WITHOUT authentication -> **HIGH** (public data leak)
735
- - Filtered response (only IDs, titles, metadata) -> **REJECT** (properly filtered)
736
- - Response filtering visible nearby (.map, sanitize, redact) -> **INFO**
737
-
738
- **Context Logging:**
739
- - Logging retrieved documents (debug) -> **INFO** (hygiene, not direct risk)
740
- - Logging full prompts with context -> **LOW** (audit concern if logs are accessible)
741
- - Persisting prompts/context to database -> **MEDIUM** (sensitive data retention)
742
-
743
- **CRITICAL RAG RULES**:
744
- - Cross-tenant data access is the PRIMARY risk - always check for user/tenant scoping
745
- - Authenticated endpoints exposing context are MEDIUM; unauthenticated are HIGH
746
- - Debug logging is INFO severity - it's not a direct vulnerability
747
- - If RLS or middleware protection is visible, downgrade significantly
748
-
749
- ### 3.9 AI Endpoint Protection (ai_endpoint_unprotected)
750
- AI/LLM API endpoints can incur significant costs and enable data exfiltration.
751
-
752
- **No Authentication + No Rate Limiting -> HIGH:**
753
- - Endpoint calls OpenAI/Anthropic/etc. without any auth check or rate limit
754
- - Anyone on the internet can abuse the endpoint and run up API costs
755
- - Potential for prompt exfiltration or model abuse
756
-
757
- **Has Rate Limiting but No Authentication -> MEDIUM:**
758
- - Rate limit provides some protection against abuse
759
- - Still allows anonymous access to AI functionality
760
- - Suggest adding authentication
761
-
762
- **Has Authentication but No Rate Limiting -> LOW:**
763
- - Authenticated users could still abuse the endpoint
764
- - Suggest adding rate limiting for cost control
765
- - severity: low (suggest improvement)
766
-
767
- **Has Both Auth and Rate Limiting -> INFO/REJECT:**
768
- - Properly protected endpoint
769
- - REJECT if both are clearly present
770
- - INFO if you want to note the good pattern
771
-
772
- **BYOK (Bring Your Own Key) Endpoints:**
773
- - If user provides their own API key, risk is LOWER
774
- - User pays for their own usage - cost abuse is their problem
775
- - Downgrade severity by one level for BYOK patterns
776
-
777
- **Protected by Middleware:**
778
- - If project context shows auth middleware protecting the route, downgrade to INFO
779
- - Internal/admin routes should be INFO or REJECT
780
-
781
- **CRITICAL ENDPOINT RULES**:
782
- - Cost abuse is real - unprotected AI endpoints can bankrupt a startup
783
- - Rate limiting alone isn't enough - need auth to prevent anonymous abuse
784
- - BYOK endpoints have lower risk since user bears the cost
785
- - Check for middleware protection before flagging
786
-
787
- ### 3.10 Schema/Tooling Mismatch (ai_schema_mismatch)
788
- AI-generated structured outputs need validation before use in security-sensitive contexts.
789
-
790
- **Unvalidated AI Output Parsing:**
791
- - JSON.parse(response.content) without schema validation -> **MEDIUM**
792
- - AI may return malformed or unexpected structures
793
- - Suggest zod/ajv/joi validation
794
- - AI output to EXECUTION SINK (eval, exec, query) without validation -> **HIGH**
795
- - Direct path to code/SQL injection
796
- - AI output to DISPLAY only (console.log, UI render) -> **REJECT**
797
- - Not a security issue for display purposes
798
- - OpenAI Structured Outputs (json_schema in request) -> **REJECT**
799
- - API-level validation provides guarantees
800
-
801
- **Weak Schema Patterns:**
802
- - response: any at API boundary -> **MEDIUM** (no type safety)
803
- - z.any() or z.unknown() -> **LOW** (defeats purpose of validation)
804
- - z.passthrough() -> **INFO** (allows extra properties, minor concern)
805
- - Specific schema defined and used -> **REJECT** (properly validated)
806
-
807
- **Tool Parameter Validation:**
808
- - Tool parameter -> file path without validation -> **HIGH** (path traversal)
809
- - Tool parameter -> shell command without validation -> **CRITICAL** (command injection)
810
- - Tool parameter -> URL without validation -> **HIGH** (SSRF)
811
- - Tool parameter -> DB query without validation -> **HIGH** (SQL injection)
812
- - Tool parameter with allowlist check visible -> **LOW/REJECT** (mitigated)
813
-
814
- **CRITICAL SCHEMA RULES**:
815
- - The severity depends on WHERE the AI output is used, not just that it's parsed
816
- - Execution sinks (eval, exec, query, fs) need HIGH severity without validation
817
- - Display-only usage is NOT a security issue
818
- - Schema validation (zod, ajv, joi) significantly reduces risk
819
- - OpenAI Structured Outputs provide API-level guarantees
820
-
821
- ## False Positive Patterns (ALWAYS REJECT - keep: false)
822
-
823
- 1. **CSS/Styling flagged as secrets**:
824
- - Tailwind classes, gradients, hex colors, rgba/hsla
825
- - style={{...}} objects, CSS-in-JS
826
-
827
- 2. **Development URLs in dev contexts**:
828
- - localhost in test/mock/example files
829
- - URLs via environment variables
830
-
831
- 3. **Test/Example/Scanner code**:
832
- - Files with test, spec, mock, example, fixture in path
833
- - Scanner's own rule definitions
834
- - Documentation/README files
835
-
836
- 4. **TypeScript 'any' in safe contexts**:
837
- - Type definitions, .d.ts files
838
- - Internal utilities (not API boundaries)
839
-
840
- 5. **Public endpoints**:
841
- - /health, /healthz, /ready, /ping, /status
842
- - /webhook with signature verification nearby
843
-
844
- 6. **Generic AI patterns that are NOT security issues**:
845
- - console.log with non-sensitive data → REJECT
846
- - TODO/FIXME reminders (not security-critical) → REJECT
847
- - Magic number timeouts → REJECT
848
- - Verbose/step-by-step comments → REJECT
849
- - Generic error messages → REJECT or downgrade to info
850
- - Basic validation patterns (if (!data) return) → REJECT
851
-
852
- 7. **Style/Code quality issues (NOT security)**:
853
- - Empty functions (unless auth-critical)
854
- - Generic success messages
855
- - Placeholder comments in non-security code
856
-
857
- ## Response Format (OPTIMIZED FOR MINIMAL OUTPUT)
858
-
859
- For each candidate finding, return:
860
- \`\`\`json
861
- {
862
- "index": <number>,
863
- "keep": true | false,
864
- "notes": "<concise context>" | null,
865
- "adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null
866
- }
867
- \`\`\`
868
-
869
- **CRITICAL**: To minimize costs:
870
- - For \`keep: false\` (rejected): Set \`notes: null\` and \`adjustedSeverity: null\`. NO explanation needed.
871
- - For \`keep: true\` (accepted): Include \`notes\` field with brief context (10-30 words). Set \`adjustedSeverity: null\` if keeping original severity.
872
-
873
- ## Severity Guidelines
874
- - **critical/high**: Realistically exploitable, should block deploys - ONLY for clear vulnerabilities
875
- - **medium/low**: Important but non-blocking, hardening opportunities - use sparingly
876
- - **info**: Robustness/hygiene tips, not direct security risks - use for marginal cases you want to keep
877
-
878
- ## Decision Framework
879
- 1. **Default to REJECTION** (keep: false) for:
880
- - Style/code quality issues
881
- - Marginal findings with unclear exploitation path
882
- - Patterns that are standard practice (basic auth checks, error logging)
883
- - Anything in test/example/documentation files
884
-
885
- 2. **Downgrade to info** when:
886
- - Finding has some merit but low practical risk
887
- - Context shows mitigating factors
888
- - Better as a "nice to know" than an action item
889
-
890
- 3. **Keep with original/higher severity** ONLY when:
891
- - Clear, exploitable vulnerability
892
- - No visible mitigating factors in context
893
- - Real-world attack scenario is plausible
894
-
895
- **REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT.
896
-
897
- ## Response Format
898
-
899
- For EACH file, provide a JSON object with the file path and validation results.
900
- Return a JSON array where each element has:
901
- - "file": the file path (e.g., "src/routes/api.ts")
902
- - "validations": array of validation results for that file's candidates
903
-
904
- Example response format (OPTIMIZED):
905
- \`\`\`json
906
- [
907
- {
908
- "file": "src/auth.ts",
909
- "validations": [
910
- { "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
911
- { "index": 1, "keep": false }
912
- ]
913
- },
914
- {
915
- "file": "src/api.ts",
916
- "validations": [
917
- { "index": 0, "keep": true, "notes": "User input flows to SQL query" }
918
- ]
919
- }
920
- ]
921
- \`\`\`
922
-
923
- **REMEMBER**: Rejected findings (keep: false) need NO explanation. Keep notes brief (10-30 words).`;
924
- // Cache for project context (built once per scan)
925
- let cachedProjectContext = null;
926
- /**
927
- * Helper function to make API calls with retry logic for rate limiting
928
- * Implements exponential backoff for 429 (rate limit) errors
929
- */
930
- async function makeAnthropicRequestWithRetry(requestFn, maxRetries = 3, initialDelayMs = 1000) {
931
- let lastError = null;
932
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
933
- try {
934
- return await requestFn();
935
- }
936
- catch (error) {
937
- lastError = error;
938
- // Check if it's a rate limit error (429)
939
- const isRateLimit = error?.status === 429 || error?.message?.includes('rate limit');
940
- if (isRateLimit && attempt < maxRetries) {
941
- // Exponential backoff: 1s, 2s, 4s
942
- const delayMs = initialDelayMs * Math.pow(2, attempt);
943
- console.log(`[AI Validation] Rate limit hit, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`);
944
- await new Promise(resolve => setTimeout(resolve, delayMs));
945
- continue;
946
- }
947
- // If not rate limit or max retries reached, throw
948
- throw error;
949
- }
950
- }
951
- throw lastError || new Error('Max retries exceeded');
952
- }
953
- /**
954
- * Helper to make OpenAI requests with retry logic for rate limits
955
- */
956
- async function makeOpenAIRequestWithRetry(requestFn, maxRetries = 3, initialDelayMs = 1000) {
957
- let lastError = null;
958
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
959
- try {
960
- return await requestFn();
961
- }
962
- catch (error) {
963
- lastError = error;
964
- // Check if it's a rate limit error (429) - but NOT insufficient_quota
965
- const isRateLimit = error?.status === 429 && error?.code !== 'insufficient_quota';
966
- if (isRateLimit && attempt < maxRetries) {
967
- const delayMs = initialDelayMs * Math.pow(2, attempt);
968
- console.log(`[OpenAI Validation] Rate limit hit, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`);
969
- await new Promise(resolve => setTimeout(resolve, delayMs));
970
- continue;
971
- }
972
- // If it's a quota error or max retries reached, throw
973
- throw error;
974
- }
975
- }
976
- throw lastError || new Error('Max retries exceeded');
977
- }
978
- // ============================================================================
979
- // OpenAI Provider Implementation (GPT-5-mini)
980
- // ============================================================================
981
- /**
982
- * Validate findings using OpenAI GPT-5-mini
983
- * This mirrors the Anthropic validation flow but uses OpenAI's API
984
- */
985
- async function validateWithOpenAI(findings, files, projectContext, stats) {
986
- const client = getOpenAIClient();
987
- // Build or use cached project context
988
- const context = projectContext || cachedProjectContext || (0, project_context_builder_1.buildProjectContext)(files);
989
- if (!projectContext && !cachedProjectContext) {
990
- cachedProjectContext = context;
991
- console.log('[OpenAI Validation] Built project context:', {
992
- hasAuthMiddleware: context.auth.hasGlobalMiddleware,
993
- authProvider: context.auth.authProvider,
994
- orm: context.dataAccess.orm,
995
- framework: context.frameworks.primary,
996
- });
997
- }
998
- // Group findings by file for efficient validation
999
- const findingsByFile = new Map();
1000
- for (const finding of findings) {
1001
- const existing = findingsByFile.get(finding.filePath) || [];
1002
- existing.push(finding);
1003
- findingsByFile.set(finding.filePath, existing);
1004
- }
1005
- const validatedFindings = [];
1006
- const fileEntries = Array.from(findingsByFile.entries());
1007
- // Track metrics (thread-safe accumulator)
1008
- let totalApiBatches = 0;
1009
- const statsLock = {
1010
- apiCalls: 0,
1011
- estimatedInputTokens: 0,
1012
- estimatedOutputTokens: 0,
1013
- cacheReadTokens: 0,
1014
- estimatedCost: 0,
1015
- validatedFindings: 0,
1016
- confirmedFindings: 0,
1017
- dismissedFindings: 0,
1018
- downgradedFindings: 0,
1019
- };
1020
- const totalFileBatches = Math.ceil(fileEntries.length / FILES_PER_API_BATCH);
1021
- console.log(`[OpenAI Validation] Processing ${fileEntries.length} files in ${totalFileBatches} API batch(es) (${PARALLEL_API_BATCHES} parallel)`);
1022
- // Create all batch definitions
1023
- const allBatches = [];
1024
- for (let batchStart = 0; batchStart < fileEntries.length; batchStart += FILES_PER_API_BATCH) {
1025
- const fileBatch = fileEntries.slice(batchStart, batchStart + FILES_PER_API_BATCH);
1026
- const batchNum = Math.floor(batchStart / FILES_PER_API_BATCH) + 1;
1027
- allBatches.push({ batchNum, fileBatch });
1028
- }
1029
- // Process a single batch - returns validated findings for that batch
1030
- const processBatch = async (batchDef) => {
1031
- const { batchNum, fileBatch } = batchDef;
1032
- const batchFindings = [];
1033
- // Prepare file data for batch request
1034
- const fileDataList = [];
1035
- const filesWithoutContent = [];
1036
- for (const [filePath, fileFindings] of fileBatch) {
1037
- const file = files.find(f => f.path === filePath);
1038
- if (!file) {
1039
- filesWithoutContent.push({ filePath, findings: fileFindings });
1040
- }
1041
- else {
1042
- fileDataList.push({ file, findings: fileFindings, filePath });
1043
- }
1044
- }
1045
- // Handle files without content
1046
- for (const { findings: fileFindings } of filesWithoutContent) {
1047
- for (const f of fileFindings) {
1048
- batchFindings.push({
1049
- ...f,
1050
- validatedByAI: false,
1051
- validationStatus: 'not_validated',
1052
- validationNotes: 'File content not available for validation',
1053
- });
1054
- }
1055
- }
1056
- if (fileDataList.length === 0) {
1057
- return batchFindings;
1058
- }
1059
- try {
1060
- // Build multi-file validation request
1061
- const validationRequest = buildMultiFileValidationRequest(fileDataList.map(({ file, findings: fileFindings }) => ({ file, findings: fileFindings })), context);
1062
- // Call OpenAI GPT-5-mini with retry logic
1063
- const response = await makeOpenAIRequestWithRetry(async () => client.chat.completions.create({
1064
- model: 'gpt-5-mini-2025-08-07',
1065
- messages: [
1066
- { role: 'system', content: HIGH_CONTEXT_VALIDATION_PROMPT },
1067
- { role: 'user', content: validationRequest },
1068
- ],
1069
- max_completion_tokens: 4096, // Sufficient for larger batches with many findings
1070
- response_format: {
1071
- type: 'json_schema',
1072
- json_schema: {
1073
- name: 'validation_response',
1074
- strict: true,
1075
- schema: {
1076
- type: 'object',
1077
- properties: {
1078
- validations: {
1079
- type: 'array',
1080
- items: {
1081
- type: 'object',
1082
- properties: {
1083
- file: { type: 'string' },
1084
- validations: {
1085
- type: 'array',
1086
- items: {
1087
- type: 'object',
1088
- properties: {
1089
- index: { type: 'number' },
1090
- keep: { type: 'boolean' },
1091
- notes: {
1092
- type: ['string', 'null'],
1093
- default: null
1094
- },
1095
- adjustedSeverity: {
1096
- type: ['string', 'null'],
1097
- enum: ['critical', 'high', 'medium', 'low', 'info', null],
1098
- default: null
1099
- }
1100
- },
1101
- required: ['index', 'keep', 'notes', 'adjustedSeverity'],
1102
- additionalProperties: false
1103
- }
1104
- }
1105
- },
1106
- required: ['file', 'validations'],
1107
- additionalProperties: false
1108
- }
1109
- }
1110
- },
1111
- required: ['validations'],
1112
- additionalProperties: false
1113
- }
1114
- }
1115
- }
1116
- }));
1117
- // Track API call stats (accumulate to shared stats)
1118
- statsLock.apiCalls++;
1119
- // Extract token usage from OpenAI response
1120
- const usage = response.usage;
1121
- if (usage) {
1122
- const promptTokens = usage.prompt_tokens || 0;
1123
- const completionTokens = usage.completion_tokens || 0;
1124
- const cachedTokens = usage.prompt_tokens_details?.cached_tokens || 0;
1125
- const freshInputTokens = promptTokens - cachedTokens;
1126
- statsLock.estimatedInputTokens += freshInputTokens;
1127
- statsLock.estimatedOutputTokens += completionTokens;
1128
- statsLock.cacheReadTokens += cachedTokens;
1129
- console.log(`[OpenAI] Batch ${batchNum} tokens: ${promptTokens} input (${cachedTokens} cached), ${completionTokens} output`);
1130
- const freshCost = (freshInputTokens * GPT5_MINI_PRICING.input) / 1000000;
1131
- const cachedCost = (cachedTokens * GPT5_MINI_PRICING.cached) / 1000000;
1132
- const outputCost = (completionTokens * GPT5_MINI_PRICING.output) / 1000000;
1133
- statsLock.estimatedCost += freshCost + cachedCost + outputCost;
1134
- }
1135
- // Parse response content
1136
- const content = response.choices[0]?.message?.content;
1137
- if (!content) {
1138
- for (const { findings: fileFindings } of fileDataList) {
1139
- for (const f of fileFindings) {
1140
- batchFindings.push({
1141
- ...f,
1142
- validatedByAI: false,
1143
- validationStatus: 'not_validated',
1144
- validationNotes: 'No valid response from OpenAI',
1145
- });
1146
- }
1147
- }
1148
- return batchFindings;
1149
- }
1150
- // Parse structured JSON response (with validations wrapper from response_format)
1151
- let parsedContent;
1152
- try {
1153
- parsedContent = JSON.parse(content);
1154
- console.log(`[OpenAI Debug] Raw parsed content keys:`, Object.keys(parsedContent));
1155
- // Unwrap the validations array if present (from structured output)
1156
- if (parsedContent.validations && Array.isArray(parsedContent.validations)) {
1157
- console.log(`[OpenAI Debug] Unwrapping 'validations' array with ${parsedContent.validations.length} items`);
1158
- parsedContent = parsedContent.validations;
1159
- }
1160
- else if (Array.isArray(parsedContent)) {
1161
- console.log(`[OpenAI Debug] Content is already an array with ${parsedContent.length} items`);
1162
- }
1163
- else {
1164
- console.log(`[OpenAI Debug] Content structure:`, typeof parsedContent, Array.isArray(parsedContent));
1165
- }
1166
- }
1167
- catch (e) {
1168
- console.warn('[OpenAI] Failed to parse JSON response:', e);
1169
- parsedContent = content;
1170
- }
1171
- // Parse multi-file response
1172
- const expectedFiles = fileDataList.map(({ filePath }) => filePath);
1173
- const validationResultsMap = parseMultiFileValidationResponse(typeof parsedContent === 'string' ? parsedContent : JSON.stringify(parsedContent), expectedFiles);
1174
- console.log(`[OpenAI] Batch ${batchNum} parsed ${validationResultsMap.size} file results from ${fileDataList.length} files`);
1175
- if (validationResultsMap.size === 0) {
1176
- console.warn(`[OpenAI] WARNING: No file results parsed! Content type: ${typeof parsedContent}, isArray: ${Array.isArray(parsedContent)}`);
1177
- if (Array.isArray(parsedContent) && parsedContent.length > 0) {
1178
- console.log(`[OpenAI] First item structure:`, Object.keys(parsedContent[0]));
1179
- }
1180
- }
1181
- // Log any missing files from the response (these will be REJECTED)
1182
- if (validationResultsMap.size !== fileDataList.length) {
1183
- const missing = fileDataList
1184
- .filter(({ filePath }) => !validationResultsMap.has(filePath))
1185
- .map(({ filePath }) => filePath);
1186
- if (missing.length > 0) {
1187
- console.warn(`[OpenAI] Missing ${missing.length} files from response (will be REJECTED): ${missing.join(', ')}`);
1188
- }
1189
- }
1190
- // Apply results per file
1191
- for (const { filePath, findings: fileFindings } of fileDataList) {
1192
- const fileResults = validationResultsMap.get(filePath);
1193
- console.log(`[OpenAI] File ${filePath}: ${fileResults?.length || 0} validation results for ${fileFindings.length} findings`);
1194
- if (!fileResults || fileResults.length === 0) {
1195
- const singleFileResults = parseValidationResponse(content);
1196
- if (singleFileResults.length > 0 && fileDataList.length === 1) {
1197
- const { processed: processedFindings, dismissedCount } = applyValidationResults(fileFindings, singleFileResults);
1198
- statsLock.validatedFindings += processedFindings.length + dismissedCount;
1199
- statsLock.dismissedFindings += dismissedCount;
1200
- for (const processed of processedFindings) {
1201
- if (processed.validationStatus === 'confirmed')
1202
- statsLock.confirmedFindings++;
1203
- else if (processed.validationStatus === 'downgraded')
1204
- statsLock.downgradedFindings++;
1205
- batchFindings.push(processed);
1206
- }
1207
- }
1208
- else {
1209
- // No validation results - REJECT all findings for this file (conservative approach)
1210
- console.warn(`[OpenAI] No validation results for ${filePath} - REJECTING ${fileFindings.length} findings`);
1211
- statsLock.validatedFindings += fileFindings.length;
1212
- statsLock.dismissedFindings += fileFindings.length;
1213
- // Don't add to batchFindings - findings are rejected
1214
- }
1215
- }
1216
- else {
1217
- const { processed: processedFindings, dismissedCount } = applyValidationResults(fileFindings, fileResults);
1218
- statsLock.validatedFindings += processedFindings.length + dismissedCount;
1219
- statsLock.dismissedFindings += dismissedCount;
1220
- for (const processed of processedFindings) {
1221
- if (processed.validationStatus === 'confirmed')
1222
- statsLock.confirmedFindings++;
1223
- else if (processed.validationStatus === 'downgraded')
1224
- statsLock.downgradedFindings++;
1225
- batchFindings.push(processed);
1226
- }
1227
- }
1228
- }
1229
- }
1230
- catch (error) {
1231
- console.error(`[OpenAI Validation] Error in batch ${batchNum}:`, error);
1232
- for (const { findings: fileFindings } of fileDataList) {
1233
- for (const f of fileFindings) {
1234
- batchFindings.push({
1235
- ...f,
1236
- validatedByAI: false,
1237
- validationStatus: 'not_validated',
1238
- validationNotes: 'Validation failed due to API error',
1239
- });
1240
- }
1241
- }
1242
- }
1243
- return batchFindings;
1244
- };
1245
- // Process batches in parallel groups
1246
- const startTime = Date.now();
1247
- for (let i = 0; i < allBatches.length; i += PARALLEL_API_BATCHES) {
1248
- const parallelGroup = allBatches.slice(i, i + PARALLEL_API_BATCHES);
1249
- const batchNums = parallelGroup.map(b => b.batchNum).join(', ');
1250
- console.log(`[OpenAI Validation] Processing batches ${batchNums} in parallel`);
1251
- const results = await Promise.all(parallelGroup.map(processBatch));
1252
- for (const batchResults of results) {
1253
- validatedFindings.push(...batchResults);
1254
- }
1255
- totalApiBatches += parallelGroup.length;
1256
- }
1257
- const totalDuration = Date.now() - startTime;
1258
- // Copy accumulated stats back
1259
- stats.apiCalls = statsLock.apiCalls;
1260
- stats.estimatedInputTokens = statsLock.estimatedInputTokens;
1261
- stats.estimatedOutputTokens = statsLock.estimatedOutputTokens;
1262
- stats.cacheReadTokens = statsLock.cacheReadTokens;
1263
- stats.estimatedCost = statsLock.estimatedCost;
1264
- stats.validatedFindings = statsLock.validatedFindings;
1265
- stats.confirmedFindings = statsLock.confirmedFindings;
1266
- stats.dismissedFindings = statsLock.dismissedFindings;
1267
- stats.downgradedFindings = statsLock.downgradedFindings;
1268
- // Calculate cache hit rate
1269
- const totalCacheableTokens = stats.cacheCreationTokens + stats.cacheReadTokens;
1270
- stats.cacheHitRate = totalCacheableTokens > 0
1271
- ? stats.cacheReadTokens / totalCacheableTokens
1272
- : 0;
1273
- // Log validation stats
1274
- const avgTimePerFile = fileEntries.length > 0
1275
- ? (totalDuration / fileEntries.length).toFixed(2)
1276
- : '0';
1277
- console.log(`[OpenAI Validation] Stats:`);
1278
- console.log(` - Total findings: ${stats.totalFindings}`);
1279
- console.log(` - AI validated: ${stats.validatedFindings}`);
1280
- console.log(` - Confirmed: ${stats.confirmedFindings}`);
1281
- console.log(` - Dismissed: ${stats.dismissedFindings}`);
1282
- console.log(` - Downgraded: ${stats.downgradedFindings}`);
1283
- console.log(` - API calls: ${stats.apiCalls}`);
1284
- console.log(` - Performance:`);
1285
- console.log(` - Total API batches: ${totalApiBatches}`);
1286
- console.log(` - Avg time per file: ${avgTimePerFile}s`);
1287
- console.log(` - Token usage:`);
1288
- console.log(` - Input (fresh): ${stats.estimatedInputTokens} tokens`);
1289
- console.log(` - Cached: ${stats.cacheReadTokens} tokens`);
1290
- console.log(` - Output: ${stats.estimatedOutputTokens} tokens`);
1291
- console.log(` - Estimated cost: $${stats.estimatedCost.toFixed(4)}`);
1292
- return { vulnerabilities: validatedFindings, stats };
1293
- }
1294
- /**
1295
- * Validate Layer 1/2 findings using AI with HIGH-CONTEXT validation
1296
- *
1297
- * Key improvements over previous version:
1298
- * 1. Sends FULL FILE CONTENT (not just snippets) for better context
1299
- * 2. Includes PROJECT CONTEXT (auth patterns, data access, etc.)
1300
- * 3. Uses generalised rules from Section 3 of the security model
1301
- */
1302
- async function validateFindingsWithAI(findings, files, projectContext, onProgress) {
1303
- // Initialize stats tracking
1304
- const stats = {
1305
- totalFindings: findings.length,
1306
- validatedFindings: 0,
1307
- confirmedFindings: 0,
1308
- dismissedFindings: 0,
1309
- downgradedFindings: 0,
1310
- autoDismissedFindings: 0,
1311
- estimatedInputTokens: 0,
1312
- estimatedOutputTokens: 0,
1313
- estimatedCost: 0,
1314
- apiCalls: 0,
1315
- cacheCreationTokens: 0,
1316
- cacheReadTokens: 0,
1317
- cacheHitRate: 0,
1318
- };
1319
- if (findings.length === 0) {
1320
- return { vulnerabilities: [], stats };
1321
- }
1322
- // Check for provider override (GPT-5-mini is default for 47% cost savings)
1323
- const aiProvider = process.env.AI_PROVIDER || 'openai';
1324
- if (aiProvider === 'anthropic') {
1325
- console.log('[AI Validation] Using Anthropic provider (Claude 3.5 Haiku)');
1326
- // Fall through to Anthropic implementation below
1327
- }
1328
- else {
1329
- console.log('[AI Validation] Using OpenAI provider (GPT-5-mini)');
1330
- return validateWithOpenAI(findings, files, projectContext, stats);
1331
- }
1332
- // Anthropic implementation
1333
- console.log('[AI Validation] Initializing Anthropic client...');
1334
- const client = getAnthropicClient();
1335
- // Build or use cached project context
1336
- const context = projectContext || cachedProjectContext || (0, project_context_builder_1.buildProjectContext)(files);
1337
- if (!projectContext && !cachedProjectContext) {
1338
- cachedProjectContext = context;
1339
- console.log('[AI Validation] Built project context:', {
1340
- hasAuthMiddleware: context.auth.hasGlobalMiddleware,
1341
- authProvider: context.auth.authProvider,
1342
- orm: context.dataAccess.orm,
1343
- framework: context.frameworks.primary,
1344
- });
1345
- }
1346
- // Group findings by file for efficient validation
1347
- const findingsByFile = new Map();
1348
- for (const finding of findings) {
1349
- const existing = findingsByFile.get(finding.filePath) || [];
1350
- existing.push(finding);
1351
- findingsByFile.set(finding.filePath, existing);
1352
- }
1353
- const validatedFindings = [];
1354
- // Phase 2: Multi-file batching
1355
- // Instead of one API call per file, batch multiple files into single requests
1356
- // This reduces API overhead and leverages prompt caching more effectively
1357
- const fileEntries = Array.from(findingsByFile.entries());
1358
- // Track metrics
1359
- let totalBatchWaitTime = 0;
1360
- let totalApiBatches = 0;
1361
- // Calculate how many API batches we'll make
1362
- const totalFileBatches = Math.ceil(fileEntries.length / FILES_PER_API_BATCH);
1363
- console.log(`[AI Validation] Phase 2: Processing ${fileEntries.length} files in ${totalFileBatches} API batch(es) (${FILES_PER_API_BATCH} files/batch)`);
1364
- // Track files processed for progress reporting
1365
- let filesValidated = 0;
1366
- // Process files in batches - each batch is ONE API call with multiple files
1367
- for (let batchStart = 0; batchStart < fileEntries.length; batchStart += FILES_PER_API_BATCH) {
1368
- const fileBatch = fileEntries.slice(batchStart, batchStart + FILES_PER_API_BATCH);
1369
- const batchNum = Math.floor(batchStart / FILES_PER_API_BATCH) + 1;
1370
- // Report progress before processing batch
1371
- if (onProgress) {
1372
- onProgress({
1373
- filesProcessed: filesValidated,
1374
- totalFiles: fileEntries.length,
1375
- status: `AI validating batch ${batchNum}/${totalFileBatches}`,
1376
- });
1377
- }
1378
- console.log(`[AI Validation] API Batch ${batchNum}/${totalFileBatches}: ${fileBatch.length} files`);
1379
- // Prepare file data for batch request
1380
- const fileDataList = [];
1381
- const filesWithoutContent = [];
1382
- for (const [filePath, fileFindings] of fileBatch) {
1383
- const file = files.find(f => f.path === filePath);
1384
- if (!file) {
1385
- // Can't validate without file content
1386
- filesWithoutContent.push({ filePath, findings: fileFindings });
1387
- }
1388
- else {
1389
- fileDataList.push({ file, findings: fileFindings, filePath });
1390
- }
1391
- }
1392
- // Handle files without content - mark as not validated
1393
- for (const { findings } of filesWithoutContent) {
1394
- for (const f of findings) {
1395
- validatedFindings.push({
1396
- ...f,
1397
- validatedByAI: false,
1398
- validationStatus: 'not_validated',
1399
- validationNotes: 'File content not available for validation',
1400
- });
1401
- }
1402
- }
1403
- // Skip API call if no files with content
1404
- if (fileDataList.length === 0) {
1405
- continue;
1406
- }
1407
- const batchStartTime = Date.now();
1408
- try {
1409
- // Build multi-file validation request
1410
- const validationRequest = buildMultiFileValidationRequest(fileDataList.map(({ file, findings }) => ({ file, findings })), context);
1411
- // Use Anthropic prompt caching with multi-file request
1412
- const response = await makeAnthropicRequestWithRetry(() => client.messages.create({
1413
- model: 'claude-3-5-haiku-20241022',
1414
- max_tokens: 1500, // Reduced from 4096 - optimized format needs less output
1415
- system: [
1416
- {
1417
- type: 'text',
1418
- text: HIGH_CONTEXT_VALIDATION_PROMPT,
1419
- cache_control: { type: 'ephemeral' }, // Cache for 5 minutes
1420
- },
1421
- ],
1422
- messages: [{ role: 'user', content: validationRequest }],
1423
- }));
1424
- // Track API call stats
1425
- stats.apiCalls++;
1426
- totalApiBatches++;
1427
- // Extract cache metrics from usage
1428
- const usage = response.usage;
1429
- if (usage) {
1430
- // DEBUG: Log full usage object to understand token breakdown
1431
- console.log(`[DEBUG] Batch ${batchNum} - Full API Response Usage:`);
1432
- console.log(JSON.stringify(usage, null, 2));
1433
- console.log(`[DEBUG] Breakdown:`);
1434
- console.log(` - input_tokens: ${usage.input_tokens || 0}`);
1435
- console.log(` - output_tokens: ${usage.output_tokens || 0}`);
1436
- // @ts-ignore
1437
- console.log(` - cache_creation_input_tokens: ${usage.cache_creation_input_tokens || 0}`);
1438
- // @ts-ignore
1439
- console.log(` - cache_read_input_tokens: ${usage.cache_read_input_tokens || 0}`);
1440
- stats.estimatedInputTokens += usage.input_tokens || 0;
1441
- stats.estimatedOutputTokens += usage.output_tokens || 0;
1442
- // @ts-ignore - cache fields not in types yet
1443
- const cacheCreation = usage.cache_creation_input_tokens || 0;
1444
- // @ts-ignore
1445
- const cacheRead = usage.cache_read_input_tokens || 0;
1446
- stats.cacheCreationTokens += cacheCreation;
1447
- stats.cacheReadTokens += cacheRead;
1448
- }
1449
- const textContent = response.content.find((block) => block.type === 'text');
1450
- if (!textContent || textContent.type !== 'text') {
1451
- // No valid response - mark all findings as not validated
1452
- for (const { findings } of fileDataList) {
1453
- for (const f of findings) {
1454
- validatedFindings.push({
1455
- ...f,
1456
- validatedByAI: false,
1457
- validationStatus: 'not_validated',
1458
- validationNotes: 'No valid response from AI',
1459
- });
1460
- }
1461
- }
1462
- continue;
1463
- }
1464
- // Parse multi-file response
1465
- const expectedFiles = fileDataList.map(({ filePath }) => filePath);
1466
- const validationResultsMap = parseMultiFileValidationResponse(textContent.text, expectedFiles);
1467
- // Apply results per file
1468
- for (const { filePath, findings } of fileDataList) {
1469
- const fileResults = validationResultsMap.get(filePath);
1470
- if (!fileResults || fileResults.length === 0) {
1471
- // No results for this file - try single-file parsing as fallback
1472
- // This handles cases where AI doesn't follow multi-file format
1473
- const singleFileResults = parseValidationResponse(textContent.text);
1474
- if (singleFileResults.length > 0 && fileDataList.length === 1) {
1475
- // Single file in batch, use single-file parsing
1476
- const { processed: processedFindings, dismissedCount } = applyValidationResults(findings, singleFileResults);
1477
- stats.validatedFindings += processedFindings.length + dismissedCount;
1478
- stats.dismissedFindings += dismissedCount;
1479
- for (const processed of processedFindings) {
1480
- if (processed.validationStatus === 'confirmed') {
1481
- stats.confirmedFindings++;
1482
- }
1483
- else if (processed.validationStatus === 'downgraded') {
1484
- stats.downgradedFindings++;
1485
- }
1486
- validatedFindings.push(processed);
1487
- }
1488
- }
1489
- else {
1490
- // No validation results - REJECT all findings for this file (conservative approach)
1491
- console.warn(`[AI Validation] No results for ${filePath} - REJECTING ${findings.length} findings`);
1492
- stats.validatedFindings += findings.length;
1493
- stats.dismissedFindings += findings.length;
1494
- // Don't add to validatedFindings - findings are rejected
1495
- }
1496
- }
1497
- else {
1498
- // Apply validation results for this file
1499
- const { processed: processedFindings, dismissedCount } = applyValidationResults(findings, fileResults);
1500
- stats.validatedFindings += processedFindings.length + dismissedCount;
1501
- stats.dismissedFindings += dismissedCount;
1502
- for (const processed of processedFindings) {
1503
- if (processed.validationStatus === 'confirmed') {
1504
- stats.confirmedFindings++;
1505
- }
1506
- else if (processed.validationStatus === 'downgraded') {
1507
- stats.downgradedFindings++;
1508
- }
1509
- validatedFindings.push(processed);
1510
- }
1511
- }
1512
- }
1513
- }
1514
- catch (error) {
1515
- console.error(`[AI Validation] Error in batch ${batchNum}:`, error);
1516
- // Fallback: keep all findings but mark as not validated
1517
- for (const { findings } of fileDataList) {
1518
- for (const f of findings) {
1519
- validatedFindings.push({
1520
- ...f,
1521
- validatedByAI: false,
1522
- validationStatus: 'not_validated',
1523
- validationNotes: 'Validation failed due to API error',
1524
- });
1525
- }
1526
- }
1527
- }
1528
- const batchDuration = Date.now() - batchStartTime;
1529
- totalBatchWaitTime += batchDuration;
1530
- // Update files validated counter
1531
- filesValidated += fileBatch.length;
1532
- // Report progress after batch completion
1533
- if (onProgress) {
1534
- onProgress({
1535
- filesProcessed: filesValidated,
1536
- totalFiles: fileEntries.length,
1537
- status: `AI validation complete for batch ${batchNum}/${totalFileBatches}`,
1538
- });
1539
- }
1540
- }
1541
- // Calculate cache hit rate
1542
- const totalCacheableTokens = stats.cacheCreationTokens + stats.cacheReadTokens;
1543
- stats.cacheHitRate = totalCacheableTokens > 0
1544
- ? stats.cacheReadTokens / totalCacheableTokens
1545
- : 0;
1546
- // Calculate estimated cost with cache pricing
1547
- // Claude 3.5 Haiku pricing (claude-3-5-haiku-20241022):
1548
- // - Base input: $0.80/1M tokens
1549
- // - 5m cache writes: $1.00/1M tokens
1550
- // - Cache hits: $0.08/1M tokens
1551
- // - Output: $4.00/1M tokens
1552
- //
1553
- // Note: input_tokens from Anthropic API represents only fresh (non-cached) tokens
1554
- // Cache tokens are reported separately and billed at different rates
1555
- const freshInputCost = (stats.estimatedInputTokens * 0.80) / 1000000;
1556
- const cacheWriteCost = (stats.cacheCreationTokens * 1.00) / 1000000;
1557
- const cacheReadCost = (stats.cacheReadTokens * 0.08) / 1000000;
1558
- const outputCost = (stats.estimatedOutputTokens * 4.00) / 1000000;
1559
- stats.estimatedCost = freshInputCost + cacheWriteCost + cacheReadCost + outputCost;
1560
- // Log validation stats with cache metrics and performance
1561
- console.log(`[AI Validation] Stats:`);
1562
- console.log(` - Total findings: ${stats.totalFindings}`);
1563
- console.log(` - AI validated: ${stats.validatedFindings}`);
1564
- console.log(` - Confirmed: ${stats.confirmedFindings}`);
1565
- console.log(` - Dismissed: ${stats.dismissedFindings}`);
1566
- console.log(` - Downgraded: ${stats.downgradedFindings}`);
1567
- console.log(` - API calls: ${stats.apiCalls}`);
1568
- console.log(` - Performance (Phase 2 Multi-File Batching):`);
1569
- console.log(` - Files per API batch: ${FILES_PER_API_BATCH}`);
1570
- console.log(` - Total API batches: ${totalApiBatches}`);
1571
- console.log(` - Total validation time: ${(totalBatchWaitTime / 1000).toFixed(2)}s`);
1572
- console.log(` - Avg time per file: ${fileEntries.length > 0 ? (totalBatchWaitTime / fileEntries.length / 1000).toFixed(2) : 0}s`);
1573
- console.log(` - Cache metrics:`);
1574
- console.log(` - Cache writes: ${stats.cacheCreationTokens.toLocaleString()} tokens`);
1575
- console.log(` - Cache reads: ${stats.cacheReadTokens.toLocaleString()} tokens`);
1576
- console.log(` - Cache hit rate: ${(stats.cacheHitRate * 100).toFixed(1)}%`);
1577
- console.log(` - Token usage:`);
1578
- console.log(` - Input (total): ${stats.estimatedInputTokens.toLocaleString()} tokens`);
1579
- console.log(` - Output: ${stats.estimatedOutputTokens.toLocaleString()} tokens`);
1580
- console.log(` - Estimated cost: $${stats.estimatedCost.toFixed(4)}`);
1581
- // Clear cache after validation complete
1582
- cachedProjectContext = null;
1583
- return { vulnerabilities: validatedFindings, stats };
1584
- }
1585
- /**
1586
- * Build a high-context validation request with full file content
1587
- */
1588
- function buildHighContextValidationRequest(file, findings, projectContext) {
1589
- // Add line numbers to full file content
1590
- const numberedContent = file.content
1591
- .split('\n')
1592
- .map((line, i) => `${String(i + 1).padStart(4, ' ')} | ${line}`)
1593
- .join('\n');
1594
- // Build candidate findings list
1595
- const candidatesText = findings.map((f, idx) => {
1596
- return `### Candidate ${idx}
1597
- - **Rule**: ${f.title}
1598
- - **Category**: ${f.category}
1599
- - **Original Severity**: ${f.severity}
1600
- - **Line**: ${f.lineNumber}
1601
- - **Detection Layer**: ${f.layer}
1602
- - **Description**: ${f.description}
1603
- - **Flagged Code**: \`${f.lineContent.trim()}\``;
1604
- }).join('\n\n');
1605
- // Get file-specific context
1606
- const fileContext = (0, project_context_builder_1.getFileValidationContext)(file, projectContext);
1607
- return `## Project Context
1608
- ${projectContext.summary}
1609
-
1610
- ${fileContext}
1611
-
1612
- ## Full File Content
1613
- \`\`\`${file.language || getLanguageFromPath(file.path)}
1614
- ${numberedContent}
1615
- \`\`\`
1616
-
1617
- ## Candidate Findings to Validate (${findings.length} total)
1618
-
1619
- ${candidatesText}
1620
-
1621
- ---
1622
-
1623
- Please validate each candidate finding. Return a JSON array with your decision for each.
1624
- Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context and project architecture to make informed decisions.`;
1625
- }
1626
- /**
1627
- * Build a multi-file validation request (Phase 2 optimization)
1628
- * Batches multiple files into a single API call to reduce overhead
1629
- */
1630
- function buildMultiFileValidationRequest(fileDataList, projectContext) {
1631
- const filesContent = fileDataList.map(({ file, findings }, fileIndex) => {
1632
- // Add line numbers to full file content
1633
- const numberedContent = file.content
1634
- .split('\n')
1635
- .map((line, i) => `${String(i + 1).padStart(4, ' ')} | ${line}`)
1636
- .join('\n');
1637
- // Build candidate findings list with file-specific indices
1638
- const candidatesText = findings.map((f, idx) => {
1639
- return `### Candidate ${idx}
1640
- - **Rule**: ${f.title}
1641
- - **Category**: ${f.category}
1642
- - **Original Severity**: ${f.severity}
1643
- - **Line**: ${f.lineNumber}
1644
- - **Detection Layer**: ${f.layer}
1645
- - **Description**: ${f.description}
1646
- - **Flagged Code**: \`${f.lineContent.trim()}\``;
1647
- }).join('\n\n');
1648
- // Get file-specific context
1649
- const fileContext = (0, project_context_builder_1.getFileValidationContext)(file, projectContext);
1650
- return `
1651
- ================================================================================
1652
- FILE ${fileIndex + 1}: ${file.path}
1653
- ================================================================================
1654
-
1655
- ${fileContext}
1656
-
1657
- ### Full File Content
1658
- \`\`\`${file.language || getLanguageFromPath(file.path)}
1659
- ${numberedContent}
1660
- \`\`\`
1661
-
1662
- ### Candidate Findings to Validate (${findings.length} total)
1663
-
1664
- ${candidatesText}`;
1665
- }).join('\n\n');
1666
- return `## Project Context
1667
- ${projectContext.summary}
1668
-
1669
- ${filesContent}
1670
-
1671
- ---
1672
-
1673
- ## Response Format
1674
-
1675
- For EACH file, provide a JSON object with the file path and validation results.
1676
- Return a JSON array where each element has:
1677
- - "file": the file path (e.g., "${fileDataList[0]?.file.path || 'path/to/file.ts'}")
1678
- - "validations": array of validation results for that file's candidates
1679
-
1680
- Example response format:
1681
- \`\`\`json
1682
- [
1683
- {
1684
- "file": "src/auth.ts",
1685
- "validations": [
1686
- { "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
1687
- { "index": 1, "keep": false }
1688
- ]
1689
- },
1690
- {
1691
- "file": "src/api.ts",
1692
- "validations": [
1693
- { "index": 0, "keep": true, "notes": "User input flows to SQL query" }
1694
- ]
1695
- }
1696
- ]
1697
- \`\`\`
1698
-
1699
- Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context and project architecture to make informed decisions.`;
1700
- }
1701
- /**
1702
- * Parse multi-file validation response (Phase 2)
1703
- * Returns a map of file path -> validation results
1704
- */
1705
- function parseMultiFileValidationResponse(response, expectedFiles) {
1706
- const resultMap = new Map();
1707
- try {
1708
- // Extract the first top-level JSON array from the response
1709
- const extractTopLevelArray = (text) => {
1710
- const startIndex = text.indexOf('[');
1711
- if (startIndex === -1)
1712
- return null;
1713
- let depth = 0;
1714
- let inString = false;
1715
- let stringChar = null;
1716
- let escape = false;
1717
- for (let i = startIndex; i < text.length; i++) {
1718
- const ch = text[i];
1719
- if (inString) {
1720
- if (escape) {
1721
- escape = false;
1722
- continue;
1723
- }
1724
- if (ch === '\\') {
1725
- escape = true;
1726
- continue;
1727
- }
1728
- if (stringChar && ch === stringChar) {
1729
- inString = false;
1730
- stringChar = null;
1731
- }
1732
- continue;
1733
- }
1734
- if (ch === '"' || ch === "'") {
1735
- inString = true;
1736
- stringChar = ch;
1737
- continue;
1738
- }
1739
- if (ch === '[') {
1740
- depth++;
1741
- }
1742
- else if (ch === ']') {
1743
- depth--;
1744
- if (depth === 0) {
1745
- return text.slice(startIndex, i + 1);
1746
- }
1747
- }
1748
- }
1749
- return null;
1750
- };
1751
- const jsonSlice = extractTopLevelArray(response);
1752
- if (!jsonSlice) {
1753
- console.error('[AI Validation] Multi-file: No JSON array found in response');
1754
- return resultMap;
1755
- }
1756
- const parsed = JSON.parse(jsonSlice);
1757
- if (!Array.isArray(parsed)) {
1758
- console.error('[AI Validation] Multi-file: Parsed result is not an array');
1759
- return resultMap;
1760
- }
1761
- // Process each file's results
1762
- for (const fileResult of parsed) {
1763
- if (!fileResult.file || !Array.isArray(fileResult.validations)) {
1764
- console.warn('[AI Validation] Multi-file: Invalid file result structure, skipping');
1765
- continue;
1766
- }
1767
- // Use path normalization to match AI response paths to expected paths
1768
- const responsePath = fileResult.file;
1769
- const matchedPath = findMatchingFilePath(responsePath, expectedFiles);
1770
- if (!matchedPath) {
1771
- console.warn(`[AI Validation] Multi-file: Could not match path "${responsePath}" to any expected file`);
1772
- continue;
1773
- }
1774
- const validations = fileResult.validations
1775
- .filter((item) => typeof item.index === 'number' &&
1776
- typeof item.keep === 'boolean')
1777
- .map((item) => {
1778
- // Normalize notes field: prefer new 'notes', fallback to legacy 'reason' or 'validationNotes'
1779
- const notes = item.notes || item.validationNotes || item.reason || undefined;
1780
- return {
1781
- index: item.index,
1782
- keep: item.keep,
1783
- notes,
1784
- adjustedSeverity: item.adjustedSeverity || null,
1785
- // Keep legacy fields for backward compatibility
1786
- reason: item.reason,
1787
- validationNotes: item.validationNotes,
1788
- };
1789
- });
1790
- resultMap.set(matchedPath, validations);
1791
- }
1792
- // Log any files that weren't in the response (these will be REJECTED by default)
1793
- const missingFiles = expectedFiles.filter(f => !resultMap.has(f));
1794
- if (missingFiles.length > 0) {
1795
- console.warn(`[AI Validation] Multi-file: Missing ${missingFiles.length} files from response: ${missingFiles.join(', ')}`);
1796
- }
1797
- }
1798
- catch (error) {
1799
- console.error('[AI Validation] Multi-file: Failed to parse response:', error);
1800
- }
1801
- return resultMap;
1802
- }
1803
- /**
1804
- * Apply validation results to findings
1805
- */
1806
- function applyValidationResults(findings, validationResults) {
1807
- const processed = [];
1808
- let dismissedCount = 0;
1809
- for (let i = 0; i < findings.length; i++) {
1810
- const finding = findings[i];
1811
- const validation = validationResults.find(v => v.index === i);
1812
- if (!validation) {
1813
- // No validation result - REJECT by default (conservative approach)
1814
- // If AI doesn't explicitly validate a finding, assume it's a false positive
1815
- console.warn(`[AI Validation] No result for finding ${i}: ${finding.title} - REJECTING`);
1816
- dismissedCount++;
1817
- continue; // Don't add to processed - finding is removed
1818
- }
1819
- if (validation.keep) {
1820
- // Keep the finding
1821
- const adjustedFinding = {
1822
- ...finding,
1823
- validatedByAI: true,
1824
- confidence: 'high',
1825
- };
1826
- // Extract notes from optimized or legacy format
1827
- const validationNotes = validation.notes || validation.validationNotes || validation.reason || undefined;
1828
- if (validation.adjustedSeverity && validation.adjustedSeverity !== finding.severity) {
1829
- // Severity was adjusted
1830
- adjustedFinding.originalSeverity = finding.severity;
1831
- adjustedFinding.severity = validation.adjustedSeverity;
1832
- adjustedFinding.validationStatus = 'downgraded';
1833
- adjustedFinding.validationNotes = validationNotes || 'Severity adjusted by AI validation';
1834
- }
1835
- else {
1836
- // Confirmed at original severity
1837
- adjustedFinding.validationStatus = 'confirmed';
1838
- adjustedFinding.validationNotes = validationNotes;
1839
- }
1840
- processed.push(adjustedFinding);
1841
- }
1842
- else {
1843
- // Finding was dismissed - no need to log verbose reason (cost optimization)
1844
- console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber}`);
1845
- dismissedCount++;
1846
- // Don't add to processed - finding is removed
1847
- }
1848
- }
1849
- return { processed, dismissedCount };
1850
- }
1851
- /**
1852
- * Get language identifier from file path
1853
- */
1854
- function getLanguageFromPath(path) {
1855
- const ext = path.split('.').pop()?.toLowerCase();
1856
- const langMap = {
1857
- ts: 'typescript',
1858
- tsx: 'tsx',
1859
- js: 'javascript',
1860
- jsx: 'jsx',
1861
- py: 'python',
1862
- rb: 'ruby',
1863
- go: 'go',
1864
- java: 'java',
1865
- php: 'php',
1866
- cs: 'csharp',
1867
- json: 'json',
1868
- yaml: 'yaml',
1869
- yml: 'yaml',
1870
- };
1871
- return langMap[ext || ''] || ext || 'text';
1872
- }
1873
- function parseValidationResponse(response) {
1874
- try {
1875
- // Extract the first top-level JSON array from the response.
1876
- // The model may include prose before/after the JSON, so we cannot
1877
- // assume the entire response is valid JSON.
1878
- const extractTopLevelArray = (text) => {
1879
- const startIndex = text.indexOf('[');
1880
- if (startIndex === -1)
1881
- return null;
1882
- let depth = 0;
1883
- let inString = false;
1884
- let stringChar = null;
1885
- let escape = false;
1886
- for (let i = startIndex; i < text.length; i++) {
1887
- const ch = text[i];
1888
- if (inString) {
1889
- if (escape) {
1890
- escape = false;
1891
- continue;
1892
- }
1893
- if (ch === '\\') {
1894
- escape = true;
1895
- continue;
1896
- }
1897
- if (stringChar && ch === stringChar) {
1898
- inString = false;
1899
- stringChar = null;
1900
- }
1901
- continue;
1902
- }
1903
- if (ch === '"' || ch === "'") {
1904
- inString = true;
1905
- stringChar = ch;
1906
- continue;
1907
- }
1908
- if (ch === '[') {
1909
- depth++;
1910
- }
1911
- else if (ch === ']') {
1912
- depth--;
1913
- if (depth === 0) {
1914
- return text.slice(startIndex, i + 1);
1915
- }
1916
- }
1917
- }
1918
- return null;
1919
- };
1920
- const jsonSlice = extractTopLevelArray(response);
1921
- if (!jsonSlice)
1922
- return [];
1923
- const parsed = JSON.parse(jsonSlice);
1924
- if (!Array.isArray(parsed))
1925
- return [];
1926
- return parsed
1927
- .filter(item => typeof item.index === 'number' &&
1928
- typeof item.keep === 'boolean')
1929
- .map(item => {
1930
- // Normalize notes field: prefer new 'notes', fallback to legacy 'reason' or 'validationNotes'
1931
- const notes = item.notes || item.validationNotes || item.reason || undefined;
1932
- return {
1933
- index: item.index,
1934
- keep: item.keep,
1935
- notes,
1936
- adjustedSeverity: item.adjustedSeverity || null,
1937
- // Keep legacy fields for backward compatibility
1938
- reason: item.reason,
1939
- validationNotes: item.validationNotes,
1940
- };
1941
- });
1942
- }
1943
- catch (error) {
1944
- console.error('Failed to parse validation response:', error);
1945
- return [];
1946
- }
1947
- }
1948
- //# sourceMappingURL=anthropic.js.map