@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
package/dist/index.js ADDED
@@ -0,0 +1,648 @@
1
+ "use strict";
2
+ /**
3
+ * Scanner Orchestrator
4
+ * Coordinates all three scanning layers and produces final results
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.validateFindingsWithAI = exports.buildProjectContext = exports.runLayer3Scan = exports.runLayer2Scan = exports.runLayer1Scan = void 0;
22
+ exports.runScan = runScan;
23
+ exports.computeIssueMixFromVulnerabilities = computeIssueMixFromVulnerabilities;
24
+ const types_1 = require("./types");
25
+ const layer1_1 = require("./layer1");
26
+ const layer2_1 = require("./layer2");
27
+ const layer3_1 = require("./layer3");
28
+ const anthropic_1 = require("./layer3/anthropic");
29
+ const urls_1 = require("./layer1/urls");
30
+ const middleware_detector_1 = require("./utils/middleware-detector");
31
+ const auth_helper_detector_1 = require("./utils/auth-helper-detector");
32
+ const imported_auth_detector_1 = require("./utils/imported-auth-detector");
33
+ // Tier system imports for filtering by scan depth
34
+ const tiers_1 = require("./tiers");
35
+ // Maximum candidates per file to send to AI validation (cost control)
36
+ const MAX_VALIDATION_CANDIDATES_PER_FILE = 10;
37
+ /**
38
+ * Classify vulnerabilities by their detector tier
39
+ */
40
+ function classifyByTier(vulnerabilities) {
41
+ const result = {
42
+ core: [],
43
+ ai_assisted: [],
44
+ experimental: [],
45
+ };
46
+ for (const vuln of vulnerabilities) {
47
+ const tier = (0, tiers_1.getTierForCategory)(vuln.category, vuln.layer);
48
+ result[tier].push(vuln);
49
+ }
50
+ return result;
51
+ }
52
+ /**
53
+ * Filter vulnerabilities based on scan depth and tier
54
+ *
55
+ * - cheap: Only Tier A (core) findings are surfaced
56
+ * - validated: Tier A visible, Tier B goes through AI validation
57
+ * - deep: Same as validated, plus Layer 3 semantic analysis
58
+ *
59
+ * Tier C (experimental) is NEVER surfaced to users, regardless of depth.
60
+ */
61
+ function filterByTierAndDepth(vulnerabilities, depth) {
62
+ const classified = classifyByTier(vulnerabilities);
63
+ const tierStats = (0, tiers_1.computeTierStats)(vulnerabilities.map(v => ({ category: v.category, layer: v.layer })));
64
+ switch (depth) {
65
+ case 'cheap':
66
+ // Only Tier A is visible, no AI validation
67
+ return {
68
+ toSurface: classified.core,
69
+ toValidate: [],
70
+ hidden: [...classified.ai_assisted, ...classified.experimental],
71
+ tierStats,
72
+ };
73
+ case 'validated':
74
+ case 'deep':
75
+ // Tier A always surfaces, Tier B goes to AI validation
76
+ // For findings that already require AI validation, only include Tier A/B
77
+ const coreRequiringValidation = classified.core.filter(v => v.requiresAIValidation ||
78
+ v.category === 'high_entropy_string' ||
79
+ v.category === 'hardcoded_secret' ||
80
+ (v.category === 'sensitive_url' && v.severity !== 'info'));
81
+ const coreNotRequiringValidation = classified.core.filter(v => !coreRequiringValidation.includes(v));
82
+ return {
83
+ toSurface: coreNotRequiringValidation,
84
+ toValidate: [...coreRequiringValidation, ...classified.ai_assisted],
85
+ hidden: classified.experimental,
86
+ tierStats,
87
+ };
88
+ }
89
+ }
90
+ /**
91
+ * Resolve scan mode configuration from options
92
+ *
93
+ * Handles both scan mode (full/incremental) and depth (cheap/validated/deep).
94
+ * Depth controls AI usage:
95
+ * - cheap: skip AI validation + skip Layer 3
96
+ * - validated: run AI validation, skip Layer 3
97
+ * - deep: run AI validation + Layer 3
98
+ */
99
+ function resolveScanModeConfig(options) {
100
+ const scanModeOption = options.scanMode;
101
+ // Determine base mode
102
+ const mode = !scanModeOption ? 'full'
103
+ : typeof scanModeOption === 'string' ? scanModeOption
104
+ : scanModeOption.mode;
105
+ const defaults = types_1.SCAN_MODE_DEFAULTS[mode];
106
+ // Build config from defaults + explicit overrides
107
+ let config = {
108
+ ...defaults,
109
+ mode,
110
+ ...(typeof scanModeOption === 'object' ? scanModeOption : {}),
111
+ };
112
+ // Apply depth mode (default: 'cheap')
113
+ const depth = options.scanDepth ?? 'cheap';
114
+ config.scanDepth = depth;
115
+ // Map depth to skipAIValidation/skipLayer3 unless explicitly overridden
116
+ const hasExplicitSkipAI = typeof scanModeOption === 'object' && scanModeOption.skipAIValidation !== undefined;
117
+ const hasExplicitSkipL3 = typeof scanModeOption === 'object' && scanModeOption.skipLayer3 !== undefined;
118
+ if (!hasExplicitSkipAI) {
119
+ config.skipAIValidation = depth === 'cheap';
120
+ }
121
+ if (!hasExplicitSkipL3) {
122
+ config.skipLayer3 = depth !== 'deep';
123
+ }
124
+ return config;
125
+ }
126
+ /**
127
+ * Run a complete security scan on the provided files
128
+ *
129
+ * Supports two scan modes:
130
+ * - full: Complete scan with AI validation on all files (initial onboarding, deep audits)
131
+ * - incremental: Focused scan on changed files only (CI/CD, fast feedback)
132
+ */
133
+ async function runScan(files, repoInfo, options = {}, onProgress) {
134
+ const startTime = Date.now();
135
+ const allVulnerabilities = [];
136
+ // Resolve scan mode configuration
137
+ const scanModeConfig = resolveScanModeConfig(options);
138
+ const isIncremental = scanModeConfig.mode === 'incremental';
139
+ const depth = scanModeConfig.scanDepth || 'cheap';
140
+ console.log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`);
141
+ if (isIncremental && scanModeConfig.changedFiles) {
142
+ console.log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`);
143
+ }
144
+ // Report progress helper
145
+ const reportProgress = (status, message, vulnerabilitiesFound = allVulnerabilities.length) => {
146
+ if (onProgress) {
147
+ onProgress({
148
+ status,
149
+ message,
150
+ filesProcessed: files.length,
151
+ totalFiles: files.length,
152
+ vulnerabilitiesFound,
153
+ });
154
+ }
155
+ };
156
+ // For incremental scans, filter to only changed files for AI-heavy operations
157
+ // but still run static analysis on all files for context
158
+ const filesForAI = isIncremental && scanModeConfig.changedFiles
159
+ ? files.filter(f => scanModeConfig.changedFiles.some(cf => f.path.endsWith(cf) || f.path.includes(cf)))
160
+ : files;
161
+ try {
162
+ // Detect global auth middleware before scanning (always on all files for context)
163
+ const middlewareConfig = (0, middleware_detector_1.detectGlobalAuthMiddleware)(files);
164
+ if (middlewareConfig.hasAuthMiddleware) {
165
+ console.log(`[Scanner] repo=${repoInfo.name} auth_middleware=${middlewareConfig.authType || 'unknown'} file=${middlewareConfig.middlewareFile}`);
166
+ }
167
+ // Build imported auth registry for cross-file middleware detection
168
+ const fileAuthImports = (0, imported_auth_detector_1.buildFileAuthImports)(files);
169
+ const filesWithImportedAuth = Array.from(fileAuthImports.values()).filter(f => f.usesImportedAuth).length;
170
+ if (filesWithImportedAuth > 0) {
171
+ console.log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`);
172
+ }
173
+ // Layer 1: Surface Scan
174
+ reportProgress('layer1', 'Running surface scan (patterns, entropy, config)...');
175
+ let layer1Result = await (0, layer1_1.runLayer1Scan)(files);
176
+ // Aggregate repeated localhost findings
177
+ const layer1RawCount = layer1Result.vulnerabilities.length;
178
+ layer1Result = {
179
+ ...layer1Result,
180
+ vulnerabilities: (0, urls_1.aggregateLocalhostFindings)(layer1Result.vulnerabilities)
181
+ };
182
+ console.log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`);
183
+ // Layer 2: Structural Scan
184
+ reportProgress('layer2', 'Running structural scan (variables, logic gates)...', layer1Result.vulnerabilities.length);
185
+ const layer2Result = await (0, layer2_1.runLayer2Scan)(files, { middlewareConfig, fileAuthImports });
186
+ // Format heuristic breakdown for logging
187
+ const heuristicBreakdown = Object.entries(layer2Result.stats.raw)
188
+ .filter(([, count]) => count > 0)
189
+ .map(([name, count]) => `${name}:${count}`)
190
+ .join(',');
191
+ console.log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b) => a + b, 0)} findings_deduped=${layer2Result.vulnerabilities.length} heuristic_breakdown={${heuristicBreakdown}}`);
192
+ // Combine Layer 1 and Layer 2 findings
193
+ const layer12Findings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities];
194
+ // Aggregate noisy findings BEFORE tier filtering to reduce noise
195
+ const beforeAggregationCount = layer12Findings.length;
196
+ const aggregatedFindings = aggregateNoisyFindings(layer12Findings);
197
+ const aggregatedCount = beforeAggregationCount - aggregatedFindings.length;
198
+ // Apply tier-based filtering based on scan depth
199
+ // This is the key integration point for the detector tier system
200
+ const tierFiltered = filterByTierAndDepth(aggregatedFindings, depth);
201
+ // Log tier breakdown
202
+ console.log(`[Scanner] repo=${repoInfo.name} tier_breakdown=${(0, tiers_1.formatTierStats)(tierFiltered.tierStats)}`);
203
+ console.log(`[Scanner] repo=${repoInfo.name} depth=${depth} tier_routing: surface=${tierFiltered.toSurface.length} validate=${tierFiltered.toValidate.length} hidden=${tierFiltered.hidden.length}`);
204
+ // For cheap scans: Tier A surfaces directly, Tier B/C are hidden
205
+ // For validated/deep: Tier A surfaces, Tier B goes through AI validation, Tier C hidden
206
+ // Some Tier A findings still need AI validation (entropy, secrets, AI-specific)
207
+ const additionalValidation = tierFiltered.toSurface.filter(v => v.requiresAIValidation ||
208
+ v.category === 'ai_prompt_injection' || // Story B1: Prompt hygiene
209
+ v.category === 'ai_unsafe_execution' || // Story B2: LLM output execution
210
+ v.category === 'ai_overpermissive_tool' // Story B4: Agent tool permissions
211
+ );
212
+ // Surface findings that don't need validation (excluding those that do)
213
+ const noValidationNeeded = tierFiltered.toSurface.filter(v => !additionalValidation.includes(v));
214
+ // Combine tier-filtered validation candidates with additional ones
215
+ const requiresValidation = [...tierFiltered.toValidate, ...additionalValidation];
216
+ // Apply smart auto-dismiss rules BEFORE AI validation (saves API costs)
217
+ const { toValidate: afterAutoDismiss, dismissed: autoDismissed } = (0, anthropic_1.applyAutoDismissRules)(requiresValidation);
218
+ // Track auto-dismiss by severity for logging
219
+ const autoDismissBySeverity = { info: 0, low: 0, medium: 0, high: 0, critical: 0 };
220
+ for (const d of autoDismissed) {
221
+ autoDismissBySeverity[d.finding.severity] = (autoDismissBySeverity[d.finding.severity] || 0) + 1;
222
+ }
223
+ if (autoDismissed.length > 0) {
224
+ console.log(`[Layer2] repo=${repoInfo.name} auto_dismissed_total=${autoDismissed.length} by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}}`);
225
+ }
226
+ // Apply per-file cap to validation candidates (cost control)
227
+ // Use scan mode config for max files
228
+ const maxValidationFiles = scanModeConfig.maxAIValidationFiles || MAX_VALIDATION_CANDIDATES_PER_FILE;
229
+ const cappedValidation = capValidationCandidatesPerFile(afterAutoDismiss, maxValidationFiles);
230
+ // AI Validation of selected findings (if AI is enabled and not skipped by scan mode)
231
+ let validatedFindings = cappedValidation;
232
+ let capturedValidationStats = undefined;
233
+ const shouldValidate = options.enableAI !== false && !scanModeConfig.skipAIValidation && cappedValidation.length > 0;
234
+ if (shouldValidate) {
235
+ reportProgress('validating', 'AI validating findings (entropy, secrets, AI patterns)...', cappedValidation.length);
236
+ // For incremental scans, only validate findings in changed files
237
+ const findingsToValidate = isIncremental && scanModeConfig.changedFiles
238
+ ? cappedValidation.filter(v => scanModeConfig.changedFiles.some(cf => v.filePath.endsWith(cf) || v.filePath.includes(cf)))
239
+ : cappedValidation;
240
+ if (findingsToValidate.length > 0) {
241
+ const validationResult = await (0, anthropic_1.validateFindingsWithAI)(findingsToValidate, filesForAI);
242
+ validatedFindings = validationResult.vulnerabilities;
243
+ const { stats: validationStats } = validationResult;
244
+ capturedValidationStats = validationStats; // Capture for return
245
+ console.log(`[AI Validation] repo=${repoInfo.name} depth=${depth} candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`);
246
+ console.log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`);
247
+ // Add back findings that weren't validated (not in changed files)
248
+ const notValidated = cappedValidation.filter(v => !findingsToValidate.includes(v));
249
+ validatedFindings.push(...notValidated);
250
+ }
251
+ }
252
+ else if (scanModeConfig.skipAIValidation) {
253
+ console.log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
254
+ }
255
+ // Combine validated and non-validated findings
256
+ allVulnerabilities.push(...validatedFindings, ...noValidationNeeded);
257
+ // Layer 3: AI Semantic Analysis (new findings)
258
+ // Skip for incremental scans by default (configurable)
259
+ const shouldRunLayer3 = options.enableAI !== false && !scanModeConfig.skipLayer3;
260
+ if (shouldRunLayer3) {
261
+ reportProgress('layer3', 'Running AI semantic analysis...', allVulnerabilities.length);
262
+ // For incremental scans, only analyze changed files
263
+ const filesToAnalyze = isIncremental ? filesForAI : files;
264
+ const maxLayer3Files = scanModeConfig.maxLayer3Files || 10;
265
+ // Detect auth helpers for Layer 3 context
266
+ const authHelperContext = (0, auth_helper_detector_1.detectAuthHelpers)(files);
267
+ const layer3Result = await (0, layer3_1.runLayer3Scan)(filesToAnalyze, {
268
+ enableAI: options.enableAI,
269
+ maxFiles: maxLayer3Files,
270
+ projectContext: {
271
+ middlewareConfig: middlewareConfig.hasAuthMiddleware ? {
272
+ hasAuthMiddleware: true,
273
+ authType: middlewareConfig.authType,
274
+ protectedPaths: middlewareConfig.protectedPaths,
275
+ } : undefined,
276
+ authHelpers: authHelperContext.hasThrowingHelpers ? {
277
+ hasThrowingHelpers: true,
278
+ summary: authHelperContext.summary,
279
+ } : undefined,
280
+ },
281
+ });
282
+ allVulnerabilities.push(...layer3Result.vulnerabilities);
283
+ console.log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
284
+ }
285
+ else if (scanModeConfig.skipLayer3) {
286
+ console.log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
287
+ }
288
+ // Deduplicate vulnerabilities
289
+ const uniqueVulnerabilities = deduplicateVulnerabilities(allVulnerabilities);
290
+ // Resolve contradictions (e.g., middleware-protected INFO vs missing-auth CRITICAL on same route)
291
+ const resolvedVulnerabilities = resolveContradictions(uniqueVulnerabilities, middlewareConfig);
292
+ // Sort by severity
293
+ const sortedVulnerabilities = sortBySeverity(resolvedVulnerabilities);
294
+ // Compute issue-mix counts
295
+ const severityCounts = computeSeverityCounts(sortedVulnerabilities);
296
+ const categoryCounts = computeCategoryCounts(sortedVulnerabilities);
297
+ const hasBlockingIssues = severityCounts.critical > 0 || severityCounts.high > 0;
298
+ reportProgress('complete', 'Scan complete!', sortedVulnerabilities.length);
299
+ return {
300
+ repoName: repoInfo.name,
301
+ repoUrl: repoInfo.url,
302
+ branch: repoInfo.branch,
303
+ filesScanned: files.length,
304
+ filesSkipped: 0, // TODO: track skipped files
305
+ vulnerabilities: sortedVulnerabilities,
306
+ severityCounts,
307
+ categoryCounts,
308
+ hasBlockingIssues,
309
+ scanDuration: Date.now() - startTime,
310
+ timestamp: new Date().toISOString(),
311
+ validationStats: capturedValidationStats,
312
+ };
313
+ }
314
+ catch (error) {
315
+ reportProgress('failed', `Scan failed: ${error}`);
316
+ throw error;
317
+ }
318
+ }
319
+ /**
320
+ * Aggregate noisy findings in the same file to reduce clutter
321
+ * Groups repeated findings with same filePath + category + title
322
+ * Especially useful for AI pattern spam
323
+ */
324
+ function aggregateNoisyFindings(vulnerabilities) {
325
+ // Group findings by file + category + title
326
+ const groups = new Map();
327
+ for (const vuln of vulnerabilities) {
328
+ // Create grouping key: same file, category, and base title (without line-specific info)
329
+ const baseTitle = vuln.title.replace(/\s*\(\d+ instances?\)/, '').trim();
330
+ const key = `${vuln.filePath}|${vuln.category}|${baseTitle}`;
331
+ const existing = groups.get(key) || [];
332
+ existing.push(vuln);
333
+ groups.set(key, existing);
334
+ }
335
+ const result = [];
336
+ for (const [key, group] of groups) {
337
+ // If only 1-2 findings, keep them as-is
338
+ if (group.length <= 2) {
339
+ result.push(...group);
340
+ continue;
341
+ }
342
+ // For 3+ similar findings in same file, aggregate into one
343
+ const first = group[0];
344
+ const lineNumbers = group.map(v => v.lineNumber).sort((a, b) => a - b);
345
+ const uniqueLines = [...new Set(lineNumbers)];
346
+ // Format line numbers nicely (show first few, then "...")
347
+ const lineDisplay = uniqueLines.length > 5
348
+ ? `${uniqueLines.slice(0, 5).join(', ')}... (${uniqueLines.length} total)`
349
+ : uniqueLines.join(', ');
350
+ // Keep highest severity from the group
351
+ const highestSeverity = group.reduce((max, v) => severityRank(v.severity) > severityRank(max.severity) ? v : max, group[0]).severity;
352
+ // Create aggregated finding
353
+ const aggregated = {
354
+ id: `${first.id}-aggregated`,
355
+ filePath: first.filePath,
356
+ lineNumber: uniqueLines[0], // First occurrence
357
+ lineContent: `${group.length} instances across this file`,
358
+ severity: highestSeverity,
359
+ category: first.category,
360
+ title: `${first.title.replace(/\s*\(\d+ instances?\)/, '')} (${group.length} instances)`,
361
+ description: `${first.description}\n\nFound ${group.length} occurrences at lines: ${lineDisplay}`,
362
+ suggestedFix: first.suggestedFix,
363
+ confidence: first.confidence,
364
+ layer: first.layer,
365
+ requiresAIValidation: first.requiresAIValidation,
366
+ };
367
+ result.push(aggregated);
368
+ }
369
+ return result;
370
+ }
371
+ /**
372
+ * Cap validation candidates per file to control AI costs
373
+ * Prioritizes by:
374
+ * 1. Tier (ai_assisted findings need AI most, so they get priority)
375
+ * 2. Severity (critical > high > medium > low > info)
376
+ * 3. Category importance (secrets/URLs/auth before cosmetic patterns)
377
+ */
378
+ function capValidationCandidatesPerFile(vulnerabilities, maxPerFile = MAX_VALIDATION_CANDIDATES_PER_FILE) {
379
+ // Group by file
380
+ const byFile = new Map();
381
+ for (const vuln of vulnerabilities) {
382
+ const existing = byFile.get(vuln.filePath) || [];
383
+ existing.push(vuln);
384
+ byFile.set(vuln.filePath, existing);
385
+ }
386
+ const result = [];
387
+ for (const [filePath, fileVulns] of byFile) {
388
+ // Sort by priority: tier first (ai_assisted needs AI most), then severity, then category
389
+ const sorted = [...fileVulns].sort((a, b) => {
390
+ // Tier comparison: ai_assisted (needs AI) > core (high precision) > experimental (hidden anyway)
391
+ const tierPriority = (v) => {
392
+ const tier = (0, tiers_1.getTierForCategory)(v.category, v.layer);
393
+ if (tier === 'ai_assisted')
394
+ return 10; // Highest priority - these NEED AI validation
395
+ if (tier === 'core')
396
+ return 5; // High precision, but AI can still help
397
+ return 1; // Experimental - shouldn't even be here
398
+ };
399
+ const tierDiff = tierPriority(b) - tierPriority(a);
400
+ if (tierDiff !== 0)
401
+ return tierDiff;
402
+ // Severity comparison (higher severity = higher priority)
403
+ const severityDiff = severityRank(b.severity) - severityRank(a.severity);
404
+ if (severityDiff !== 0)
405
+ return severityDiff;
406
+ // Category importance (secrets/URLs/auth before AI patterns)
407
+ const categoryPriority = (v) => {
408
+ if (v.category === 'hardcoded_secret')
409
+ return 10;
410
+ if (v.category === 'high_entropy_string')
411
+ return 9;
412
+ if (v.category === 'sensitive_url')
413
+ return 8;
414
+ if (v.category === 'missing_auth')
415
+ return 7;
416
+ if (v.category === 'ai_pattern')
417
+ return 3;
418
+ return 5;
419
+ };
420
+ return categoryPriority(b) - categoryPriority(a);
421
+ });
422
+ // Take top N per file
423
+ const capped = sorted.slice(0, maxPerFile);
424
+ result.push(...capped);
425
+ if (sorted.length > maxPerFile) {
426
+ console.log(`[Scanner] Capped ${filePath}: ${sorted.length} → ${maxPerFile} validation candidates`);
427
+ }
428
+ }
429
+ return result;
430
+ }
431
+ /**
432
+ * Remove duplicate vulnerabilities (same file, line, category)
433
+ * Also handles cross-layer URL duplicates (sensitive_url + ai_pattern)
434
+ */
435
+ function deduplicateVulnerabilities(vulnerabilities) {
436
+ const seen = new Map();
437
+ const urlDedupMap = new Map();
438
+ for (const vuln of vulnerabilities) {
439
+ // Special handling for URL duplicates across layers
440
+ // (e.g., Layer 1 detects as sensitive_url, Layer 2 detects as ai_pattern)
441
+ if ((vuln.category === 'sensitive_url' || vuln.category === 'ai_pattern') &&
442
+ /url|localhost|127\.0\.0\.1|http/i.test(vuln.lineContent)) {
443
+ // Create compound key that ignores category differences for URLs
444
+ const urlKey = `${vuln.filePath}:${vuln.lineNumber}:url_finding`;
445
+ const existing = urlDedupMap.get(urlKey);
446
+ if (!existing) {
447
+ urlDedupMap.set(urlKey, vuln);
448
+ }
449
+ else {
450
+ // Keep Layer 1 (more specific) over Layer 2 AI pattern
451
+ // Or keep higher severity
452
+ if (vuln.layer < existing.layer ||
453
+ severityRank(vuln.severity) > severityRank(existing.severity)) {
454
+ urlDedupMap.set(urlKey, vuln);
455
+ }
456
+ }
457
+ continue;
458
+ }
459
+ // Standard deduplication for non-URL findings
460
+ const key = `${vuln.filePath}:${vuln.lineNumber}:${vuln.category}`;
461
+ const existing = seen.get(key);
462
+ // Keep the higher severity or higher confidence finding
463
+ if (!existing) {
464
+ seen.set(key, vuln);
465
+ }
466
+ else if (severityRank(vuln.severity) > severityRank(existing.severity)) {
467
+ seen.set(key, vuln);
468
+ }
469
+ else if (severityRank(vuln.severity) === severityRank(existing.severity) &&
470
+ confidenceRank(vuln.confidence) > confidenceRank(existing.confidence)) {
471
+ seen.set(key, vuln);
472
+ }
473
+ }
474
+ // Combine URL and non-URL findings
475
+ return [...Array.from(seen.values()), ...Array.from(urlDedupMap.values())];
476
+ }
477
+ /**
478
+ * Resolve contradictions in findings
479
+ *
480
+ * Key contradiction types:
481
+ * 1. Same route has both "protected by middleware" (info) AND "missing auth" (high/critical)
482
+ * → Keep only the info-level "protected by middleware" finding
483
+ * 2. Same file has BYOK "transient use" (info) AND "key stored insecurely" (high)
484
+ * → Keep only the most accurate one based on context
485
+ * 3. Same line has conflicting severities from different layers
486
+ * → Prefer the lower severity if one explicitly notes protection
487
+ */
488
+ function resolveContradictions(vulnerabilities, middlewareConfig) {
489
+ // Group findings by file path for contradiction analysis
490
+ const byFile = new Map();
491
+ for (const vuln of vulnerabilities) {
492
+ const existing = byFile.get(vuln.filePath) || [];
493
+ existing.push(vuln);
494
+ byFile.set(vuln.filePath, existing);
495
+ }
496
+ const result = [];
497
+ for (const [filePath, fileVulns] of byFile) {
498
+ // Check for auth contradictions in this file
499
+ const authFindings = fileVulns.filter(v => v.category === 'missing_auth');
500
+ const otherFindings = fileVulns.filter(v => v.category !== 'missing_auth');
501
+ // Identify protected routes (middleware or auth helper)
502
+ const protectedInfos = authFindings.filter(v => v.severity === 'info' &&
503
+ (v.validationNotes === 'MIDDLEWARE_PROTECTED' ||
504
+ v.validationNotes === 'AUTH_HELPER_PROTECTED' ||
505
+ v.title.includes('protected by middleware') ||
506
+ v.title.includes('uses auth helper')));
507
+ // NEW: Check if this file's route is protected by middleware (even without explicit info finding)
508
+ // This catches Layer 3 findings that don't have the Layer 2 protection info
509
+ const routePath = (0, middleware_detector_1.getRoutePathFromFile)(filePath);
510
+ const isAPIRouteProtected = routePath && middlewareConfig?.hasAuthMiddleware
511
+ ? (0, middleware_detector_1.isRouteProtectedByMiddleware)(routePath, middlewareConfig).isProtected
512
+ : false;
513
+ // Also check if file is a client component calling protected API routes
514
+ // Client components (components/, app/ without route.ts) calling /api/** are safe
515
+ const isClientCallingProtectedAPI = middlewareConfig?.hasAuthMiddleware &&
516
+ (filePath.includes('/components/') ||
517
+ (filePath.includes('/app/') && !filePath.includes('route.ts')));
518
+ // If we have protected route info findings OR the route is protected by middleware
519
+ if (protectedInfos.length > 0 || isAPIRouteProtected || isClientCallingProtectedAPI) {
520
+ // Keep the protected info findings
521
+ result.push(...protectedInfos);
522
+ // For other auth findings on same file, either drop or downgrade to info
523
+ const otherAuthFindings = authFindings.filter(v => !protectedInfos.includes(v));
524
+ for (const vuln of otherAuthFindings) {
525
+ // If it's high/critical missing auth on a protected route, drop it entirely
526
+ // (the middleware/helper protection supersedes)
527
+ if (vuln.severity === 'critical' || vuln.severity === 'high') {
528
+ const reason = isAPIRouteProtected
529
+ ? 'API route protected by middleware'
530
+ : isClientCallingProtectedAPI
531
+ ? 'client component calling protected API'
532
+ : 'route is protected';
533
+ console.log(`[Contradiction] Dropping "${vuln.title}" (${vuln.severity}) - ${reason}`);
534
+ continue; // Skip this finding
535
+ }
536
+ // Keep lower severity auth findings as-is
537
+ result.push(vuln);
538
+ }
539
+ }
540
+ else {
541
+ // No protection detected - keep all auth findings as-is
542
+ result.push(...authFindings);
543
+ }
544
+ // Handle BYOK contradictions
545
+ const byokFindings = otherFindings.filter(v => v.category === 'ai_pattern' && v.title.toLowerCase().includes('byok'));
546
+ const nonByokFindings = otherFindings.filter(v => !(v.category === 'ai_pattern' && v.title.toLowerCase().includes('byok')));
547
+ if (byokFindings.length > 0) {
548
+ // Check if we have both transient (low) and storage (high) BYOK findings
549
+ const transientByok = byokFindings.filter(v => v.severity === 'low' || v.severity === 'info');
550
+ const storageByok = byokFindings.filter(v => v.severity === 'high' || v.severity === 'medium');
551
+ if (transientByok.length > 0 && storageByok.length > 0) {
552
+ // If we detected transient usage, prefer the lower severity
553
+ // The higher severity ones may be false positives
554
+ result.push(...transientByok);
555
+ // Mark high-severity BYOK for review
556
+ for (const vuln of storageByok) {
557
+ result.push({
558
+ ...vuln,
559
+ severity: 'low',
560
+ validationNotes: `${vuln.validationNotes || ''} (downgraded: transient BYOK usage detected in same file)`.trim(),
561
+ });
562
+ }
563
+ }
564
+ else {
565
+ // Keep all BYOK findings as-is
566
+ result.push(...byokFindings);
567
+ }
568
+ }
569
+ // Add non-BYOK other findings
570
+ result.push(...nonByokFindings);
571
+ }
572
+ return result;
573
+ }
574
+ /**
575
+ * Sort vulnerabilities by severity (critical first)
576
+ */
577
+ function sortBySeverity(vulnerabilities) {
578
+ return [...vulnerabilities].sort((a, b) => {
579
+ const severityDiff = severityRank(b.severity) - severityRank(a.severity);
580
+ if (severityDiff !== 0)
581
+ return severityDiff;
582
+ // Secondary sort by confidence
583
+ return confidenceRank(b.confidence) - confidenceRank(a.confidence);
584
+ });
585
+ }
586
+ function severityRank(severity) {
587
+ const ranks = {
588
+ critical: 5,
589
+ high: 4,
590
+ medium: 3,
591
+ low: 2,
592
+ info: 1,
593
+ };
594
+ return ranks[severity] || 0;
595
+ }
596
+ function confidenceRank(confidence) {
597
+ const ranks = {
598
+ high: 3,
599
+ medium: 2,
600
+ low: 1,
601
+ };
602
+ return ranks[confidence] || 0;
603
+ }
604
+ /**
605
+ * Compute severity counts from vulnerabilities
606
+ */
607
+ function computeSeverityCounts(vulnerabilities) {
608
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
609
+ for (const vuln of vulnerabilities) {
610
+ if (vuln.severity in counts) {
611
+ counts[vuln.severity]++;
612
+ }
613
+ }
614
+ return counts;
615
+ }
616
+ /**
617
+ * Compute category counts from vulnerabilities
618
+ */
619
+ function computeCategoryCounts(vulnerabilities) {
620
+ const counts = {};
621
+ for (const vuln of vulnerabilities) {
622
+ const category = vuln.category;
623
+ counts[category] = (counts[category] || 0) + 1;
624
+ }
625
+ return counts;
626
+ }
627
+ /**
628
+ * Helper to compute counts from vulnerabilities (for backfilling legacy scans)
629
+ */
630
+ function computeIssueMixFromVulnerabilities(vulnerabilities) {
631
+ const severityCounts = computeSeverityCounts(vulnerabilities);
632
+ const categoryCounts = computeCategoryCounts(vulnerabilities);
633
+ const hasBlockingIssues = severityCounts.critical > 0 || severityCounts.high > 0;
634
+ return { severityCounts, categoryCounts, hasBlockingIssues };
635
+ }
636
+ // Re-export types and utilities
637
+ __exportStar(require("./types"), exports);
638
+ var layer1_2 = require("./layer1");
639
+ Object.defineProperty(exports, "runLayer1Scan", { enumerable: true, get: function () { return layer1_2.runLayer1Scan; } });
640
+ var layer2_2 = require("./layer2");
641
+ Object.defineProperty(exports, "runLayer2Scan", { enumerable: true, get: function () { return layer2_2.runLayer2Scan; } });
642
+ var layer3_2 = require("./layer3");
643
+ Object.defineProperty(exports, "runLayer3Scan", { enumerable: true, get: function () { return layer3_2.runLayer3Scan; } });
644
+ var project_context_builder_1 = require("./utils/project-context-builder");
645
+ Object.defineProperty(exports, "buildProjectContext", { enumerable: true, get: function () { return project_context_builder_1.buildProjectContext; } });
646
+ var anthropic_2 = require("./layer3/anthropic");
647
+ Object.defineProperty(exports, "validateFindingsWithAI", { enumerable: true, get: function () { return anthropic_2.validateFindingsWithAI; } });
648
+ //# sourceMappingURL=index.js.map