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