circle-ir-ai 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (420) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +15 -0
  3. package/README.md +336 -0
  4. package/dist/action-queue/aggregator.d.ts +40 -0
  5. package/dist/action-queue/aggregator.d.ts.map +1 -0
  6. package/dist/action-queue/aggregator.js +375 -0
  7. package/dist/action-queue/aggregator.js.map +1 -0
  8. package/dist/action-queue/index.d.ts +14 -0
  9. package/dist/action-queue/index.d.ts.map +1 -0
  10. package/dist/action-queue/index.js +17 -0
  11. package/dist/action-queue/index.js.map +1 -0
  12. package/dist/action-queue/queue.d.ts +74 -0
  13. package/dist/action-queue/queue.d.ts.map +1 -0
  14. package/dist/action-queue/queue.js +433 -0
  15. package/dist/action-queue/queue.js.map +1 -0
  16. package/dist/action-queue/types.d.ts +162 -0
  17. package/dist/action-queue/types.d.ts.map +1 -0
  18. package/dist/action-queue/types.js +44 -0
  19. package/dist/action-queue/types.js.map +1 -0
  20. package/dist/agents/enrichment-agent.d.ts +16 -0
  21. package/dist/agents/enrichment-agent.d.ts.map +1 -0
  22. package/dist/agents/enrichment-agent.js +102 -0
  23. package/dist/agents/enrichment-agent.js.map +1 -0
  24. package/dist/agents/index.d.ts +12 -0
  25. package/dist/agents/index.d.ts.map +1 -0
  26. package/dist/agents/index.js +15 -0
  27. package/dist/agents/index.js.map +1 -0
  28. package/dist/agents/mastra/agents.d.ts +373 -0
  29. package/dist/agents/mastra/agents.d.ts.map +1 -0
  30. package/dist/agents/mastra/agents.js +347 -0
  31. package/dist/agents/mastra/agents.js.map +1 -0
  32. package/dist/agents/mastra/index.d.ts +12 -0
  33. package/dist/agents/mastra/index.d.ts.map +1 -0
  34. package/dist/agents/mastra/index.js +17 -0
  35. package/dist/agents/mastra/index.js.map +1 -0
  36. package/dist/agents/mastra/instance.d.ts +383 -0
  37. package/dist/agents/mastra/instance.d.ts.map +1 -0
  38. package/dist/agents/mastra/instance.js +37 -0
  39. package/dist/agents/mastra/instance.js.map +1 -0
  40. package/dist/agents/mastra/steps.d.ts +300 -0
  41. package/dist/agents/mastra/steps.d.ts.map +1 -0
  42. package/dist/agents/mastra/steps.js +468 -0
  43. package/dist/agents/mastra/steps.js.map +1 -0
  44. package/dist/agents/mastra/swarm.d.ts +106 -0
  45. package/dist/agents/mastra/swarm.d.ts.map +1 -0
  46. package/dist/agents/mastra/swarm.js +501 -0
  47. package/dist/agents/mastra/swarm.js.map +1 -0
  48. package/dist/agents/mastra/workflow.d.ts +81 -0
  49. package/dist/agents/mastra/workflow.d.ts.map +1 -0
  50. package/dist/agents/mastra/workflow.js +460 -0
  51. package/dist/agents/mastra/workflow.js.map +1 -0
  52. package/dist/agents/multi/agents/security.d.ts +29 -0
  53. package/dist/agents/multi/agents/security.d.ts.map +1 -0
  54. package/dist/agents/multi/agents/security.js +830 -0
  55. package/dist/agents/multi/agents/security.js.map +1 -0
  56. package/dist/agents/multi/extractor.d.ts +21 -0
  57. package/dist/agents/multi/extractor.d.ts.map +1 -0
  58. package/dist/agents/multi/extractor.js +483 -0
  59. package/dist/agents/multi/extractor.js.map +1 -0
  60. package/dist/agents/multi/index.d.ts +32 -0
  61. package/dist/agents/multi/index.d.ts.map +1 -0
  62. package/dist/agents/multi/index.js +34 -0
  63. package/dist/agents/multi/index.js.map +1 -0
  64. package/dist/agents/multi/runner.d.ts +79 -0
  65. package/dist/agents/multi/runner.d.ts.map +1 -0
  66. package/dist/agents/multi/runner.js +323 -0
  67. package/dist/agents/multi/runner.js.map +1 -0
  68. package/dist/agents/security-agent.d.ts +16 -0
  69. package/dist/agents/security-agent.d.ts.map +1 -0
  70. package/dist/agents/security-agent.js +299 -0
  71. package/dist/agents/security-agent.js.map +1 -0
  72. package/dist/agents/types.d.ts +373 -0
  73. package/dist/agents/types.d.ts.map +1 -0
  74. package/dist/agents/types.js +14 -0
  75. package/dist/agents/types.js.map +1 -0
  76. package/dist/agents/verification-agent.d.ts +23 -0
  77. package/dist/agents/verification-agent.d.ts.map +1 -0
  78. package/dist/agents/verification-agent.js +217 -0
  79. package/dist/agents/verification-agent.js.map +1 -0
  80. package/dist/agents/workflow.d.ts +30 -0
  81. package/dist/agents/workflow.d.ts.map +1 -0
  82. package/dist/agents/workflow.js +79 -0
  83. package/dist/agents/workflow.js.map +1 -0
  84. package/dist/analysis/enriched.d.ts +16 -0
  85. package/dist/analysis/enriched.d.ts.map +1 -0
  86. package/dist/analysis/enriched.js +297 -0
  87. package/dist/analysis/enriched.js.map +1 -0
  88. package/dist/analysis/llm-correlated-predicates.d.ts +80 -0
  89. package/dist/analysis/llm-correlated-predicates.d.ts.map +1 -0
  90. package/dist/analysis/llm-correlated-predicates.js +255 -0
  91. package/dist/analysis/llm-correlated-predicates.js.map +1 -0
  92. package/dist/analysis/llm-cross-file-taint.d.ts +86 -0
  93. package/dist/analysis/llm-cross-file-taint.d.ts.map +1 -0
  94. package/dist/analysis/llm-cross-file-taint.js +264 -0
  95. package/dist/analysis/llm-cross-file-taint.js.map +1 -0
  96. package/dist/analysis/pattern-discovery.d.ts +79 -0
  97. package/dist/analysis/pattern-discovery.d.ts.map +1 -0
  98. package/dist/analysis/pattern-discovery.js +447 -0
  99. package/dist/analysis/pattern-discovery.js.map +1 -0
  100. package/dist/cache/file-cache.d.ts +89 -0
  101. package/dist/cache/file-cache.d.ts.map +1 -0
  102. package/dist/cache/file-cache.js +208 -0
  103. package/dist/cache/file-cache.js.map +1 -0
  104. package/dist/cache/index.d.ts +6 -0
  105. package/dist/cache/index.d.ts.map +1 -0
  106. package/dist/cache/index.js +5 -0
  107. package/dist/cache/index.js.map +1 -0
  108. package/dist/cli/args.d.ts +52 -0
  109. package/dist/cli/args.d.ts.map +1 -0
  110. package/dist/cli/args.js +422 -0
  111. package/dist/cli/args.js.map +1 -0
  112. package/dist/cli/colors.d.ts +31 -0
  113. package/dist/cli/colors.d.ts.map +1 -0
  114. package/dist/cli/colors.js +80 -0
  115. package/dist/cli/colors.js.map +1 -0
  116. package/dist/cli/commands/analyze-skill.d.ts +33 -0
  117. package/dist/cli/commands/analyze-skill.d.ts.map +1 -0
  118. package/dist/cli/commands/analyze-skill.js +217 -0
  119. package/dist/cli/commands/analyze-skill.js.map +1 -0
  120. package/dist/cli/commands/analyze.d.ts +18 -0
  121. package/dist/cli/commands/analyze.d.ts.map +1 -0
  122. package/dist/cli/commands/analyze.js +30 -0
  123. package/dist/cli/commands/analyze.js.map +1 -0
  124. package/dist/cli/commands/benchmark-runner.d.ts +42 -0
  125. package/dist/cli/commands/benchmark-runner.d.ts.map +1 -0
  126. package/dist/cli/commands/benchmark-runner.js +18 -0
  127. package/dist/cli/commands/benchmark-runner.js.map +1 -0
  128. package/dist/cli/commands/benchmark.d.ts +11 -0
  129. package/dist/cli/commands/benchmark.d.ts.map +1 -0
  130. package/dist/cli/commands/benchmark.js +90 -0
  131. package/dist/cli/commands/benchmark.js.map +1 -0
  132. package/dist/cli/commands/dead-code.d.ts +11 -0
  133. package/dist/cli/commands/dead-code.d.ts.map +1 -0
  134. package/dist/cli/commands/dead-code.js +65 -0
  135. package/dist/cli/commands/dead-code.js.map +1 -0
  136. package/dist/cli/commands/generate-spec.d.ts +11 -0
  137. package/dist/cli/commands/generate-spec.d.ts.map +1 -0
  138. package/dist/cli/commands/generate-spec.js +67 -0
  139. package/dist/cli/commands/generate-spec.js.map +1 -0
  140. package/dist/cli/commands/health.d.ts +11 -0
  141. package/dist/cli/commands/health.d.ts.map +1 -0
  142. package/dist/cli/commands/health.js +67 -0
  143. package/dist/cli/commands/health.js.map +1 -0
  144. package/dist/cli/commands/project.d.ts +21 -0
  145. package/dist/cli/commands/project.d.ts.map +1 -0
  146. package/dist/cli/commands/project.js +92 -0
  147. package/dist/cli/commands/project.js.map +1 -0
  148. package/dist/cli/commands/scan.d.ts +11 -0
  149. package/dist/cli/commands/scan.d.ts.map +1 -0
  150. package/dist/cli/commands/scan.js +68 -0
  151. package/dist/cli/commands/scan.js.map +1 -0
  152. package/dist/cli/commands/secrets.d.ts +11 -0
  153. package/dist/cli/commands/secrets.d.ts.map +1 -0
  154. package/dist/cli/commands/secrets.js +71 -0
  155. package/dist/cli/commands/secrets.js.map +1 -0
  156. package/dist/cli/commands/swarm.d.ts +20 -0
  157. package/dist/cli/commands/swarm.d.ts.map +1 -0
  158. package/dist/cli/commands/swarm.js +174 -0
  159. package/dist/cli/commands/swarm.js.map +1 -0
  160. package/dist/cli/config.d.ts +103 -0
  161. package/dist/cli/config.d.ts.map +1 -0
  162. package/dist/cli/config.js +307 -0
  163. package/dist/cli/config.js.map +1 -0
  164. package/dist/cli/discovery.d.ts +31 -0
  165. package/dist/cli/discovery.d.ts.map +1 -0
  166. package/dist/cli/discovery.js +212 -0
  167. package/dist/cli/discovery.js.map +1 -0
  168. package/dist/cli/formatters/index.d.ts +15 -0
  169. package/dist/cli/formatters/index.d.ts.map +1 -0
  170. package/dist/cli/formatters/index.js +51 -0
  171. package/dist/cli/formatters/index.js.map +1 -0
  172. package/dist/cli/formatters/json.d.ts +11 -0
  173. package/dist/cli/formatters/json.d.ts.map +1 -0
  174. package/dist/cli/formatters/json.js +12 -0
  175. package/dist/cli/formatters/json.js.map +1 -0
  176. package/dist/cli/formatters/project-json.d.ts +11 -0
  177. package/dist/cli/formatters/project-json.d.ts.map +1 -0
  178. package/dist/cli/formatters/project-json.js +12 -0
  179. package/dist/cli/formatters/project-json.js.map +1 -0
  180. package/dist/cli/formatters/project-sarif.d.ts +11 -0
  181. package/dist/cli/formatters/project-sarif.d.ts.map +1 -0
  182. package/dist/cli/formatters/project-sarif.js +127 -0
  183. package/dist/cli/formatters/project-sarif.js.map +1 -0
  184. package/dist/cli/formatters/project-summary.d.ts +11 -0
  185. package/dist/cli/formatters/project-summary.d.ts.map +1 -0
  186. package/dist/cli/formatters/project-summary.js +202 -0
  187. package/dist/cli/formatters/project-summary.js.map +1 -0
  188. package/dist/cli/formatters/sarif-shared.d.ts +101 -0
  189. package/dist/cli/formatters/sarif-shared.d.ts.map +1 -0
  190. package/dist/cli/formatters/sarif-shared.js +57 -0
  191. package/dist/cli/formatters/sarif-shared.js.map +1 -0
  192. package/dist/cli/formatters/sarif.d.ts +12 -0
  193. package/dist/cli/formatters/sarif.d.ts.map +1 -0
  194. package/dist/cli/formatters/sarif.js +92 -0
  195. package/dist/cli/formatters/sarif.js.map +1 -0
  196. package/dist/cli/formatters/summary.d.ts +11 -0
  197. package/dist/cli/formatters/summary.d.ts.map +1 -0
  198. package/dist/cli/formatters/summary.js +240 -0
  199. package/dist/cli/formatters/summary.js.map +1 -0
  200. package/dist/cli/formatters/two-phase-summary.d.ts +11 -0
  201. package/dist/cli/formatters/two-phase-summary.d.ts.map +1 -0
  202. package/dist/cli/formatters/two-phase-summary.js +188 -0
  203. package/dist/cli/formatters/two-phase-summary.js.map +1 -0
  204. package/dist/cli/index.d.ts +15 -0
  205. package/dist/cli/index.d.ts.map +1 -0
  206. package/dist/cli/index.js +555 -0
  207. package/dist/cli/index.js.map +1 -0
  208. package/dist/components/clustering.d.ts +60 -0
  209. package/dist/components/clustering.d.ts.map +1 -0
  210. package/dist/components/clustering.js +129 -0
  211. package/dist/components/clustering.js.map +1 -0
  212. package/dist/components/enrichment.d.ts +45 -0
  213. package/dist/components/enrichment.d.ts.map +1 -0
  214. package/dist/components/enrichment.js +193 -0
  215. package/dist/components/enrichment.js.map +1 -0
  216. package/dist/components/index.d.ts +29 -0
  217. package/dist/components/index.d.ts.map +1 -0
  218. package/dist/components/index.js +56 -0
  219. package/dist/components/index.js.map +1 -0
  220. package/dist/dead-code/detector.d.ts +200 -0
  221. package/dist/dead-code/detector.d.ts.map +1 -0
  222. package/dist/dead-code/detector.js +1003 -0
  223. package/dist/dead-code/detector.js.map +1 -0
  224. package/dist/dead-code/index.d.ts +7 -0
  225. package/dist/dead-code/index.d.ts.map +1 -0
  226. package/dist/dead-code/index.js +7 -0
  227. package/dist/dead-code/index.js.map +1 -0
  228. package/dist/extractors/index.d.ts +15 -0
  229. package/dist/extractors/index.d.ts.map +1 -0
  230. package/dist/extractors/index.js +14 -0
  231. package/dist/extractors/index.js.map +1 -0
  232. package/dist/extractors/natural-language.d.ts +46 -0
  233. package/dist/extractors/natural-language.d.ts.map +1 -0
  234. package/dist/extractors/natural-language.js +228 -0
  235. package/dist/extractors/natural-language.js.map +1 -0
  236. package/dist/extractors/tree-sitter.d.ts +33 -0
  237. package/dist/extractors/tree-sitter.d.ts.map +1 -0
  238. package/dist/extractors/tree-sitter.js +69 -0
  239. package/dist/extractors/tree-sitter.js.map +1 -0
  240. package/dist/extractors/types.d.ts +62 -0
  241. package/dist/extractors/types.d.ts.map +1 -0
  242. package/dist/extractors/types.js +54 -0
  243. package/dist/extractors/types.js.map +1 -0
  244. package/dist/health-score/calculator.d.ts +123 -0
  245. package/dist/health-score/calculator.d.ts.map +1 -0
  246. package/dist/health-score/calculator.js +444 -0
  247. package/dist/health-score/calculator.js.map +1 -0
  248. package/dist/health-score/index.d.ts +12 -0
  249. package/dist/health-score/index.d.ts.map +1 -0
  250. package/dist/health-score/index.js +14 -0
  251. package/dist/health-score/index.js.map +1 -0
  252. package/dist/health-score/metrics.d.ts +142 -0
  253. package/dist/health-score/metrics.d.ts.map +1 -0
  254. package/dist/health-score/metrics.js +332 -0
  255. package/dist/health-score/metrics.js.map +1 -0
  256. package/dist/index.d.ts +26 -0
  257. package/dist/index.d.ts.map +1 -0
  258. package/dist/index.js +43 -0
  259. package/dist/index.js.map +1 -0
  260. package/dist/llm/ax-client.d.ts +477 -0
  261. package/dist/llm/ax-client.d.ts.map +1 -0
  262. package/dist/llm/ax-client.js +1641 -0
  263. package/dist/llm/ax-client.js.map +1 -0
  264. package/dist/llm/config.d.ts +58 -0
  265. package/dist/llm/config.d.ts.map +1 -0
  266. package/dist/llm/config.js +97 -0
  267. package/dist/llm/config.js.map +1 -0
  268. package/dist/llm/discovery.d.ts +123 -0
  269. package/dist/llm/discovery.d.ts.map +1 -0
  270. package/dist/llm/discovery.js +505 -0
  271. package/dist/llm/discovery.js.map +1 -0
  272. package/dist/llm/enrichment.d.ts +108 -0
  273. package/dist/llm/enrichment.d.ts.map +1 -0
  274. package/dist/llm/enrichment.js +312 -0
  275. package/dist/llm/enrichment.js.map +1 -0
  276. package/dist/llm/index.d.ts +13 -0
  277. package/dist/llm/index.d.ts.map +1 -0
  278. package/dist/llm/index.js +22 -0
  279. package/dist/llm/index.js.map +1 -0
  280. package/dist/llm/language-context.d.ts +64 -0
  281. package/dist/llm/language-context.d.ts.map +1 -0
  282. package/dist/llm/language-context.js +492 -0
  283. package/dist/llm/language-context.js.map +1 -0
  284. package/dist/llm/pattern-verification.d.ts +39 -0
  285. package/dist/llm/pattern-verification.d.ts.map +1 -0
  286. package/dist/llm/pattern-verification.js +127 -0
  287. package/dist/llm/pattern-verification.js.map +1 -0
  288. package/dist/llm/prompt-security.d.ts +120 -0
  289. package/dist/llm/prompt-security.d.ts.map +1 -0
  290. package/dist/llm/prompt-security.js +301 -0
  291. package/dist/llm/prompt-security.js.map +1 -0
  292. package/dist/llm/prompts/index.d.ts +31 -0
  293. package/dist/llm/prompts/index.d.ts.map +1 -0
  294. package/dist/llm/prompts/index.js +92 -0
  295. package/dist/llm/prompts/index.js.map +1 -0
  296. package/dist/llm/prompts/rust.d.ts +30 -0
  297. package/dist/llm/prompts/rust.d.ts.map +1 -0
  298. package/dist/llm/prompts/rust.js +121 -0
  299. package/dist/llm/prompts/rust.js.map +1 -0
  300. package/dist/llm/schemas.d.ts +892 -0
  301. package/dist/llm/schemas.d.ts.map +1 -0
  302. package/dist/llm/schemas.js +258 -0
  303. package/dist/llm/schemas.js.map +1 -0
  304. package/dist/llm/verification.d.ts +127 -0
  305. package/dist/llm/verification.d.ts.map +1 -0
  306. package/dist/llm/verification.js +394 -0
  307. package/dist/llm/verification.js.map +1 -0
  308. package/dist/project/analyzer.d.ts +30 -0
  309. package/dist/project/analyzer.d.ts.map +1 -0
  310. package/dist/project/analyzer.js +358 -0
  311. package/dist/project/analyzer.js.map +1 -0
  312. package/dist/project/call-graph.d.ts +22 -0
  313. package/dist/project/call-graph.d.ts.map +1 -0
  314. package/dist/project/call-graph.js +246 -0
  315. package/dist/project/call-graph.js.map +1 -0
  316. package/dist/project/index.d.ts +18 -0
  317. package/dist/project/index.d.ts.map +1 -0
  318. package/dist/project/index.js +20 -0
  319. package/dist/project/index.js.map +1 -0
  320. package/dist/project/taint-paths.d.ts +22 -0
  321. package/dist/project/taint-paths.d.ts.map +1 -0
  322. package/dist/project/taint-paths.js +265 -0
  323. package/dist/project/taint-paths.js.map +1 -0
  324. package/dist/project/two-phase-analyzer.d.ts +143 -0
  325. package/dist/project/two-phase-analyzer.d.ts.map +1 -0
  326. package/dist/project/two-phase-analyzer.js +646 -0
  327. package/dist/project/two-phase-analyzer.js.map +1 -0
  328. package/dist/project/type-hierarchy.d.ts +28 -0
  329. package/dist/project/type-hierarchy.d.ts.map +1 -0
  330. package/dist/project/type-hierarchy.js +218 -0
  331. package/dist/project/type-hierarchy.js.map +1 -0
  332. package/dist/secret-scan/index.d.ts +12 -0
  333. package/dist/secret-scan/index.d.ts.map +1 -0
  334. package/dist/secret-scan/index.js +14 -0
  335. package/dist/secret-scan/index.js.map +1 -0
  336. package/dist/secret-scan/patterns.d.ts +38 -0
  337. package/dist/secret-scan/patterns.d.ts.map +1 -0
  338. package/dist/secret-scan/patterns.js +473 -0
  339. package/dist/secret-scan/patterns.js.map +1 -0
  340. package/dist/secret-scan/scanner.d.ts +162 -0
  341. package/dist/secret-scan/scanner.d.ts.map +1 -0
  342. package/dist/secret-scan/scanner.js +511 -0
  343. package/dist/secret-scan/scanner.js.map +1 -0
  344. package/dist/security-scan/index.d.ts +12 -0
  345. package/dist/security-scan/index.d.ts.map +1 -0
  346. package/dist/security-scan/index.js +15 -0
  347. package/dist/security-scan/index.js.map +1 -0
  348. package/dist/security-scan/owasp-mapping.d.ts +29 -0
  349. package/dist/security-scan/owasp-mapping.d.ts.map +1 -0
  350. package/dist/security-scan/owasp-mapping.js +246 -0
  351. package/dist/security-scan/owasp-mapping.js.map +1 -0
  352. package/dist/security-scan/scanner.d.ts +204 -0
  353. package/dist/security-scan/scanner.d.ts.map +1 -0
  354. package/dist/security-scan/scanner.js +693 -0
  355. package/dist/security-scan/scanner.js.map +1 -0
  356. package/dist/security-scan/trend-tracker.d.ts +150 -0
  357. package/dist/security-scan/trend-tracker.d.ts.map +1 -0
  358. package/dist/security-scan/trend-tracker.js +299 -0
  359. package/dist/security-scan/trend-tracker.js.map +1 -0
  360. package/dist/skills/bundle-loader.d.ts +26 -0
  361. package/dist/skills/bundle-loader.d.ts.map +1 -0
  362. package/dist/skills/bundle-loader.js +284 -0
  363. package/dist/skills/bundle-loader.js.map +1 -0
  364. package/dist/skills/capability-mismatch.d.ts +21 -0
  365. package/dist/skills/capability-mismatch.d.ts.map +1 -0
  366. package/dist/skills/capability-mismatch.js +188 -0
  367. package/dist/skills/capability-mismatch.js.map +1 -0
  368. package/dist/skills/index.d.ts +10 -0
  369. package/dist/skills/index.d.ts.map +1 -0
  370. package/dist/skills/index.js +9 -0
  371. package/dist/skills/index.js.map +1 -0
  372. package/dist/skills/skill-analyzer.d.ts +16 -0
  373. package/dist/skills/skill-analyzer.d.ts.map +1 -0
  374. package/dist/skills/skill-analyzer.js +361 -0
  375. package/dist/skills/skill-analyzer.js.map +1 -0
  376. package/dist/skills/types.d.ts +195 -0
  377. package/dist/skills/types.d.ts.map +1 -0
  378. package/dist/skills/types.js +7 -0
  379. package/dist/skills/types.js.map +1 -0
  380. package/dist/specifica/conflict-resolver.d.ts +23 -0
  381. package/dist/specifica/conflict-resolver.d.ts.map +1 -0
  382. package/dist/specifica/conflict-resolver.js +129 -0
  383. package/dist/specifica/conflict-resolver.js.map +1 -0
  384. package/dist/specifica/evidence-aggregator.d.ts +33 -0
  385. package/dist/specifica/evidence-aggregator.d.ts.map +1 -0
  386. package/dist/specifica/evidence-aggregator.js +236 -0
  387. package/dist/specifica/evidence-aggregator.js.map +1 -0
  388. package/dist/specifica/evidence-extractor.d.ts +13 -0
  389. package/dist/specifica/evidence-extractor.d.ts.map +1 -0
  390. package/dist/specifica/evidence-extractor.js +431 -0
  391. package/dist/specifica/evidence-extractor.js.map +1 -0
  392. package/dist/specifica/feature-clustering.d.ts +19 -0
  393. package/dist/specifica/feature-clustering.d.ts.map +1 -0
  394. package/dist/specifica/feature-clustering.js +231 -0
  395. package/dist/specifica/feature-clustering.js.map +1 -0
  396. package/dist/specifica/generator.d.ts +16 -0
  397. package/dist/specifica/generator.d.ts.map +1 -0
  398. package/dist/specifica/generator.js +277 -0
  399. package/dist/specifica/generator.js.map +1 -0
  400. package/dist/specifica/index.d.ts +15 -0
  401. package/dist/specifica/index.d.ts.map +1 -0
  402. package/dist/specifica/index.js +18 -0
  403. package/dist/specifica/index.js.map +1 -0
  404. package/dist/specifica/prompts.d.ts +21 -0
  405. package/dist/specifica/prompts.d.ts.map +1 -0
  406. package/dist/specifica/prompts.js +196 -0
  407. package/dist/specifica/prompts.js.map +1 -0
  408. package/dist/specifica/spec-generator.d.ts +22 -0
  409. package/dist/specifica/spec-generator.d.ts.map +1 -0
  410. package/dist/specifica/spec-generator.js +229 -0
  411. package/dist/specifica/spec-generator.js.map +1 -0
  412. package/dist/specifica/types.d.ts +213 -0
  413. package/dist/specifica/types.d.ts.map +1 -0
  414. package/dist/specifica/types.js +7 -0
  415. package/dist/specifica/types.js.map +1 -0
  416. package/dist/utils/logger.d.ts +17 -0
  417. package/dist/utils/logger.d.ts.map +1 -0
  418. package/dist/utils/logger.js +51 -0
  419. package/dist/utils/logger.js.map +1 -0
  420. package/package.json +99 -0
