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