@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,336 @@
1
+ /**
2
+ * Layer 2: BYOK (Bring Your Own Key) Pattern Detection
3
+ * Distinguishes per-user BYOK flows from central/shared key storage
4
+ * BYOK is often a feature, not a vulnerability - severity depends on context
5
+ */
6
+
7
+ import type { Vulnerability, VulnerabilitySeverity } from '../types'
8
+ import type { MiddlewareAuthConfig } from '../utils/middleware-detector'
9
+ import { isComment, isTestOrMockFile, isExampleFile, isPlaceholderValue } from '../utils/context-helpers'
10
+ import { isRouteProtectedByMiddleware, getRoutePathFromFile, detectUserScopingPatterns } from '../utils/middleware-detector'
11
+
12
+ /**
13
+ * Check if line contains example/placeholder API key patterns
14
+ */
15
+ function isExampleCredential(line: string): boolean {
16
+ const examplePatterns = [
17
+ /your[-_]?api[-_]?key/i,
18
+ /example[-_]?api[-_]?key/i,
19
+ /sample[-_]?key/i,
20
+ /replace[-_]?with/i,
21
+ /placeholder/i,
22
+ /your[-_]?secret/i,
23
+ /example[-_]?secret/i,
24
+ /<.*api.*key.*>/i, // <YOUR_API_KEY>
25
+ /\[.*api.*key.*\]/i, // [API_KEY]
26
+ /xxx+/i,
27
+ /demo[-_]?key/i,
28
+ /test[-_]?key/i,
29
+ /fake[-_]?key/i,
30
+ /mock[-_]?key/i,
31
+ ]
32
+ return examplePatterns.some(p => p.test(line))
33
+ }
34
+
35
+ interface BYOKPattern {
36
+ name: string
37
+ pattern: RegExp
38
+ keyType: 'openai' | 'anthropic' | 'generic_ai' | 'generic'
39
+ description: string
40
+ }
41
+
42
+ const BYOK_PATTERNS: BYOKPattern[] = [
43
+ // OpenAI API key patterns
44
+ {
45
+ name: 'User-provided OpenAI API key',
46
+ pattern: /(?:openai|api).*key.*(?:req\.|body\.|params\.|input\.|user)|(?:req\.|body\.|params\.).*(?:openai|api).*key/gi,
47
+ keyType: 'openai',
48
+ description: 'User-provided OpenAI API key detected',
49
+ },
50
+ {
51
+ name: 'OpenAI key from request',
52
+ pattern: /new\s+OpenAI\s*\(\s*\{[^}]*apiKey\s*:\s*(?:req|body|params|input)/gi,
53
+ keyType: 'openai',
54
+ description: 'OpenAI client initialized with user-provided key',
55
+ },
56
+
57
+ // Anthropic API key patterns
58
+ {
59
+ name: 'User-provided Anthropic API key',
60
+ pattern: /(?:anthropic|claude).*key.*(?:req\.|body\.|params\.|input\.|user)|(?:req\.|body\.|params\.).*(?:anthropic|claude).*key/gi,
61
+ keyType: 'anthropic',
62
+ description: 'User-provided Anthropic API key detected',
63
+ },
64
+ {
65
+ name: 'Anthropic key from request',
66
+ pattern: /new\s+Anthropic\s*\(\s*\{[^}]*apiKey\s*:\s*(?:req|body|params|input)/gi,
67
+ keyType: 'anthropic',
68
+ description: 'Anthropic client initialized with user-provided key',
69
+ },
70
+
71
+ // Generic AI key patterns
72
+ {
73
+ name: 'User-provided AI API key',
74
+ pattern: /(?:ai|llm|model).*(?:api)?.*key.*(?:req\.|body\.|params\.|input\.)/gi,
75
+ keyType: 'generic_ai',
76
+ description: 'User-provided AI API key detected',
77
+ },
78
+
79
+ // Generic key patterns (more cautious)
80
+ {
81
+ name: 'API key from user input',
82
+ pattern: /apiKey\s*[=:]\s*(?:req|body|params|input)\./gi,
83
+ keyType: 'generic',
84
+ description: 'API key received from user input',
85
+ },
86
+ ]
87
+
88
+ /**
89
+ * Check if file has explicit authentication checks
90
+ */
91
+ function hasExplicitAuthCheck(content: string): boolean {
92
+ const authPatterns = [
93
+ // Common auth check functions
94
+ /getCurrentUser(Id)?\s*\(/i,
95
+ /getSession\s*\(/i,
96
+ /getServerSession\s*\(/i,
97
+ /auth\s*\(\s*\)/i,
98
+ /requireAuth\s*\(/i,
99
+ /verifyToken\s*\(/i,
100
+ /isAuthenticated/i,
101
+ /checkAuth\s*\(/i,
102
+ /withAuth\s*\(/i,
103
+ // Request user checks
104
+ /req\.user|request\.user/i,
105
+ /context\.user/i,
106
+ // Supabase auth
107
+ /supabase\.auth\.getUser/i,
108
+ /supabase\.auth\.getSession/i,
109
+ // NextAuth
110
+ /getServerSession\s*\(/i,
111
+ /useSession\s*\(/i,
112
+ // Clerk
113
+ /currentUser\s*\(/i,
114
+ /auth\(\)\.protect/i,
115
+ ]
116
+ return authPatterns.some(p => p.test(content))
117
+ }
118
+
119
+ /**
120
+ * Check if key appears to be stored to database
121
+ */
122
+ function isKeyStoredToDatabase(content: string, lineNumber: number): boolean {
123
+ const lines = content.split('\n')
124
+ const startLine = Math.max(0, lineNumber - 5)
125
+ const endLine = Math.min(lines.length, lineNumber + 15)
126
+ const context = lines.slice(startLine, endLine).join('\n')
127
+
128
+ // Patterns indicating database storage operations
129
+ // NOTE: We use specific patterns to avoid false positives with API calls like
130
+ // openai.chat.completions.create() or anthropic.messages.create()
131
+ const storagePatterns = [
132
+ /\.insert\s*\(/i,
133
+ /\.update\s*\(/i,
134
+ /\.upsert\s*\(/i,
135
+ /\.save\s*\(/i,
136
+ /INSERT\s+INTO/i,
137
+ /UPDATE\s+.*SET/i,
138
+ // ORM-specific patterns - must have table/model context
139
+ /prisma\.\w+\.create\s*\(/i,
140
+ /supabase\.from\(.*\)\.(?:insert|update|upsert)/i,
141
+ /db\.\w+\.create\s*\(/i,
142
+ /knex\(.*\)\.insert/i,
143
+ /\.collection\(.*\)\.(?:insertOne|updateOne|save)/i, // MongoDB
144
+ /sequelize\..*\.create\s*\(/i,
145
+ // localStorage/sessionStorage (client-side storage)
146
+ /localStorage\.setItem/i,
147
+ /sessionStorage\.setItem/i,
148
+ ]
149
+
150
+ // Exclude AI SDK patterns that use .create() for API calls, not storage
151
+ const aiApiPatterns = [
152
+ /openai\..*\.create/i,
153
+ /anthropic\..*\.create/i,
154
+ /\.chat\.completions\.create/i,
155
+ /\.messages\.create/i,
156
+ /\.completions\.create/i,
157
+ /\.embeddings\.create/i,
158
+ ]
159
+
160
+ const hasStoragePattern = storagePatterns.some(p => p.test(context))
161
+ const hasAiApiPattern = aiApiPatterns.some(p => p.test(context))
162
+
163
+ // Only consider it storage if we have storage patterns but NOT AI API patterns
164
+ // If both are present, check if storage patterns are separate from AI patterns
165
+ if (hasStoragePattern && hasAiApiPattern) {
166
+ // Check if there's a clear database storage operation separate from AI calls
167
+ // Look for keywords like 'user', 'key', 'credential' near storage patterns
168
+ return /(?:insert|update|upsert|save).*(?:apiKey|api_key|key|credential)/i.test(context) ||
169
+ /(?:apiKey|api_key|key|credential).*(?:insert|update|upsert|save)/i.test(context)
170
+ }
171
+
172
+ return hasStoragePattern
173
+ }
174
+
175
+ /**
176
+ * Check if key is only used transiently (same request, not stored)
177
+ */
178
+ function isTransientKeyUsage(content: string, lineNumber: number): boolean {
179
+ const lines = content.split('\n')
180
+ const startLine = Math.max(0, lineNumber - 3)
181
+ const endLine = Math.min(lines.length, lineNumber + 20)
182
+ const context = lines.slice(startLine, endLine).join('\n')
183
+
184
+ // Transient usage patterns - key used immediately then discarded
185
+ const transientPatterns = [
186
+ /new\s+(?:OpenAI|Anthropic)\s*\(/i, // Immediate client creation
187
+ /\.create\s*\(\s*\{[^}]*messages/i, // Immediate API call
188
+ /\.completions\.create/i,
189
+ /\.messages\.create/i,
190
+ /await\s+(?:openai|anthropic|client)\./i,
191
+ ]
192
+
193
+ // If we see API calls but no storage, it's transient
194
+ const hasApiCalls = transientPatterns.some(p => p.test(context))
195
+ const hasStorage = isKeyStoredToDatabase(content, lineNumber)
196
+
197
+ return hasApiCalls && !hasStorage
198
+ }
199
+
200
+ /**
201
+ * Check if key appears to be logged (bad practice)
202
+ */
203
+ function isKeyLogged(content: string, lineNumber: number): boolean {
204
+ const lines = content.split('\n')
205
+ const startLine = Math.max(0, lineNumber - 3)
206
+ const endLine = Math.min(lines.length, lineNumber + 10)
207
+ const context = lines.slice(startLine, endLine).join('\n')
208
+
209
+ const loggingPatterns = [
210
+ /console\.(log|info|debug|warn|error)\s*\([^)]*(?:apiKey|api_key|key)/i,
211
+ /logger\.(log|info|debug|warn|error)\s*\([^)]*(?:apiKey|api_key|key)/i,
212
+ /\.(log|info|debug|warn|error)\s*\(.*(?:apiKey|api_key|key)/i,
213
+ ]
214
+
215
+ return loggingPatterns.some(p => p.test(context))
216
+ }
217
+
218
+ /**
219
+ * Detect BYOK patterns and classify their risk level
220
+ */
221
+ export function detectBYOKPatterns(
222
+ content: string,
223
+ filePath: string,
224
+ middlewareConfig?: MiddlewareAuthConfig
225
+ ): Vulnerability[] {
226
+ const vulnerabilities: Vulnerability[] = []
227
+ const lines = content.split('\n')
228
+ const isTestFile = isTestOrMockFile(filePath)
229
+
230
+ // Skip example/demo files entirely - they contain placeholder credentials by design
231
+ if (isExampleFile(filePath)) {
232
+ return vulnerabilities
233
+ }
234
+
235
+ // Check route protection
236
+ const routePath = getRoutePathFromFile(filePath)
237
+ const middlewareProtection = routePath && middlewareConfig
238
+ ? isRouteProtectedByMiddleware(routePath, middlewareConfig)
239
+ : { isProtected: false, reason: '' }
240
+
241
+ // Check for user scoping patterns in the file
242
+ const userScoping = detectUserScopingPatterns(content)
243
+
244
+ // Check for explicit auth in file content (more reliable than middleware detection)
245
+ const hasExplicitAuth = hasExplicitAuthCheck(content)
246
+
247
+ lines.forEach((line, index) => {
248
+ if (isComment(line)) return
249
+
250
+ // Skip lines with example/placeholder credentials
251
+ if (isExampleCredential(line) || isPlaceholderValue('', line)) {
252
+ return
253
+ }
254
+
255
+ for (const pattern of BYOK_PATTERNS) {
256
+ const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags)
257
+
258
+ if (regex.test(line)) {
259
+ // Determine context
260
+ const isAuthenticated = middlewareProtection.isProtected || hasExplicitAuth
261
+ const isUserScoped = userScoping.hasUserScoping
262
+ const isStoredCentrally = isKeyStoredToDatabase(content, index)
263
+ const isTransient = isTransientKeyUsage(content, index)
264
+
265
+ // Determine severity based on context
266
+ let severity: VulnerabilitySeverity
267
+ let description: string
268
+ let suggestedFix: string
269
+
270
+ if (isAuthenticated && isTransient) {
271
+ // Authenticated and transient - this is the IDEAL BYOK pattern
272
+ // Check if key is being logged (bad practice)
273
+ const keyLogged = isKeyLogged(content, index)
274
+ if (keyLogged) {
275
+ severity = 'low'
276
+ description = `BYOK feature detected: ${pattern.description}. Keys are used transiently (good!) but appear to be logged (avoid logging API keys, even in debug).`
277
+ suggestedFix = 'Remove logging of API keys. Best practices: (1) Validate API key format, (2) Add per-user rate limiting.'
278
+ } else {
279
+ // IDEAL PATTERN: Authenticated + transient + no logging = no issue
280
+ // Skip emitting a finding entirely for the ideal case
281
+ continue
282
+ }
283
+ } else if (!isAuthenticated && isTransient) {
284
+ // Unauthenticated but transient - still lower risk than storage
285
+ severity = 'low'
286
+ description = `BYOK feature detected: ${pattern.description}. Keys are used transiently (not stored). Consider adding authentication or rate limiting.`
287
+ suggestedFix = 'Consider adding authentication. If intentionally public: add rate limiting, key format validation, and usage tracking.'
288
+ } else if (!isAuthenticated && isStoredCentrally) {
289
+ // Unauthenticated AND storing keys - this is the real risk
290
+ severity = 'medium'
291
+ description = `${pattern.description}. This endpoint appears to lack authentication AND stores keys. This could allow unauthorized key storage.`
292
+ suggestedFix = 'Add authentication. Ensure stored keys are scoped by user_id with proper access controls.'
293
+ } else if (isStoredCentrally && !isUserScoped) {
294
+ // Authenticated but keys stored without user scoping - medium risk
295
+ severity = 'medium'
296
+ description = `${pattern.description}. Keys appear to be stored centrally without user-scoping, which could lead to cross-tenant key access.`
297
+ suggestedFix = 'Ensure stored keys are scoped by user_id. Add proper access controls to prevent users from accessing other users\' keys.'
298
+ } else if (isAuthenticated && isUserScoped) {
299
+ // Authenticated and user-scoped with storage - generally okay
300
+ severity = 'info'
301
+ description = `${pattern.description}. Route is authenticated and operations appear user-scoped. If keys are stored, consider encryption at rest.`
302
+ suggestedFix = 'If storing keys: consider encryption at rest. Add rate limiting to prevent cost abuse.'
303
+ } else {
304
+ // Authenticated but unclear scoping - needs review, but low priority
305
+ severity = 'info'
306
+ description = `${pattern.description}. Route is authenticated. This appears to be a BYOK feature.`
307
+ suggestedFix = 'Verify user-scoping for stored keys. Add rate limiting for cost control.'
308
+ }
309
+
310
+ // Downgrade test files
311
+ if (isTestFile) {
312
+ severity = 'info'
313
+ description = `${description} (in test file)`
314
+ }
315
+
316
+ vulnerabilities.push({
317
+ id: `byok-${filePath}-${index + 1}-${pattern.name}`,
318
+ filePath,
319
+ lineNumber: index + 1,
320
+ lineContent: line.trim(),
321
+ severity,
322
+ category: 'ai_pattern',
323
+ title: `BYOK: ${pattern.name}`,
324
+ description,
325
+ suggestedFix,
326
+ confidence: isTestFile ? 'low' : 'medium',
327
+ layer: 2,
328
+ })
329
+
330
+ break // One finding per line
331
+ }
332
+ }
333
+ })
334
+
335
+ return vulnerabilities
336
+ }