@@ -0,0 +1,1641 @@
1
+ /**
2
+ * Ax-LLM Client
3
+ *
4
+ * DSPy-style LLM client using @ax-llm/ax library.
5
+ * Provides typed signatures and structured outputs.
6
+ */
7
+ import { AxAIOpenAI, AxGen, AxSignature, } from '@ax-llm/ax';
8
+ import { getDefaultLLMConfig, validateLLMConfig } from './config.js';
9
+ import { RoleClassificationResponseSchema, SourceDiscoveryResponseSchema, SinkDiscoveryResponseSchema, VerificationResponseSchema, } from './schemas.js';
10
+ import { sanitizeCodeForPrompt, sanitizeListForPrompt, formatSystemPrompt, logInjectionAttempt, } from './prompt-security.js';
11
+ import { getPrompt } from './prompts/index.js';
12
+ // ============================================================================
13
+ // Signatures for Security Analysis
14
+ // ============================================================================
15
+ /**
16
+ * Signature for source/sink enrichment
17
+ */
18
+ export const enrichmentSignature = AxSignature.create(`
19
+ code:string, className:string, methodName:string, existingSources:string[], existingSinks:string[] ->
20
+ additionalSources:object[], additionalSinks:object[], role:string, reasoning:string
21
+ `);
22
+ /**
23
+ * Signature for role classification
24
+ */
25
+ export const roleClassificationSignature = AxSignature.create(`
26
+ className:string, methodNames:string, annotations:string, imports:string ->
27
+ role:string, confidence:number, reasoning:string, indicators:string[]
28
+ `);
29
+ /**
30
+ * Signature for source discovery
31
+ */
32
+ export const sourceDiscoverySignature = AxSignature.create(`
33
+ methodCode:string, methodName:string, classRole:string, existingSources:string ->
34
+ additionalSources:object[]
35
+ `);
36
+ /**
37
+ * Signature for sink discovery
38
+ */
39
+ export const sinkDiscoverySignature = AxSignature.create(`
40
+ methodCode:string, methodName:string, methodCalls:string, existingSinks:string ->
41
+ additionalSinks:object[]
42
+ `);
43
+ /**
44
+ * Signature for virtual call resolution
45
+ */
46
+ export const virtualCallResolutionSignature = AxSignature.create(`
47
+ callExpression:string, interfaceType:string, availableImplementations:string, context:string ->
48
+ resolvedImplementation:string, confidence:number, reasoning:string
49
+ `);
50
+ /**
51
+ * Signature for pattern verification
52
+ */
53
+ export const patternVerificationSignature = AxSignature.create(`
54
+ patterns:string, codeContext:string ->
55
+ verifications:object[]
56
+ `);
57
+ /**
58
+ * Signature for vulnerability verification
59
+ */
60
+ export const verificationSignature = AxSignature.create(`
61
+ sourceCode:string, sourceLine:number, sourceType:string,
62
+ sinkCode:string, sinkLine:number, sinkType:string, cwe:string,
63
+ methodCode:string, methodName:string, className:string,
64
+ sanitizersInPath:string[] ->
65
+ verdict:string, confidence:number, reasoning:string, exploitability:string,
66
+ sanitizersFound:string[], attackVector:string
67
+ `);
68
+ /**
69
+ * Signature for list index tracking
70
+ * Uses JSON strings for compatibility with non-structured LLMs
71
+ */
72
+ export const listIndexSignature = AxSignature.create(`
73
+ code:string, listVariable:string, operationsJson:string "JSON array of operations" ->
74
+ finalIndexMappingJson:string "JSON object mapping original to final indices",
75
+ taintedIndicesJson:string "JSON array of tainted index numbers",
76
+ reasoning:string
77
+ `);
78
+ /**
79
+ * Signature for correlated predicate analysis
80
+ * Uses JSON strings for compatibility with non-structured LLMs
81
+ */
82
+ export const correlatedPredicateSignature = AxSignature.create(`
83
+ code:string, predicateLocationsJson:string "JSON array of predicate locations" ->
84
+ correlatedGroupsJson:string "JSON array of correlated predicate groups",
85
+ reasoning:string
86
+ `);
87
+ /**
88
+ * Signature for cross-file taint tracking
89
+ * Uses JSON string for taintFlows to avoid structured output requirement
90
+ */
91
+ export const crossFileTaintSignature = AxSignature.create(`
92
+ sourceFile:string, sourceCode:string, targetFile:string, targetCode:string,
93
+ exportedTaintJson:string "JSON array of exported taint: [{variable, type, line}]",
94
+ importedSymbolsJson:string "JSON array of imported symbol names" ->
95
+ taintFlowsJson:string "JSON array of flows: [{sourceVar, sinkMethod, sinkLine, confidence, reasoning}]",
96
+ reasoning:string
97
+ `);
98
+ /**
99
+ * Maximum consecutive LLM failures before skipping further calls
100
+ */
101
+ const MAX_CONSECUTIVE_FAILURES = 5;
102
+ /**
103
+ * Maximum code context length to send to LLM (in characters)
104
+ * Prevents 400 errors from exceeding model context limits
105
+ */
106
+ const MAX_CODE_CONTEXT_LENGTH = 8000;
107
+ /**
108
+ * Truncate code to fit within context limits
109
+ */
110
+ function truncateCode(code, maxLength = MAX_CODE_CONTEXT_LENGTH) {
111
+ if (code.length <= maxLength)
112
+ return code;
113
+ // Try to truncate at a line boundary
114
+ const truncated = code.substring(0, maxLength);
115
+ const lastNewline = truncated.lastIndexOf('\n');
116
+ if (lastNewline > maxLength * 0.8) {
117
+ return truncated.substring(0, lastNewline) + '\n// ... (truncated)';
118
+ }
119
+ return truncated + '\n// ... (truncated)';
120
+ }
121
+ /**
122
+ * Ax-LLM based client for Circle-IR
123
+ */
124
+ export class AxLLMClient {
125
+ config;
126
+ ai;
127
+ generators = new Map();
128
+ consecutiveFailures = 0;
129
+ llmDisabled = false;
130
+ constructor(config) {
131
+ this.config = { ...getDefaultLLMConfig(), ...config };
132
+ // Validate required configuration
133
+ validateLLMConfig(this.config);
134
+ // Initialize OpenAI-compatible provider with default model
135
+ this.ai = new AxAIOpenAI({
136
+ apiKey: this.config.apiKey,
137
+ apiURL: this.config.baseUrl,
138
+ config: {
139
+ model: this.config.phases.enrichment.model,
140
+ },
141
+ });
142
+ }
143
+ /**
144
+ * Check if LLM is currently available (not disabled due to failures)
145
+ */
146
+ isAvailable() {
147
+ return !this.llmDisabled;
148
+ }
149
+ /**
150
+ * Reset the circuit breaker so LLM calls are re-enabled.
151
+ * Call this between files to prevent one file's failures from disabling
152
+ * LLM for the entire project analysis.
153
+ */
154
+ resetCircuitBreaker() {
155
+ this.consecutiveFailures = 0;
156
+ this.llmDisabled = false;
157
+ }
158
+ /**
159
+ * Reset failure counter (call when LLM succeeds)
160
+ */
161
+ resetFailures() {
162
+ this.consecutiveFailures = 0;
163
+ }
164
+ /**
165
+ * Track a failure and potentially disable LLM
166
+ */
167
+ trackFailure() {
168
+ this.consecutiveFailures++;
169
+ if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
170
+ console.warn(`LLM disabled after ${MAX_CONSECUTIVE_FAILURES} consecutive failures`);
171
+ this.llmDisabled = true;
172
+ }
173
+ }
174
+ /**
175
+ * Get or create a generator for a signature
176
+ */
177
+ getGenerator(name, signature, options) {
178
+ if (!this.generators.has(name)) {
179
+ const gen = new AxGen(signature, {
180
+ description: options?.description,
181
+ });
182
+ this.generators.set(name, gen);
183
+ }
184
+ return this.generators.get(name);
185
+ }
186
+ /**
187
+ * Get category-specific guidance for verification prompts
188
+ */
189
+ getCategoryGuidance(cwe) {
190
+ const cweNum = cwe.replace(/\D/g, '');
191
+ switch (cweNum) {
192
+ case '501': // Trust Boundary Violation
193
+ return `
194
+ TRUST BOUNDARY (CWE-501) SPECIFIC:
195
+ - VULNERABLE: User input used as session ATTRIBUTE NAME: setAttribute(userInput, value)
196
+ - VULNERABLE: Unvalidated user data stored in session without sanitization
197
+ - SAFE: Constant/hardcoded attribute name with sanitized value
198
+ - SAFE: User data that has been validated against whitelist
199
+ - Key insight: The ATTRIBUTE NAME being user-controlled is the vulnerability, not just the value`;
200
+ case '79': // XSS
201
+ case '80':
202
+ case '81':
203
+ case '83':
204
+ return `
205
+ XSS (CWE-79) SPECIFIC SANITIZERS - Mark as FALSE_POSITIVE if ANY of these are used:
206
+ - encodeForHTML(), escapeHtml(), escapeHtml4()
207
+ - HtmlUtils.htmlEscape(), StringEscapeUtils.escapeHtml()
208
+ - ESAPI.encoder().encodeForHTML()
209
+ - org.owasp.benchmark.helpers.Utils.encodeForHTML()
210
+ - Writing to response HEADERS (not body) is generally safe
211
+ - Check if output is HTML-encoded before reaching response.getWriter()
212
+
213
+ FEW-SHOT EXAMPLES:
214
+ TRUE_POSITIVE:
215
+ String name = request.getParameter("name");
216
+ response.getWriter().println("<h1>" + name + "</h1>");
217
+ // User input directly output without encoding -> VULNERABLE
218
+
219
+ FALSE_POSITIVE:
220
+ String name = request.getParameter("name");
221
+ String safe = ESAPI.encoder().encodeForHTML(name);
222
+ response.getWriter().println("<h1>" + safe + "</h1>");
223
+ // Input is HTML-encoded before output -> SAFE`;
224
+ case '89': // SQL Injection
225
+ return `
226
+ SQL INJECTION (CWE-89) SPECIFIC:
227
+ - SAFE: PreparedStatement with ? placeholders and setString/setInt
228
+ - SAFE: Parameterized queries, named parameters
229
+ - VULNERABLE: String concatenation in SQL query
230
+ - VULNERABLE: Statement.executeQuery(string + userInput)
231
+ - Check if the query uses concatenation vs parameterization
232
+
233
+ FEW-SHOT EXAMPLES:
234
+ TRUE_POSITIVE:
235
+ String id = request.getParameter("id");
236
+ String sql = "SELECT * FROM users WHERE id = '" + id + "'";
237
+ stmt.executeQuery(sql);
238
+ // Concatenation of user input into SQL -> VULNERABLE
239
+
240
+ FALSE_POSITIVE:
241
+ String id = request.getParameter("id");
242
+ PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
243
+ ps.setString(1, id);
244
+ ps.executeQuery();
245
+ // Parameterized query with ? placeholder -> SAFE`;
246
+ case '78': // Command Injection
247
+ return `
248
+ COMMAND INJECTION (CWE-78) SPECIFIC:
249
+ - SAFE: ProcessBuilder with String[] array (no shell interpretation)
250
+ - SAFE: Runtime.exec(String[]) with separate args
251
+ - VULNERABLE: Runtime.exec(String) with concatenated command
252
+ - VULNERABLE: Shell commands with user input
253
+ - Check if command is built via concatenation or array
254
+
255
+ FEW-SHOT EXAMPLES:
256
+ TRUE_POSITIVE:
257
+ String cmd = request.getParameter("cmd");
258
+ Runtime.getRuntime().exec(cmd);
259
+ // User input directly passed to exec() -> VULNERABLE
260
+
261
+ FALSE_POSITIVE:
262
+ String filename = request.getParameter("file");
263
+ String[] cmd = new String[] {"ls", "-l", filename};
264
+ ProcessBuilder pb = new ProcessBuilder(cmd);
265
+ pb.start();
266
+ // Array-based args (no shell), but still potentially dangerous
267
+ // Mark UNCERTAIN if filename not validated, TRUE_POSITIVE if clearly exploitable`;
268
+ case '22': // Path Traversal
269
+ return `
270
+ PATH TRAVERSAL (CWE-22) SPECIFIC:
271
+ - SAFE: Path normalized with getCanonicalPath() then validated
272
+ - SAFE: Paths validated against whitelist/base directory
273
+ - SAFE: FilenameUtils.getName() extracts just filename
274
+ - VULNERABLE: Direct file access with user-controlled path
275
+ - Check for path normalization and validation
276
+
277
+ FEW-SHOT EXAMPLES:
278
+ TRUE_POSITIVE:
279
+ String file = request.getParameter("file");
280
+ File f = new File("/uploads/" + file);
281
+ FileInputStream fis = new FileInputStream(f);
282
+ // User input directly used in file path -> VULNERABLE (../../../etc/passwd)
283
+
284
+ FALSE_POSITIVE:
285
+ String file = request.getParameter("file");
286
+ String safeName = FilenameUtils.getName(file); // Strips directories
287
+ File f = new File("/uploads/" + safeName);
288
+ FileInputStream fis = new FileInputStream(f);
289
+ // Path is sanitized by extracting only filename -> SAFE`;
290
+ case '90': // LDAP Injection
291
+ return `
292
+ LDAP INJECTION (CWE-90) SPECIFIC:
293
+ - SAFE: LDAP search with properly escaped filter
294
+ - SAFE: Using parameterized LDAP APIs
295
+ - VULNERABLE: String concatenation in LDAP filter
296
+ - Check for filter escaping functions`;
297
+ case '643': // XPath Injection
298
+ return `
299
+ XPATH INJECTION (CWE-643) SPECIFIC:
300
+ - SAFE: Parameterized XPath with variable binding
301
+ - SAFE: Input validated against whitelist
302
+ - VULNERABLE: String concatenation in XPath expression
303
+ - Check for XPath parameterization`;
304
+ case '918': // SSRF
305
+ return `
306
+ SSRF (CWE-918) SPECIFIC:
307
+ - VULNERABLE: User input used in URL construction without validation
308
+ - VULNERABLE: URL.openConnection() or HttpClient with user-controlled URL
309
+ - VULNERABLE: RestTemplate, WebClient with user-controlled URLs
310
+ - SAFE: URL validated against allowlist of hosts/protocols
311
+ - SAFE: Internal-only endpoints with no external access
312
+ - Key insight: Any user-controlled URL component is dangerous
313
+
314
+ FEW-SHOT EXAMPLES:
315
+ TRUE_POSITIVE:
316
+ String url = request.getParameter("url");
317
+ URL u = new URL(url);
318
+ HttpURLConnection conn = (HttpURLConnection) u.openConnection();
319
+ conn.getInputStream();
320
+ // User-controlled URL without validation -> VULNERABLE
321
+
322
+ FALSE_POSITIVE:
323
+ String id = request.getParameter("id");
324
+ URL u = new URL("https://api.internal.com/data/" + URLEncoder.encode(id, "UTF-8"));
325
+ // Fixed host, only ID is user-controlled and encoded -> Generally SAFE`;
326
+ case '611': // XXE
327
+ return `
328
+ XXE (CWE-611) SPECIFIC:
329
+ - VULNERABLE: XMLParser without disabled external entities
330
+ - VULNERABLE: DocumentBuilderFactory without setFeature(DISALLOW_DTD)
331
+ - VULNERABLE: SAXParser, XMLReader without secure configuration
332
+ - SAFE: Factory configured with FEATURE_SECURE_PROCESSING
333
+ - SAFE: setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
334
+ - Key insight: Default XML parsers are often vulnerable
335
+
336
+ FEW-SHOT EXAMPLES:
337
+ TRUE_POSITIVE:
338
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
339
+ DocumentBuilder db = dbf.newDocumentBuilder();
340
+ Document doc = db.parse(request.getInputStream());
341
+ // Default XML parser without disabling external entities -> VULNERABLE
342
+
343
+ FALSE_POSITIVE:
344
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
345
+ dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
346
+ DocumentBuilder db = dbf.newDocumentBuilder();
347
+ Document doc = db.parse(request.getInputStream());
348
+ // External entities disabled -> SAFE`;
349
+ case '502': // Deserialization
350
+ return `
351
+ DESERIALIZATION (CWE-502) SPECIFIC:
352
+ - VULNERABLE: ObjectInputStream.readObject() on untrusted data
353
+ - VULNERABLE: XMLDecoder.readObject() on untrusted input
354
+ - VULNERABLE: Yaml.load() without SafeConstructor
355
+ - VULNERABLE: JSON libraries with polymorphic type handling
356
+ - SAFE: Deserialization with allowlist of classes
357
+ - SAFE: SafeYAML, safe JSON configuration
358
+ - Key insight: Any deserialization of untrusted data is dangerous
359
+
360
+ FEW-SHOT EXAMPLES:
361
+ TRUE_POSITIVE:
362
+ ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
363
+ Object obj = ois.readObject();
364
+ // Deserializing untrusted HTTP input -> VULNERABLE
365
+
366
+ FALSE_POSITIVE:
367
+ ObjectInputStream ois = new ObjectInputStream(fileInput); // Internal file
368
+ Object obj = ois.readObject();
369
+ // Deserializing from trusted internal source -> Generally SAFE
370
+ // (but depends on whether the file content is user-controlled)`;
371
+ case '94': // Code Injection
372
+ case '95': // Eval Injection
373
+ return `
374
+ CODE INJECTION (CWE-94/95) SPECIFIC:
375
+ - VULNERABLE: ScriptEngine.eval() with user input
376
+ - VULNERABLE: Runtime.exec() with user-controlled command
377
+ - VULNERABLE: Reflection with user-controlled class/method names
378
+ - VULNERABLE: Expression language with user input (OGNL, SpEL, EL)
379
+ - SAFE: Sandboxed script execution with restricted context
380
+ - Key insight: Any dynamic code execution with user input is dangerous
381
+
382
+ FEW-SHOT EXAMPLES:
383
+ TRUE_POSITIVE:
384
+ String expr = request.getParameter("expr");
385
+ ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
386
+ engine.eval(expr);
387
+ // User input directly executed as script -> VULNERABLE
388
+
389
+ FALSE_POSITIVE:
390
+ String key = request.getParameter("key");
391
+ String value = config.get(key); // Looking up config by key
392
+ // User controls config key but not code execution -> SAFE`;
393
+ case '327': // Weak Crypto
394
+ case '328': // Reversible One-Way Hash
395
+ return `
396
+ WEAK CRYPTOGRAPHY (CWE-327/328) SPECIFIC:
397
+ - VULNERABLE: MD5, SHA1 for password hashing
398
+ - VULNERABLE: DES, 3DES, RC4 for encryption
399
+ - VULNERABLE: ECB mode for block ciphers
400
+ - SAFE: bcrypt, scrypt, Argon2 for passwords
401
+ - SAFE: AES-GCM, ChaCha20-Poly1305 for encryption`;
402
+ case '352': // CSRF
403
+ return `
404
+ CSRF (CWE-352) SPECIFIC:
405
+ - VULNERABLE: State-changing operations without CSRF token
406
+ - VULNERABLE: POST/PUT/DELETE endpoints without token validation
407
+ - SAFE: CSRF token validated on state-changing requests
408
+ - SAFE: SameSite cookie attributes with proper configuration`;
409
+ case '200': // Information Exposure
410
+ case '209': // Error Information Exposure
411
+ return `
412
+ INFORMATION EXPOSURE (CWE-200/209) SPECIFIC:
413
+ - VULNERABLE: Stack traces shown to users
414
+ - VULNERABLE: Internal paths, database info in error messages
415
+ - VULNERABLE: Debug information in production
416
+ - SAFE: Generic error messages with logged details
417
+ - SAFE: Custom error pages without sensitive info`;
418
+ default:
419
+ return '';
420
+ }
421
+ }
422
+ /**
423
+ * Make a raw JSON chat completion call
424
+ * Public to allow batch verification and other custom prompts
425
+ * @param phase - Which phase config to use ('enrichment', 'verification', or 'componentEnrichment')
426
+ * @param retryCount - Internal retry counter (don't set manually)
427
+ */
428
+ async chatJSON(systemPrompt, userPrompt, phase = 'enrichment', retryCount = 0) {
429
+ // Skip if LLM has been disabled due to repeated failures
430
+ if (this.llmDisabled) {
431
+ return null;
432
+ }
433
+ // Use the correct phase config
434
+ const phaseConfig = this.config.phases[phase];
435
+ // On retry, request fewer tokens to reduce truncation risk
436
+ const maxRetries = 1;
437
+ const maxTokens = retryCount > 0
438
+ ? Math.min(phaseConfig.maxTokens, 4000) // Reduced tokens on retry
439
+ : phaseConfig.maxTokens;
440
+ try {
441
+ // Create abort controller with timeout
442
+ const controller = new AbortController();
443
+ const timeoutMs = phaseConfig.timeout || 60000;
444
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
445
+ try {
446
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
447
+ method: 'POST',
448
+ headers: {
449
+ 'Content-Type': 'application/json',
450
+ 'Authorization': `Bearer ${this.config.apiKey}`,
451
+ },
452
+ body: JSON.stringify({
453
+ model: phaseConfig.model,
454
+ messages: [
455
+ { role: 'system', content: systemPrompt },
456
+ { role: 'user', content: userPrompt },
457
+ ],
458
+ max_tokens: maxTokens,
459
+ temperature: phaseConfig.temperature,
460
+ }),
461
+ signal: controller.signal,
462
+ });
463
+ clearTimeout(timeoutId);
464
+ if (!response.ok) {
465
+ console.error('LLM call failed: HTTP', response.status);
466
+ this.trackFailure();
467
+ return null;
468
+ }
469
+ // Success - reset failure counter
470
+ this.resetFailures();
471
+ const data = await response.json();
472
+ const rawContent = data.choices?.[0]?.message?.content;
473
+ // Handle case where LLM returns JSON object directly (not as string)
474
+ if (rawContent && typeof rawContent === 'object') {
475
+ return rawContent;
476
+ }
477
+ const content = typeof rawContent === 'string' ? rawContent : '';
478
+ // Parse JSON from response (handle markdown code blocks)
479
+ let jsonStr = content;
480
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
481
+ if (jsonMatch) {
482
+ jsonStr = jsonMatch[1];
483
+ }
484
+ // Fix common JSON issues from LLM
485
+ jsonStr = jsonStr.trim();
486
+ // Fix word-numbers (e.g., "0. nine" -> "0.9", "0. Nine" -> "0.9")
487
+ const wordToNum = {
488
+ 'zero': '0', 'one': '1', 'two': '2', 'three': '3', 'four': '4',
489
+ 'five': '5', 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9',
490
+ };
491
+ jsonStr = jsonStr.replace(/(\d+)\.\s*(zero|one|two|three|four|five|six|seven|eight|nine)/gi, (_, int, word) => `${int}.${wordToNum[word.toLowerCase()]}`);
492
+ // Fix trailing decimal points followed by any non-digit (e.g., 0. -> 0.0)
493
+ jsonStr = jsonStr.replace(/(\d+)\.\s*([,}\]\n\r])/g, '$1.0$2');
494
+ // Fix decimal points followed by space then digit (e.g., 0. 5 -> 0.5)
495
+ jsonStr = jsonStr.replace(/(\d+)\.\s+(\d)/g, '$1.$2');
496
+ // Fix missing quotes around string values that look like identifiers
497
+ jsonStr = jsonStr.replace(/:\s*([A-Z_]+)\s*([,}\]])/g, ': "$1"$2');
498
+ try {
499
+ return JSON.parse(jsonStr);
500
+ }
501
+ catch {
502
+ // Try to recover truncated JSON by completing brackets
503
+ const recovered = this.tryRecoverTruncatedJSON(jsonStr);
504
+ if (recovered) {
505
+ try {
506
+ return JSON.parse(recovered);
507
+ }
508
+ catch {
509
+ // Recovery failed, try verification-specific recovery
510
+ }
511
+ }
512
+ // Try to extract verification response from truncated JSON
513
+ const verificationRecovered = this.tryRecoverVerificationJSON(jsonStr);
514
+ if (verificationRecovered) {
515
+ return verificationRecovered;
516
+ }
517
+ // Log the problematic JSON for debugging
518
+ console.error('Failed to parse JSON:', jsonStr.substring(0, 100));
519
+ // Retry with reduced tokens if this was the first attempt
520
+ if (retryCount < maxRetries) {
521
+ console.log('Retrying LLM call with reduced tokens...');
522
+ return this.chatJSON(systemPrompt, userPrompt, phase, retryCount + 1);
523
+ }
524
+ return null;
525
+ }
526
+ }
527
+ catch (fetchError) {
528
+ clearTimeout(timeoutId);
529
+ if (fetchError instanceof Error && fetchError.name === 'AbortError') {
530
+ console.error(`LLM call timed out (attempt ${retryCount + 1}/${maxRetries + 1})`);
531
+ // Retry on timeout with extended timeout
532
+ if (retryCount < maxRetries) {
533
+ console.log('Retrying LLM call with reduced tokens...');
534
+ return this.chatJSON(systemPrompt, userPrompt, phase, retryCount + 1);
535
+ }
536
+ }
537
+ else {
538
+ console.error('LLM fetch failed:', fetchError);
539
+ // Retry on network errors if this was the first attempt
540
+ if (retryCount < maxRetries) {
541
+ console.log('Retrying LLM call due to network error...');
542
+ return this.chatJSON(systemPrompt, userPrompt, phase, retryCount + 1);
543
+ }
544
+ }
545
+ this.trackFailure();
546
+ return null;
547
+ }
548
+ }
549
+ catch (error) {
550
+ console.error('LLM JSON call failed:', error);
551
+ this.trackFailure();
552
+ return null;
553
+ }
554
+ }
555
+ /**
556
+ * Try to recover truncated JSON by completing missing brackets
557
+ * Handles cases where LLM output was cut off mid-response
558
+ */
559
+ tryRecoverTruncatedJSON(jsonStr) {
560
+ // Count open brackets
561
+ let braces = 0;
562
+ let brackets = 0;
563
+ let inString = false;
564
+ let escaped = false;
565
+ for (const char of jsonStr) {
566
+ if (escaped) {
567
+ escaped = false;
568
+ continue;
569
+ }
570
+ if (char === '\\') {
571
+ escaped = true;
572
+ continue;
573
+ }
574
+ if (char === '"') {
575
+ inString = !inString;
576
+ continue;
577
+ }
578
+ if (inString)
579
+ continue;
580
+ if (char === '{')
581
+ braces++;
582
+ if (char === '}')
583
+ braces--;
584
+ if (char === '[')
585
+ brackets++;
586
+ if (char === ']')
587
+ brackets--;
588
+ }
589
+ // If we're truncated mid-string, close it
590
+ if (inString) {
591
+ jsonStr = jsonStr + '"';
592
+ }
593
+ // Truncate to last complete property (find last comma or colon not in value position)
594
+ // If we ended mid-value, try to find the last complete key-value pair
595
+ const lastValidEnd = this.findLastCompleteValue(jsonStr);
596
+ if (lastValidEnd > 0 && lastValidEnd < jsonStr.length - 1) {
597
+ jsonStr = jsonStr.substring(0, lastValidEnd);
598
+ }
599
+ // Add missing brackets
600
+ while (brackets > 0) {
601
+ jsonStr += ']';
602
+ brackets--;
603
+ }
604
+ while (braces > 0) {
605
+ jsonStr += '}';
606
+ braces--;
607
+ }
608
+ // Quick validation - should start with { or [
609
+ const trimmed = jsonStr.trim();
610
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
611
+ return null;
612
+ }
613
+ return jsonStr;
614
+ }
615
+ /**
616
+ * Find the position of the last complete JSON value
617
+ */
618
+ findLastCompleteValue(jsonStr) {
619
+ // Look for patterns that indicate a complete value:
620
+ // - number followed by comma or bracket: 0.9, 0.9] 0.9}
621
+ // - string followed by comma or bracket: "text", "text"] "text"}
622
+ // - boolean/null followed by comma or bracket: true, false] null}
623
+ // - closing bracket followed by comma or bracket: ], ]] ]} }, }] }}
624
+ let lastGoodPos = jsonStr.length;
625
+ let inString = false;
626
+ let escaped = false;
627
+ for (let i = jsonStr.length - 1; i >= 0; i--) {
628
+ const char = jsonStr[i];
629
+ // Handle string detection (going backwards)
630
+ if (char === '"' && !escaped) {
631
+ // Check if this quote is escaped (odd number of preceding backslashes)
632
+ let backslashes = 0;
633
+ for (let j = i - 1; j >= 0 && jsonStr[j] === '\\'; j--) {
634
+ backslashes++;
635
+ }
636
+ if (backslashes % 2 === 0) {
637
+ inString = !inString;
638
+ }
639
+ }
640
+ if (inString)
641
+ continue;
642
+ // Found a good endpoint - comma, closing bracket not followed by incomplete value
643
+ if (char === ',' || char === ']' || char === '}') {
644
+ // Check what comes before to ensure it's a complete value
645
+ const before = jsonStr.substring(0, i + 1).trim();
646
+ try {
647
+ // Try to parse as partial JSON to validate
648
+ JSON.parse(before + (char === ',' ? '{}]}' : ''));
649
+ }
650
+ catch {
651
+ // If that fails, this is a good truncation point
652
+ return i + 1;
653
+ }
654
+ return i + 1;
655
+ }
656
+ }
657
+ return lastGoodPos;
658
+ }
659
+ /**
660
+ * Try to recover a truncated verification response by extracting key fields
661
+ * Even if reasoning is cut off, we can still use the verdict and confidence
662
+ */
663
+ tryRecoverVerificationJSON(jsonStr) {
664
+ // Look for verdict pattern
665
+ const verdictMatch = jsonStr.match(/"verdict"\s*:\s*"(TRUE_POSITIVE|FALSE_POSITIVE|UNCERTAIN)"/i);
666
+ if (!verdictMatch)
667
+ return null;
668
+ const verdict = verdictMatch[1].toUpperCase();
669
+ // Look for confidence pattern
670
+ const confidenceMatch = jsonStr.match(/"confidence"\s*:\s*(0?\.\d+|1(?:\.0)?|\d+\.\d+)/);
671
+ const confidence = confidenceMatch ? Math.min(1, Math.max(0, parseFloat(confidenceMatch[1]))) : 0.5;
672
+ // Look for exploitability pattern
673
+ const exploitabilityMatch = jsonStr.match(/"exploitability"\s*:\s*"(high|medium|low|none)"/i);
674
+ const exploitability = exploitabilityMatch ? exploitabilityMatch[1].toLowerCase() : 'none';
675
+ // Extract partial reasoning (up to truncation)
676
+ let reasoning = 'Response truncated';
677
+ const reasoningMatch = jsonStr.match(/"reasoning"\s*:\s*"([^"]*)/);
678
+ if (reasoningMatch && reasoningMatch[1].length > 10) {
679
+ reasoning = reasoningMatch[1] + '... [truncated]';
680
+ }
681
+ // Try to extract sanitizersFound if present
682
+ const sanitizersMatch = jsonStr.match(/"sanitizersFound"\s*:\s*\[(.*?)\]/);
683
+ let sanitizersFound = [];
684
+ if (sanitizersMatch) {
685
+ const sanitizerStrings = sanitizersMatch[1].match(/"([^"]+)"/g);
686
+ if (sanitizerStrings) {
687
+ sanitizersFound = sanitizerStrings.map(s => s.replace(/"/g, ''));
688
+ }
689
+ }
690
+ // Try to extract attackVector if present
691
+ const attackVectorMatch = jsonStr.match(/"attackVector"\s*:\s*"([^"]*)/);
692
+ const attackVector = attackVectorMatch ? attackVectorMatch[1] : '';
693
+ return {
694
+ verdict,
695
+ confidence,
696
+ reasoning,
697
+ exploitability,
698
+ sanitizersFound,
699
+ attackVector,
700
+ };
701
+ }
702
+ /**
703
+ * Classify the role of a class
704
+ */
705
+ async classifyRole(input) {
706
+ // Sanitize inputs to prevent prompt injection
707
+ const safeClassName = sanitizeCodeForPrompt(input.className);
708
+ const safeMethodNames = sanitizeCodeForPrompt(input.methodNames);
709
+ const safeAnnotations = sanitizeCodeForPrompt(input.annotations);
710
+ const safeImports = sanitizeCodeForPrompt(input.imports);
711
+ // Log potential injection attempts
712
+ logInjectionAttempt(input.imports, 'classifyRole.imports');
713
+ const model = this.config.phases.enrichment.model;
714
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code. Respond only in valid JSON format.', model);
715
+ const userPrompt = `Classify the role of this Java class:
716
+ Class: ${safeClassName}
717
+ Methods: ${safeMethodNames}
718
+ Annotations: ${safeAnnotations}
719
+ Imports: ${safeImports}
720
+
721
+ Respond with JSON: {"role": "controller|service|repository|utility|entity|unknown", "confidence": 0.0-1.0, "reasoning": "explanation", "indicators": ["list", "of", "indicators"]}`;
722
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
723
+ const parseResult = RoleClassificationResponseSchema.safeParse(rawResult);
724
+ if (!parseResult.success) {
725
+ console.warn('classifyRole validation failed:', parseResult.error.issues);
726
+ return { role: 'unknown', confidence: 0, reasoning: 'LLM call failed', indicators: [] };
727
+ }
728
+ return parseResult.data;
729
+ }
730
+ /**
731
+ * Discover additional taint sources in a method
732
+ */
733
+ async discoverSources(input) {
734
+ // Sanitize and truncate inputs to prevent prompt injection and context overflow
735
+ const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
736
+ const safeMethodName = sanitizeCodeForPrompt(input.methodName);
737
+ const safeClassRole = sanitizeCodeForPrompt(input.classRole);
738
+ const safeExistingSources = sanitizeCodeForPrompt(input.existingSources);
739
+ // Log potential injection attempts
740
+ logInjectionAttempt(input.methodCode, 'discoverSources.methodCode');
741
+ const model = this.config.phases.enrichment.model;
742
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code for taint sources. Respond only in valid JSON format.', model);
743
+ const userPrompt = `Find additional user-controlled input sources in this method:
744
+
745
+ Method: ${safeMethodName}
746
+ Class role: ${safeClassRole}
747
+ Already identified sources: ${safeExistingSources}
748
+
749
+ [CODE START]
750
+ ${safeMethodCode}
751
+ [CODE END]
752
+
753
+ Look for: HTTP parameters, headers, cookies, request body, file input, environment variables.
754
+ Ignore: constants, internal config, hardcoded values.
755
+
756
+ Respond with JSON: {"additionalSources": [{"line": 10, "variable": "param", "type": "http_param", "confidence": 0.9, "reasoning": "..."}]}`;
757
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
758
+ const parseResult = SourceDiscoveryResponseSchema.safeParse(rawResult);
759
+ if (!parseResult.success) {
760
+ console.warn('discoverSources validation failed:', parseResult.error.issues);
761
+ return [];
762
+ }
763
+ return parseResult.data.additionalSources;
764
+ }
765
+ /**
766
+ * Discover additional taint sinks in a method
767
+ */
768
+ async discoverSinks(input) {
769
+ // Sanitize and truncate inputs to prevent prompt injection and context overflow
770
+ const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
771
+ const safeMethodName = sanitizeCodeForPrompt(input.methodName);
772
+ const safeMethodCalls = sanitizeCodeForPrompt(input.methodCalls);
773
+ const safeExistingSinks = sanitizeCodeForPrompt(input.existingSinks);
774
+ // Log potential injection attempts
775
+ logInjectionAttempt(input.methodCode, 'discoverSinks.methodCode');
776
+ const model = this.config.phases.enrichment.model;
777
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code for dangerous sinks. Respond only in valid JSON format.', model);
778
+ const userPrompt = `Find additional dangerous operations (sinks) in this method:
779
+
780
+ Method: ${safeMethodName}
781
+ Method calls: ${safeMethodCalls}
782
+ Already identified sinks: ${safeExistingSinks}
783
+
784
+ [CODE START]
785
+ ${safeMethodCode}
786
+ [CODE END]
787
+
788
+ Look for: SQL queries, command execution, file operations, XSS output, deserialization.
789
+ Ignore: PreparedStatement with ?, logging, safe APIs.
790
+
791
+ Respond with JSON: {"additionalSinks": [{"line": 15, "method": "executeQuery", "type": "sql_injection", "cwe": "CWE-89", "argPositions": [0], "confidence": 0.9, "reasoning": "..."}]}`;
792
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
793
+ const parseResult = SinkDiscoveryResponseSchema.safeParse(rawResult);
794
+ if (!parseResult.success) {
795
+ console.warn('discoverSinks validation failed:', parseResult.error.issues);
796
+ return [];
797
+ }
798
+ return parseResult.data.additionalSinks;
799
+ }
800
+ // ==========================================================================
801
+ // Language-Aware Methods (for multi-language support)
802
+ // ==========================================================================
803
+ /**
804
+ * Classify role using a custom language-specific prompt
805
+ */
806
+ async classifyRoleWithPrompt(promptTemplate, input) {
807
+ // Sanitize inputs to prevent prompt injection
808
+ const safeClassName = sanitizeCodeForPrompt(input.className);
809
+ const safeMethodNames = sanitizeCodeForPrompt(input.methodNames);
810
+ const safeAnnotations = sanitizeCodeForPrompt(input.annotations);
811
+ const safeImports = sanitizeCodeForPrompt(input.imports);
812
+ // Log potential injection attempts
813
+ logInjectionAttempt(input.imports, 'classifyRoleWithPrompt.imports');
814
+ const model = this.config.phases.enrichment.model;
815
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing code. Respond only in valid JSON format.', model);
816
+ const userPrompt = promptTemplate
817
+ .replace('{className}', safeClassName)
818
+ .replace('{methodNames}', safeMethodNames)
819
+ .replace('{annotations}', safeAnnotations)
820
+ .replace('{imports}', safeImports);
821
+ const result = await this.chatJSON(systemPrompt, userPrompt);
822
+ if (!result) {
823
+ return { role: 'unknown', confidence: 0, reasoning: 'LLM call failed', indicators: [] };
824
+ }
825
+ const role = (result.role || 'unknown').toLowerCase();
826
+ const validRoles = ['controller', 'service', 'repository', 'utility', 'entity', 'unknown'];
827
+ return {
828
+ role: validRoles.includes(role) ? role : 'unknown',
829
+ confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
830
+ reasoning: result.reasoning || '',
831
+ indicators: result.indicators || [],
832
+ };
833
+ }
834
+ /**
835
+ * Discover sources using a custom language-specific prompt
836
+ */
837
+ async discoverSourcesWithPrompt(promptTemplate, input) {
838
+ // Sanitize and truncate inputs to prevent prompt injection and context overflow
839
+ const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
840
+ const safeMethodName = sanitizeCodeForPrompt(input.methodName);
841
+ const safeClassRole = sanitizeCodeForPrompt(input.classRole);
842
+ const safeExistingSources = sanitizeCodeForPrompt(input.existingSources);
843
+ // Log potential injection attempts
844
+ logInjectionAttempt(input.methodCode, 'discoverSourcesWithPrompt.methodCode');
845
+ const model = this.config.phases.enrichment.model;
846
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing code for taint sources. Respond only in valid JSON format.', model);
847
+ const userPrompt = promptTemplate
848
+ .replace('{methodCode}', safeMethodCode)
849
+ .replace('{methodName}', safeMethodName)
850
+ .replace('{classRole}', safeClassRole)
851
+ .replace('{existingSources}', safeExistingSources);
852
+ const result = await this.chatJSON(systemPrompt, userPrompt);
853
+ if (!result)
854
+ return [];
855
+ return (result.additionalSources || []).map(s => ({
856
+ line: s.line || 0,
857
+ variable: s.variable || '',
858
+ type: s.type || 'unknown',
859
+ confidence: s.confidence || 0.5,
860
+ reasoning: s.reasoning || '',
861
+ }));
862
+ }
863
+ /**
864
+ * Discover sinks using a custom language-specific prompt
865
+ */
866
+ async discoverSinksWithPrompt(promptTemplate, input) {
867
+ // Sanitize and truncate inputs to prevent prompt injection and context overflow
868
+ const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
869
+ const safeMethodName = sanitizeCodeForPrompt(input.methodName);
870
+ const safeMethodCalls = sanitizeCodeForPrompt(input.methodCalls);
871
+ const safeExistingSinks = sanitizeCodeForPrompt(input.existingSinks);
872
+ // Log potential injection attempts
873
+ logInjectionAttempt(input.methodCode, 'discoverSinksWithPrompt.methodCode');
874
+ const model = this.config.phases.enrichment.model;
875
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing code for dangerous sinks. Respond only in valid JSON format.', model);
876
+ const userPrompt = promptTemplate
877
+ .replace('{methodCode}', safeMethodCode)
878
+ .replace('{methodName}', safeMethodName)
879
+ .replace('{methodCalls}', safeMethodCalls)
880
+ .replace('{existingSinks}', safeExistingSinks);
881
+ const result = await this.chatJSON(systemPrompt, userPrompt);
882
+ if (!result)
883
+ return [];
884
+ return (result.additionalSinks || []).map(s => ({
885
+ line: s.line || 0,
886
+ method: s.method || '',
887
+ type: s.type || 'unknown',
888
+ cwe: s.cwe || 'CWE-unknown',
889
+ argPositions: s.argPositions || [0],
890
+ confidence: s.confidence || 0.5,
891
+ reasoning: s.reasoning || '',
892
+ }));
893
+ }
894
+ /**
895
+ * Resolve virtual/interface method calls to implementations
896
+ */
897
+ async resolveVirtualCall(input) {
898
+ // Sanitize inputs to prevent prompt injection
899
+ const safeCallExpression = sanitizeCodeForPrompt(input.callExpression);
900
+ const safeInterfaceType = sanitizeCodeForPrompt(input.interfaceType);
901
+ const safeImplementations = sanitizeCodeForPrompt(input.availableImplementations);
902
+ const safeContext = sanitizeCodeForPrompt(input.context);
903
+ // Log potential injection attempts
904
+ logInjectionAttempt(input.context, 'resolveVirtualCall.context');
905
+ const model = this.config.phases.enrichment.model;
906
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code. Respond only in valid JSON format.', model);
907
+ const userPrompt = `Which implementation is most likely being called?
908
+
909
+ Call: ${safeCallExpression}
910
+ Interface: ${safeInterfaceType}
911
+ Available implementations: ${safeImplementations}
912
+ Context: ${safeContext}
913
+
914
+ Respond with JSON: {"resolvedImplementation": "ClassName", "confidence": 0.9, "reasoning": "..."}`;
915
+ const result = await this.chatJSON(systemPrompt, userPrompt);
916
+ if (!result) {
917
+ return { resolvedImplementation: '', confidence: 0, reasoning: 'LLM call failed' };
918
+ }
919
+ return {
920
+ resolvedImplementation: result.resolvedImplementation || '',
921
+ confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
922
+ reasoning: result.reasoning || '',
923
+ };
924
+ }
925
+ /**
926
+ * Verify a batch of discovered patterns
927
+ */
928
+ async verifyPatterns(input) {
929
+ // Sanitize inputs to prevent prompt injection
930
+ const safePatterns = sanitizeCodeForPrompt(input.patterns);
931
+ const safeCodeContext = sanitizeCodeForPrompt(input.codeContext);
932
+ // Log potential injection attempts
933
+ logInjectionAttempt(input.codeContext, 'verifyPatterns.codeContext');
934
+ const model = this.config.phases.enrichment.model;
935
+ const systemPrompt = formatSystemPrompt('You are a security expert verifying potential taint sources and sinks. Respond only in valid JSON format.', model);
936
+ const userPrompt = `Verify these potential security patterns:
937
+
938
+ ${safePatterns}
939
+
940
+ ${safeCodeContext ? `Code context:\n[CODE START]\n${safeCodeContext}\n[CODE END]` : ''}
941
+
942
+ For each pattern, determine if it's a valid source/sink. Respond with JSON: {"verifications": [{"method": "...", "class": "...", "isValid": true, "confidence": 0.9, "reasoning": "..."}]}`;
943
+ const result = await this.chatJSON(systemPrompt, userPrompt);
944
+ if (!result) {
945
+ return { verifications: [] };
946
+ }
947
+ return {
948
+ verifications: (result.verifications || []).map(v => ({
949
+ method: v.method || '',
950
+ class: v.class || '',
951
+ isValid: v.isValid === true,
952
+ confidence: typeof v.confidence === 'number' ? v.confidence : 0.5,
953
+ reasoning: v.reasoning || 'No reasoning provided',
954
+ suggestedType: v.suggestedType,
955
+ suggestedCwe: v.suggestedCwe,
956
+ })),
957
+ };
958
+ }
959
+ /**
960
+ * Run enrichment to discover additional sources/sinks
961
+ */
962
+ async enrich(input) {
963
+ // Sanitize inputs to prevent prompt injection
964
+ const safeCode = sanitizeCodeForPrompt(input.code);
965
+ const safeClassName = sanitizeCodeForPrompt(input.className);
966
+ const safeMethodName = sanitizeCodeForPrompt(input.methodName);
967
+ const safeSources = sanitizeListForPrompt(input.existingSources);
968
+ const safeSinks = sanitizeListForPrompt(input.existingSinks);
969
+ // Log potential injection attempts
970
+ logInjectionAttempt(input.code, 'enrich.code');
971
+ const model = this.config.phases.enrichment.model;
972
+ const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code for taint sources and sinks. Respond only in valid JSON format.', model);
973
+ const userPrompt = `Analyze this code for additional sources and sinks:
974
+
975
+ Class: ${safeClassName}
976
+ Method: ${safeMethodName}
977
+ Existing sources: ${safeSources.join(', ') || 'none'}
978
+ Existing sinks: ${safeSinks.join(', ') || 'none'}
979
+
980
+ [CODE START]
981
+ ${safeCode}
982
+ [CODE END]
983
+
984
+ Respond with JSON: {"additionalSources": [{"line": 10, "type": "http_param", "variable": "input", "confidence": 0.9}], "additionalSinks": [{"line": 20, "type": "sql_injection", "method": "executeQuery", "cwe": "CWE-89", "confidence": 0.9}], "role": "controller", "reasoning": "..."}`;
985
+ const result = await this.chatJSON(systemPrompt, userPrompt);
986
+ if (!result) {
987
+ return { additionalSources: [], additionalSinks: [], role: 'unknown', reasoning: 'LLM call failed' };
988
+ }
989
+ return {
990
+ additionalSources: result.additionalSources || [],
991
+ additionalSinks: result.additionalSinks || [],
992
+ role: result.role || 'unknown',
993
+ reasoning: result.reasoning || '',
994
+ };
995
+ }
996
+ /**
997
+ * Pre-check for obvious sanitization patterns to improve accuracy.
998
+ * Returns null if no obvious pattern is detected (proceed with LLM).
999
+ */
1000
+ preCheckSanitization(methodCode, cwe, sanitizersInPath) {
1001
+ const lowerCode = methodCode.toLowerCase();
1002
+ // Check explicit sanitizers from static analysis
1003
+ if (sanitizersInPath.length > 0) {
1004
+ const knownSanitizers = sanitizersInPath.filter(s =>
1005
+ // SQL/Injection
1006
+ s.includes('prepareStatement') ||
1007
+ s.includes('setString') ||
1008
+ s.includes('setInt') ||
1009
+ // XSS
1010
+ s.includes('escapeHtml') ||
1011
+ s.includes('encodeForHTML') ||
1012
+ s.includes('htmlEscape') ||
1013
+ // Path validation
1014
+ s.includes('PathUtils') ||
1015
+ s.includes('FilenameUtils') ||
1016
+ s.includes('getCanonicalPath') ||
1017
+ // Allowlist
1018
+ s.includes('RedirectUtils') ||
1019
+ s.includes('verify') ||
1020
+ s.includes('validate'));
1021
+ if (knownSanitizers.length > 0) {
1022
+ return { isSafe: true, reason: `Known sanitizer found: ${knownSanitizers[0]}` };
1023
+ }
1024
+ }
1025
+ // CWE-022 (Path Traversal) - Check for path validation patterns
1026
+ if (cwe === 'CWE-022') {
1027
+ // PathUtils validation
1028
+ if ((lowerCode.includes('pathutils.isinvalidpath') ||
1029
+ lowerCode.includes('pathutils.isinvalidencodedpath')) &&
1030
+ lowerCode.includes('throw')) {
1031
+ return { isSafe: true, reason: 'PathUtils validation detected' };
1032
+ }
1033
+ // FilenameUtils
1034
+ if (lowerCode.includes('filenameutils.getname')) {
1035
+ return { isSafe: true, reason: 'FilenameUtils.getName() detected' };
1036
+ }
1037
+ // Canonical path checks (Zip Slip mitigation)
1038
+ if (lowerCode.includes('getcanonicalpath') &&
1039
+ lowerCode.includes('startswith') &&
1040
+ (lowerCode.includes('throw') || lowerCode.includes('return'))) {
1041
+ return { isSafe: true, reason: 'Canonical path check detected (Zip Slip mitigation)' };
1042
+ }
1043
+ // Custom path validation functions
1044
+ if (lowerCode.includes('ensurefilewithinbundledir') ||
1045
+ lowerCode.includes('checkdestinationfilefortraversal')) {
1046
+ return { isSafe: true, reason: 'Custom path validation function detected' };
1047
+ }
1048
+ }
1049
+ // CWE-079 (XSS/Open Redirect) - Check for encoding and validation
1050
+ if (cwe === 'CWE-079') {
1051
+ // HTML encoding
1052
+ if (lowerCode.includes('encodeforhtml') ||
1053
+ lowerCode.includes('escapehtml') ||
1054
+ lowerCode.includes('htmlescape') ||
1055
+ lowerCode.includes('htmlutils.htmlescape') ||
1056
+ lowerCode.includes('esapi.encoder')) {
1057
+ return { isSafe: true, reason: 'HTML encoding function detected' };
1058
+ }
1059
+ // Redirect validation
1060
+ if (lowerCode.includes('redirectutils.verifyredirecturi') ||
1061
+ (lowerCode.includes('verify') && lowerCode.includes('redirect') && lowerCode.includes('uri'))) {
1062
+ return { isSafe: true, reason: 'Redirect URI validation detected' };
1063
+ }
1064
+ }
1065
+ // CWE-089 (SQL Injection) - Check for prepared statements
1066
+ if (cwe === 'CWE-089' || cwe === 'CWE-89') {
1067
+ if (lowerCode.includes('preparestatement') &&
1068
+ (lowerCode.includes('setstring') || lowerCode.includes('setint') || lowerCode.includes('setlong'))) {
1069
+ return { isSafe: true, reason: 'PreparedStatement with parameter binding detected' };
1070
+ }
1071
+ }
1072
+ // CWE-094 (Code Injection) - Check for safe APIs
1073
+ if (cwe === 'CWE-094') {
1074
+ // SafeConstructor for YAML
1075
+ if (lowerCode.includes('safeconstructor')) {
1076
+ return { isSafe: true, reason: 'SafeConstructor detected (safe YAML deserialization)' };
1077
+ }
1078
+ // ProcessBuilder with array arguments
1079
+ if (lowerCode.includes('processbuilder') &&
1080
+ (lowerCode.includes('new string[]') || lowerCode.includes('arrays.aslist'))) {
1081
+ return { isSafe: true, reason: 'ProcessBuilder with array arguments detected' };
1082
+ }
1083
+ }
1084
+ // CWE-078 (Command Injection) - Check for ProcessBuilder
1085
+ if (cwe === 'CWE-078') {
1086
+ if (lowerCode.includes('processbuilder') &&
1087
+ (lowerCode.includes('new string[]') || lowerCode.includes('arrays.aslist'))) {
1088
+ return { isSafe: true, reason: 'ProcessBuilder with array arguments detected' };
1089
+ }
1090
+ }
1091
+ // No obvious pattern detected
1092
+ return null;
1093
+ }
1094
+ /**
1095
+ * Verify if a potential vulnerability is exploitable
1096
+ */
1097
+ async verify(input) {
1098
+ // Pre-check for obvious sanitization patterns
1099
+ const preCheck = this.preCheckSanitization(input.methodCode, input.cwe, input.sanitizersInPath);
1100
+ if (preCheck && preCheck.isSafe) {
1101
+ return {
1102
+ verdict: 'FALSE_POSITIVE',
1103
+ confidence: 0.9,
1104
+ reasoning: `Pre-check detected safe pattern: ${preCheck.reason}`,
1105
+ exploitability: 'none',
1106
+ sanitizersFound: input.sanitizersInPath,
1107
+ attackVector: '',
1108
+ };
1109
+ }
1110
+ // Simple prompt - ask if there's sanitization
1111
+ const systemPrompt = `You verify security findings. Check if data is sanitized between source and sink.
1112
+
1113
+ SANITIZER = a function that transforms data to remove/escape dangerous characters:
1114
+ - Known: encodeForHTML, escapeHtml, PreparedStatement.setString, clean(), sanitize(), encode()
1115
+ - Custom: any function that escapes <>&" or uses parameterized queries
1116
+
1117
+ RULES:
1118
+ 1. Default: TRUE_POSITIVE (vulnerable)
1119
+ 2. FALSE_POSITIVE only if: sanitizer function is called on tainted data before reaching sink
1120
+ 3. If unsure: TRUE_POSITIVE
1121
+
1122
+ JSON only.`;
1123
+ // Sanitize and truncate inputs to prevent prompt injection and context overflow
1124
+ const safeSourceCode = sanitizeCodeForPrompt(input.sourceCode);
1125
+ const safeSinkCode = sanitizeCodeForPrompt(input.sinkCode);
1126
+ const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
1127
+ const safeClassName = sanitizeCodeForPrompt(input.className);
1128
+ const safeMethodName = sanitizeCodeForPrompt(input.methodName);
1129
+ const safeSanitizers = sanitizeListForPrompt(input.sanitizersInPath);
1130
+ // Log potential injection attempts
1131
+ logInjectionAttempt(input.methodCode, 'verify.methodCode');
1132
+ const userPrompt = `${input.cwe}: Line ${input.sourceLine} → Line ${input.sinkLine}
1133
+
1134
+ \`\`\`java
1135
+ ${safeMethodCode}
1136
+ \`\`\`
1137
+
1138
+ Does the tainted data pass through ANY function that escapes/encodes it before reaching the sink?
1139
+
1140
+ Respond: {"verdict": "TRUE_POSITIVE|FALSE_POSITIVE", "confidence": 0.9, "reasoning": "why", "exploitability": "high|low|none", "sanitizersFound": ["function_name"], "attackVector": "attack description"}`;
1141
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt, 'verification');
1142
+ const parseResult = VerificationResponseSchema.safeParse(rawResult);
1143
+ if (!parseResult.success) {
1144
+ console.warn('verify validation failed:', parseResult.error.issues);
1145
+ return {
1146
+ verdict: 'UNCERTAIN',
1147
+ confidence: 0,
1148
+ reasoning: 'LLM call failed',
1149
+ exploitability: 'none',
1150
+ sanitizersFound: [],
1151
+ attackVector: '',
1152
+ };
1153
+ }
1154
+ const result = parseResult.data;
1155
+ // Post-processing: FALSE_POSITIVE must have evidence
1156
+ // If LLM says FALSE_POSITIVE but can't cite a specific sanitizer, flip to TRUE_POSITIVE
1157
+ let finalVerdict = result.verdict;
1158
+ if (result.verdict === 'FALSE_POSITIVE') {
1159
+ const hasSanitizerEvidence = result.sanitizersFound.length > 0 ||
1160
+ result.reasoning.toLowerCase().includes('preparedstatement') ||
1161
+ result.reasoning.toLowerCase().includes('encodefor') ||
1162
+ result.reasoning.toLowerCase().includes('escapehtml') ||
1163
+ result.reasoning.toLowerCase().includes('htmlescape') ||
1164
+ result.reasoning.toLowerCase().includes('esapi') ||
1165
+ result.reasoning.toLowerCase().includes('parameterized') ||
1166
+ result.reasoning.toLowerCase().includes('constant') ||
1167
+ result.reasoning.toLowerCase().includes('hardcoded') ||
1168
+ result.reasoning.toLowerCase().includes('literal');
1169
+ if (!hasSanitizerEvidence) {
1170
+ // No clear sanitizer evidence - flip to TRUE_POSITIVE
1171
+ finalVerdict = 'TRUE_POSITIVE';
1172
+ }
1173
+ }
1174
+ return {
1175
+ verdict: finalVerdict,
1176
+ confidence: result.confidence,
1177
+ reasoning: result.reasoning,
1178
+ exploitability: result.exploitability,
1179
+ sanitizersFound: result.sanitizersFound,
1180
+ attackVector: result.attackVector,
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Analyze list operations to track taint through index changes
1185
+ */
1186
+ async analyzeListIndices(input) {
1187
+ // Sanitize inputs to prevent prompt injection
1188
+ const safeCode = sanitizeCodeForPrompt(input.code);
1189
+ const safeListVariable = sanitizeCodeForPrompt(input.listVariable);
1190
+ const safeOperations = sanitizeListForPrompt(input.operations);
1191
+ // Log potential injection attempts
1192
+ logInjectionAttempt(input.code, 'analyzeListIndices.code');
1193
+ const model = this.config.phases.enrichment.model;
1194
+ const systemPrompt = formatSystemPrompt(`You are a security expert analyzing list/array operations. Track how indices shift when elements are added or removed.
1195
+
1196
+ CRITICAL: When list.remove(i) is called, ALL indices > i shift down by 1.
1197
+ Example: [a, TAINTED, c] after remove(0) becomes [TAINTED, c] - taint is now at index 0!
1198
+
1199
+ Respond only in valid JSON format.`, model);
1200
+ const userPrompt = `Analyze list operations for variable "${safeListVariable}":
1201
+
1202
+ Code:
1203
+ [CODE START]
1204
+ ${safeCode}
1205
+ [CODE END]
1206
+
1207
+ Operations in order: ${safeOperations.join(', ')}
1208
+
1209
+ Track each element through all add/remove operations. Determine which indices contain tainted values after ALL operations complete.
1210
+
1211
+ Respond with JSON: {"finalIndexMapping": {"0": {"value": "name", "tainted": true}, "1": {"value": "safe", "tainted": false}}, "taintedIndices": [0], "reasoning": "step-by-step trace"}`;
1212
+ const result = await this.chatJSON(systemPrompt, userPrompt);
1213
+ if (!result) {
1214
+ return { finalIndexMapping: {}, taintedIndices: [], reasoning: 'LLM call failed' };
1215
+ }
1216
+ // Convert string keys to numbers
1217
+ const mapping = {};
1218
+ for (const [key, val] of Object.entries(result.finalIndexMapping || {})) {
1219
+ mapping[parseInt(key, 10)] = val;
1220
+ }
1221
+ return {
1222
+ finalIndexMapping: mapping,
1223
+ taintedIndices: result.taintedIndices || [],
1224
+ reasoning: result.reasoning || '',
1225
+ };
1226
+ }
1227
+ /**
1228
+ * Analyze correlated predicates to detect related conditions
1229
+ */
1230
+ async analyzeCorrelatedPredicates(input) {
1231
+ const gen = this.getGenerator('correlatedPredicate', correlatedPredicateSignature, {
1232
+ description: 'Identify predicates that are correlated (use same variables, imply each other)',
1233
+ });
1234
+ // Convert to JSON string for compatibility
1235
+ const result = await gen.forward(this.ai, {
1236
+ code: input.code,
1237
+ predicateLocationsJson: JSON.stringify(input.predicateLocations),
1238
+ });
1239
+ // Parse JSON response
1240
+ let correlatedGroups = [];
1241
+ try {
1242
+ const groupsJson = result.correlatedGroupsJson || '[]';
1243
+ const parsed = JSON.parse(groupsJson.replace(/```json\n?|\n?```/g, '').trim());
1244
+ correlatedGroups = Array.isArray(parsed) ? parsed : [];
1245
+ }
1246
+ catch {
1247
+ correlatedGroups = [];
1248
+ }
1249
+ return {
1250
+ correlatedGroups,
1251
+ reasoning: result.reasoning || '',
1252
+ };
1253
+ }
1254
+ /**
1255
+ * Track taint across file boundaries
1256
+ * Uses direct chat completion for better compatibility with local LLMs
1257
+ */
1258
+ async analyzeCrossFileTaint(input) {
1259
+ // Sanitize inputs to prevent prompt injection
1260
+ const safeSourceFile = sanitizeCodeForPrompt(input.sourceFile);
1261
+ const safeSourceCode = sanitizeCodeForPrompt(input.sourceCode);
1262
+ const safeTargetFile = sanitizeCodeForPrompt(input.targetFile);
1263
+ const safeTargetCode = sanitizeCodeForPrompt(input.targetCode);
1264
+ const safeImportedSymbols = sanitizeListForPrompt(input.importedSymbols);
1265
+ // Log potential injection attempts
1266
+ logInjectionAttempt(input.sourceCode, 'analyzeCrossFileTaint.sourceCode');
1267
+ logInjectionAttempt(input.targetCode, 'analyzeCrossFileTaint.targetCode');
1268
+ // Use direct chat completion for better compatibility
1269
+ const prompt = `Analyze taint flow between these two files:
1270
+
1271
+ SOURCE FILE: ${safeSourceFile}
1272
+ [CODE START]
1273
+ ${safeSourceCode}
1274
+ [CODE END]
1275
+
1276
+ TARGET FILE: ${safeTargetFile}
1277
+ [CODE START]
1278
+ ${safeTargetCode}
1279
+ [CODE END]
1280
+
1281
+ TAINTED DATA EXPORTED FROM SOURCE:
1282
+ ${JSON.stringify(input.exportedTaint, null, 2)}
1283
+
1284
+ SYMBOLS IMPORTED BY TARGET:
1285
+ ${safeImportedSymbols.join(', ')}
1286
+
1287
+ Does tainted data flow from the source file to the target file? If yes, trace the flow.
1288
+ ${input.candidateContext ? `\nCANDIDATE TAINT PATHS (verify these):\n${input.candidateContext}\n` : ''}
1289
+ Respond in this exact JSON format:
1290
+ {
1291
+ "taintFlows": [
1292
+ {
1293
+ "sourceSymbol": "variable/method that exports taint",
1294
+ "targetSymbol": "variable/method that receives taint",
1295
+ "flowType": "direct|transitive|conditional",
1296
+ "confidence": 0.0-1.0
1297
+ }
1298
+ ],
1299
+ "reasoning": "Brief explanation of the taint flow analysis"
1300
+ }
1301
+
1302
+ If no taint flows exist, return: {"taintFlows": [], "reasoning": "No cross-file taint detected"}`;
1303
+ try {
1304
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
1305
+ method: 'POST',
1306
+ headers: {
1307
+ 'Content-Type': 'application/json',
1308
+ 'Authorization': `Bearer ${this.config.apiKey}`,
1309
+ },
1310
+ body: JSON.stringify({
1311
+ model: this.config.phases.verification.model,
1312
+ messages: [{ role: 'user', content: prompt }],
1313
+ max_tokens: 2000,
1314
+ temperature: 0.1,
1315
+ }),
1316
+ });
1317
+ if (!response.ok) {
1318
+ return { taintFlows: [], reasoning: `LLM request failed: ${response.status}` };
1319
+ }
1320
+ const data = await response.json();
1321
+ const rawContent = data.choices?.[0]?.message?.content;
1322
+ // Handle both string and object content (some providers return structured output directly)
1323
+ let parsed = null;
1324
+ if (typeof rawContent === 'object' && rawContent !== null && !Array.isArray(rawContent)) {
1325
+ // Content is already a structured object
1326
+ parsed = rawContent;
1327
+ }
1328
+ else {
1329
+ // Content is a string - need to parse JSON from it
1330
+ let content;
1331
+ if (typeof rawContent === 'string') {
1332
+ content = rawContent;
1333
+ }
1334
+ else if (Array.isArray(rawContent)) {
1335
+ content = rawContent.map((block) => {
1336
+ if (typeof block === 'string')
1337
+ return block;
1338
+ if (typeof block === 'object' && block !== null && 'text' in block) {
1339
+ return block.text;
1340
+ }
1341
+ return JSON.stringify(block);
1342
+ }).join('');
1343
+ }
1344
+ else {
1345
+ content = String(rawContent || '');
1346
+ }
1347
+ // Extract JSON from response
1348
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
1349
+ if (jsonMatch) {
1350
+ try {
1351
+ parsed = JSON.parse(jsonMatch[0]);
1352
+ }
1353
+ catch {
1354
+ // JSON parse failed
1355
+ }
1356
+ }
1357
+ }
1358
+ if (!parsed) {
1359
+ return { taintFlows: [], reasoning: 'Could not parse LLM response' };
1360
+ }
1361
+ const taintFlows = Array.isArray(parsed.taintFlows)
1362
+ ? parsed.taintFlows.filter((f) => {
1363
+ const flow = f;
1364
+ return typeof flow === 'object' &&
1365
+ flow !== null &&
1366
+ typeof flow.sourceSymbol === 'string' &&
1367
+ typeof flow.targetSymbol === 'string';
1368
+ })
1369
+ : [];
1370
+ return {
1371
+ taintFlows,
1372
+ reasoning: parsed.reasoning || '',
1373
+ };
1374
+ }
1375
+ catch (error) {
1376
+ return { taintFlows: [], reasoning: `Error: ${error instanceof Error ? error.message : String(error)}` };
1377
+ }
1378
+ }
1379
+ /**
1380
+ * Test connection to LLM
1381
+ */
1382
+ async testConnection() {
1383
+ try {
1384
+ // Simple raw API call to test connectivity
1385
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
1386
+ method: 'POST',
1387
+ headers: {
1388
+ 'Content-Type': 'application/json',
1389
+ 'Authorization': `Bearer ${this.config.apiKey}`,
1390
+ },
1391
+ body: JSON.stringify({
1392
+ model: this.config.phases.enrichment.model,
1393
+ messages: [{ role: 'user', content: 'Say OK' }],
1394
+ max_tokens: 10,
1395
+ }),
1396
+ });
1397
+ if (!response.ok) {
1398
+ console.error('LLM connection test failed: HTTP', response.status);
1399
+ return false;
1400
+ }
1401
+ const data = await response.json();
1402
+ const content = data.choices?.[0]?.message?.content || '';
1403
+ return content.toLowerCase().includes('ok');
1404
+ }
1405
+ catch (error) {
1406
+ console.error('LLM connection test failed:', error);
1407
+ return false;
1408
+ }
1409
+ }
1410
+ /**
1411
+ * Get current configuration
1412
+ */
1413
+ getConfig() {
1414
+ return this.config;
1415
+ }
1416
+ /**
1417
+ * Get the AI service instance
1418
+ */
1419
+ getAI() {
1420
+ return this.ai;
1421
+ }
1422
+ /**
1423
+ * Get enrichment-specific configuration
1424
+ */
1425
+ getEnrichmentConfig() {
1426
+ return this.config.enrichment;
1427
+ }
1428
+ /**
1429
+ * Get verification-specific configuration
1430
+ */
1431
+ getVerificationConfig() {
1432
+ return this.config.verification;
1433
+ }
1434
+ /**
1435
+ * Get phase-specific configuration
1436
+ */
1437
+ getPhaseConfig(phase) {
1438
+ return this.config.phases[phase];
1439
+ }
1440
+ // ==========================================================================
1441
+ // Language-Aware Methods
1442
+ // ==========================================================================
1443
+ /**
1444
+ * Classify role using language-specific prompt
1445
+ */
1446
+ async classifyRoleForLanguage(language, input) {
1447
+ const model = this.config.phases.enrichment.model;
1448
+ const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
1449
+ const userPrompt = getPrompt(language, 'classifyRole', {
1450
+ moduleName: sanitizeCodeForPrompt(input.moduleName),
1451
+ className: sanitizeCodeForPrompt(input.moduleName), // Java compat
1452
+ functionNames: sanitizeCodeForPrompt(input.functionNames),
1453
+ methodNames: sanitizeCodeForPrompt(input.functionNames), // Java compat
1454
+ attributes: sanitizeCodeForPrompt(input.attributes),
1455
+ annotations: sanitizeCodeForPrompt(input.attributes), // Java compat
1456
+ uses: sanitizeCodeForPrompt(input.uses),
1457
+ imports: sanitizeCodeForPrompt(input.uses), // Java compat
1458
+ });
1459
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
1460
+ const parseResult = RoleClassificationResponseSchema.safeParse(rawResult);
1461
+ if (!parseResult.success) {
1462
+ return { role: 'unknown', confidence: 0, reasoning: 'LLM call failed', indicators: [] };
1463
+ }
1464
+ return parseResult.data;
1465
+ }
1466
+ /**
1467
+ * Discover sources using language-specific prompt
1468
+ */
1469
+ async discoverSourcesForLanguage(language, input) {
1470
+ const safeCode = truncateCode(sanitizeCodeForPrompt(input.code));
1471
+ const model = this.config.phases.enrichment.model;
1472
+ const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
1473
+ const userPrompt = getPrompt(language, 'discoverSources', {
1474
+ code: safeCode,
1475
+ functionName: sanitizeCodeForPrompt(input.functionName),
1476
+ methodName: sanitizeCodeForPrompt(input.functionName), // Java compat
1477
+ moduleRole: sanitizeCodeForPrompt(input.moduleRole),
1478
+ classRole: sanitizeCodeForPrompt(input.moduleRole), // Java compat
1479
+ existingSources: sanitizeCodeForPrompt(input.existingSources),
1480
+ });
1481
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
1482
+ const parseResult = SourceDiscoveryResponseSchema.safeParse(rawResult);
1483
+ if (!parseResult.success) {
1484
+ return [];
1485
+ }
1486
+ return parseResult.data.additionalSources;
1487
+ }
1488
+ /**
1489
+ * Discover sinks using language-specific prompt
1490
+ */
1491
+ async discoverSinksForLanguage(language, input) {
1492
+ const safeCode = truncateCode(sanitizeCodeForPrompt(input.code));
1493
+ const model = this.config.phases.enrichment.model;
1494
+ const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
1495
+ const userPrompt = getPrompt(language, 'discoverSinks', {
1496
+ code: safeCode,
1497
+ functionName: sanitizeCodeForPrompt(input.functionName),
1498
+ methodName: sanitizeCodeForPrompt(input.functionName), // Java compat
1499
+ methodCalls: sanitizeCodeForPrompt(input.methodCalls),
1500
+ existingSinks: sanitizeCodeForPrompt(input.existingSinks),
1501
+ });
1502
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
1503
+ const parseResult = SinkDiscoveryResponseSchema.safeParse(rawResult);
1504
+ if (!parseResult.success) {
1505
+ return [];
1506
+ }
1507
+ return parseResult.data.additionalSinks;
1508
+ }
1509
+ /**
1510
+ * Verify vulnerability using language-specific prompt
1511
+ */
1512
+ async verifyForLanguage(language, input) {
1513
+ const safeFunctionCode = truncateCode(sanitizeCodeForPrompt(input.functionCode));
1514
+ const model = this.config.phases.verification.model;
1515
+ const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
1516
+ const userPrompt = getPrompt(language, 'verify', {
1517
+ cwe: input.cwe,
1518
+ sourceLine: String(input.sourceLine),
1519
+ sourceCode: sanitizeCodeForPrompt(input.sourceCode),
1520
+ sinkLine: String(input.sinkLine),
1521
+ sinkCode: sanitizeCodeForPrompt(input.sinkCode),
1522
+ functionCode: safeFunctionCode,
1523
+ methodCode: safeFunctionCode, // Java compat
1524
+ functionName: sanitizeCodeForPrompt(input.functionName),
1525
+ methodName: sanitizeCodeForPrompt(input.functionName), // Java compat
1526
+ moduleName: sanitizeCodeForPrompt(input.moduleName),
1527
+ className: sanitizeCodeForPrompt(input.moduleName), // Java compat
1528
+ sanitizers: input.sanitizers.join(', ') || 'none',
1529
+ });
1530
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt);
1531
+ const parseResult = VerificationResponseSchema.safeParse(rawResult);
1532
+ if (!parseResult.success) {
1533
+ return {
1534
+ verdict: 'UNCERTAIN',
1535
+ confidence: 0,
1536
+ reasoning: 'LLM call failed',
1537
+ exploitability: 'none',
1538
+ sanitizersFound: [],
1539
+ attackVector: '',
1540
+ };
1541
+ }
1542
+ return parseResult.data;
1543
+ }
1544
+ /**
1545
+ * Generate Specifica specification from code evidence
1546
+ */
1547
+ async generateSpecification(input) {
1548
+ const model = this.config.phases.enrichment.model;
1549
+ const systemPrompt = formatSystemPrompt('You are a technical specification writer. Analyze code and generate requirements specifications in JSON format.', model);
1550
+ // Build concise description of the code
1551
+ const apiSummary = input.apiEndpoints.length > 0
1552
+ ? `\nAPI Endpoints:\n${input.apiEndpoints.slice(0, 5).map(e => `- ${e.method} ${e.path} (${e.handler})`).join('\n')}`
1553
+ : '';
1554
+ const methodSummary = input.methods.length > 0
1555
+ ? `\nMethods:\n${input.methods.slice(0, 10).map(m => `- ${m.signature}`).join('\n')}`
1556
+ : '';
1557
+ const vulnSummary = input.vulnerabilities.length > 0
1558
+ ? `\nSecurity Issues:\n${input.vulnerabilities.map(v => `- ${v.cwe}: ${v.description}`).join('\n')}`
1559
+ : '';
1560
+ const userPrompt = `Generate a requirements specification for this code:
1561
+
1562
+ **File**: ${sanitizeCodeForPrompt(input.filePath)}
1563
+ **Language**: ${input.language}
1564
+ ${input.framework ? `**Framework**: ${input.framework}` : ''}
1565
+ **Lines of Code**: ${input.linesOfCode}${apiSummary}${methodSummary}${vulnSummary}
1566
+
1567
+ Generate a JSON specification answering: "What does this code do?"
1568
+
1569
+ Output this exact JSON structure:
1570
+ {
1571
+ "title": "Feature Name",
1572
+ "summary": "One-paragraph description of what this feature does",
1573
+ "requirements": [
1574
+ {
1575
+ "id": "REQ-001",
1576
+ "description": "Testable requirement as a checkbox item",
1577
+ "category": "functional|security|performance|usability",
1578
+ "priority": "must|should|could",
1579
+ "tested": false
1580
+ }
1581
+ ],
1582
+ "edgeCases": [
1583
+ "Edge case 1",
1584
+ "Edge case 2"
1585
+ ],
1586
+ "errorScenarios": [
1587
+ "Error scenario 1",
1588
+ "Error scenario 2"
1589
+ ]
1590
+ }
1591
+
1592
+ Guidelines:
1593
+ 1. Requirements must be testable - each should be something you can write a test for
1594
+ 2. Focus on WHAT, not HOW - describe behavior, not implementation
1595
+ 3. Extract from code behavior - infer requirements from what the code actually does
1596
+ 4. Include security requirements based on detected sources/sinks/vulnerabilities
1597
+ 5. Identify edge cases - unusual inputs, error conditions, boundary cases
1598
+ 6. Error scenarios - what can go wrong?`;
1599
+ const rawResult = await this.chatJSON(systemPrompt, userPrompt, 'enrichment');
1600
+ if (!rawResult) {
1601
+ return null;
1602
+ }
1603
+ // Validate basic structure
1604
+ if (typeof rawResult !== 'object' || rawResult === null) {
1605
+ console.warn('generateSpecification: Invalid response structure');
1606
+ return null;
1607
+ }
1608
+ const spec = rawResult;
1609
+ if (!spec.title || !spec.summary || !Array.isArray(spec.requirements)) {
1610
+ console.warn('generateSpecification: Missing required fields');
1611
+ return null;
1612
+ }
1613
+ // Ensure requirements have proper structure
1614
+ const requirements = spec.requirements.map((req, index) => ({
1615
+ id: req.id || `REQ-${String(index + 1).padStart(3, '0')}`,
1616
+ description: req.description || 'No description provided',
1617
+ category: ['functional', 'security', 'performance', 'usability'].includes(req.category)
1618
+ ? req.category
1619
+ : 'functional',
1620
+ priority: ['must', 'should', 'could'].includes(req.priority)
1621
+ ? req.priority
1622
+ : 'should',
1623
+ tested: Boolean(req.tested),
1624
+ }));
1625
+ return {
1626
+ title: spec.title,
1627
+ summary: spec.summary,
1628
+ requirements,
1629
+ edgeCases: Array.isArray(spec.edgeCases) ? spec.edgeCases : [],
1630
+ errorScenarios: Array.isArray(spec.errorScenarios) ? spec.errorScenarios : [],
1631
+ };
1632
+ }
1633
+ }
1634
+ /**
1635
+ * Get a new Ax-LLM client instance
1636
+ * Always creates a fresh instance for per-request isolation
1637
+ */
1638
+ export function getAxLLMClient(config) {
1639
+ return new AxLLMClient(config);
1640
+ }
1641
+ //# sourceMappingURL=ax-client.js.map