@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,333 @@
1
+ /**
2
+ * Middleware Auth Detection
3
+ * Detects global authentication middleware (Next.js, Express, etc.)
4
+ * and determines which routes are protected
5
+ */
6
+
7
+ import type { ScanFile } from '../types'
8
+
9
+ /**
10
+ * Configuration describing detected auth middleware
11
+ */
12
+ export interface MiddlewareAuthConfig {
13
+ /** Whether auth middleware was detected */
14
+ hasAuthMiddleware: boolean
15
+ /** The type of auth detected */
16
+ authType?: 'clerk' | 'nextauth' | 'auth0' | 'custom' | 'unknown'
17
+ /** File path where middleware was found */
18
+ middlewareFile?: string
19
+ /** Protected path patterns (glob-like) */
20
+ protectedPaths: string[]
21
+ /** Public/excluded path patterns */
22
+ publicPaths: string[]
23
+ /** Raw matcher patterns found */
24
+ matchers: string[]
25
+ /** Additional context about the auth setup */
26
+ context: {
27
+ usesAuthProtect: boolean
28
+ usesGetToken: boolean
29
+ usesSession: boolean
30
+ hasPublicRoutes: boolean
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Detect global auth middleware from scanned files
36
+ * Looks for Next.js middleware.ts, Express auth patterns, etc.
37
+ */
38
+ export function detectGlobalAuthMiddleware(files: ScanFile[]): MiddlewareAuthConfig {
39
+ const result: MiddlewareAuthConfig = {
40
+ hasAuthMiddleware: false,
41
+ protectedPaths: [],
42
+ publicPaths: [],
43
+ matchers: [],
44
+ context: {
45
+ usesAuthProtect: false,
46
+ usesGetToken: false,
47
+ usesSession: false,
48
+ hasPublicRoutes: false,
49
+ },
50
+ }
51
+
52
+ // Find middleware files
53
+ const middlewareFile = files.find(f =>
54
+ /^(src\/)?middleware\.(ts|js)$/.test(f.path) ||
55
+ f.path.endsWith('/middleware.ts') ||
56
+ f.path.endsWith('/middleware.js')
57
+ )
58
+
59
+ if (!middlewareFile) {
60
+ return result
61
+ }
62
+
63
+ result.middlewareFile = middlewareFile.path
64
+ const content = middlewareFile.content
65
+
66
+ // Detect auth provider type
67
+ result.authType = detectAuthType(content)
68
+
69
+ if (result.authType) {
70
+ result.hasAuthMiddleware = true
71
+ }
72
+
73
+ // Extract protected path matchers
74
+ const { protectedPaths, publicPaths, matchers } = extractPathMatchers(content)
75
+ result.protectedPaths = protectedPaths
76
+ result.publicPaths = publicPaths
77
+ result.matchers = matchers
78
+
79
+ // Detect specific auth patterns
80
+ result.context.usesAuthProtect = /auth\.protect\(\)|auth\(\)\.protect\(\)|requireAuth\(\)/i.test(content)
81
+ result.context.usesGetToken = /getToken\(|getAuth\(/i.test(content)
82
+ result.context.usesSession = /getSession\(|getServerSession\(/i.test(content)
83
+ result.context.hasPublicRoutes = /isPublicRoute|publicRoutes|ignoredRoutes|excludedPaths/i.test(content)
84
+
85
+ // If we found auth imports but no explicit matchers, assume /api/** is protected
86
+ if (result.hasAuthMiddleware && result.protectedPaths.length === 0) {
87
+ // Check if middleware runs on all routes or has default API protection
88
+ if (!result.context.hasPublicRoutes || /\/api/i.test(content)) {
89
+ result.protectedPaths.push('/api/**')
90
+ }
91
+ }
92
+
93
+ return result
94
+ }
95
+
96
+ /**
97
+ * Detect which auth provider/library is being used
98
+ */
99
+ function detectAuthType(content: string): MiddlewareAuthConfig['authType'] {
100
+ // Clerk patterns
101
+ if (/clerkMiddleware|@clerk\/nextjs|clerkClient|auth\(\)\.protect/i.test(content)) {
102
+ return 'clerk'
103
+ }
104
+
105
+ // NextAuth patterns
106
+ if (/next-auth|NextAuth|getServerSession|withAuth\(/i.test(content)) {
107
+ return 'nextauth'
108
+ }
109
+
110
+ // Auth0 patterns
111
+ if (/@auth0\/nextjs|withPageAuthRequired|withApiAuthRequired/i.test(content)) {
112
+ return 'auth0'
113
+ }
114
+
115
+ // Generic auth middleware patterns
116
+ if (/authMiddleware|requireAuth|verifyToken|checkAuth|isAuthenticated/i.test(content)) {
117
+ return 'custom'
118
+ }
119
+
120
+ // Check for common auth imports/usage that suggest middleware is doing auth
121
+ if (/authorization|bearer|jwt|session\.user|getToken/i.test(content)) {
122
+ return 'unknown'
123
+ }
124
+
125
+ return undefined
126
+ }
127
+
128
+ /**
129
+ * Extract path matchers from middleware config
130
+ */
131
+ function extractPathMatchers(content: string): {
132
+ protectedPaths: string[]
133
+ publicPaths: string[]
134
+ matchers: string[]
135
+ } {
136
+ const protectedPaths: string[] = []
137
+ const publicPaths: string[] = []
138
+ const matchers: string[] = []
139
+
140
+ // Next.js config.matcher pattern
141
+ // export const config = { matcher: [...] }
142
+ const matcherArrayMatch = content.match(/config\s*=\s*\{[^}]*matcher\s*:\s*\[([\s\S]*?)\]/m)
143
+ if (matcherArrayMatch) {
144
+ const matcherContent = matcherArrayMatch[1]
145
+ // Extract string patterns from the array
146
+ const patterns = matcherContent.match(/['"`]([^'"`]+)['"`]/g) || []
147
+ for (const p of patterns) {
148
+ const path = p.replace(/['"`]/g, '')
149
+ matchers.push(path)
150
+ // Classify as protected or public based on common patterns
151
+ if (path.includes('/api') || path.includes('/(protected)') || path.includes('/dashboard')) {
152
+ protectedPaths.push(path)
153
+ }
154
+ }
155
+ }
156
+
157
+ // Single matcher pattern
158
+ // export const config = { matcher: '/api/:path*' }
159
+ const singleMatcherMatch = content.match(/config\s*=\s*\{[^}]*matcher\s*:\s*['"`]([^'"`]+)['"`]/m)
160
+ if (singleMatcherMatch) {
161
+ const path = singleMatcherMatch[1]
162
+ matchers.push(path)
163
+ if (path.includes('/api')) {
164
+ protectedPaths.push(path)
165
+ }
166
+ }
167
+
168
+ // Clerk-specific: createRouteMatcher for public routes
169
+ // const isPublicRoute = createRouteMatcher(['/sign-in', '/sign-up', ...])
170
+ const publicRouteMatch = content.match(/createRouteMatcher\s*\(\s*\[([\s\S]*?)\]/m)
171
+ if (publicRouteMatch) {
172
+ const routeContent = publicRouteMatch[1]
173
+ const patterns = routeContent.match(/['"`]([^'"`]+)['"`]/g) || []
174
+ for (const p of patterns) {
175
+ const path = p.replace(/['"`]/g, '')
176
+ publicPaths.push(path)
177
+ }
178
+ }
179
+
180
+ // Look for publicRoutes/ignoredRoutes arrays
181
+ const publicRoutesMatch = content.match(/(publicRoutes|ignoredRoutes|excludedPaths)\s*[=:]\s*\[([\s\S]*?)\]/m)
182
+ if (publicRoutesMatch) {
183
+ const routeContent = publicRoutesMatch[2]
184
+ const patterns = routeContent.match(/['"`]([^'"`]+)['"`]/g) || []
185
+ for (const p of patterns) {
186
+ const path = p.replace(/['"`]/g, '')
187
+ publicPaths.push(path)
188
+ }
189
+ }
190
+
191
+ // If we found public paths but no explicit protected paths,
192
+ // assume everything else under /api is protected
193
+ if (publicPaths.length > 0 && protectedPaths.length === 0) {
194
+ protectedPaths.push('/api/**')
195
+ }
196
+
197
+ return { protectedPaths, publicPaths, matchers }
198
+ }
199
+
200
+ /**
201
+ * Check if a given route path is protected by the middleware
202
+ */
203
+ export function isRouteProtectedByMiddleware(
204
+ routePath: string,
205
+ config: MiddlewareAuthConfig
206
+ ): { isProtected: boolean; reason: string } {
207
+ if (!config.hasAuthMiddleware) {
208
+ return { isProtected: false, reason: 'No auth middleware detected' }
209
+ }
210
+
211
+ // Normalize path
212
+ const normalizedPath = routePath.startsWith('/') ? routePath : `/${routePath}`
213
+
214
+ // Check if explicitly public
215
+ for (const publicPath of config.publicPaths) {
216
+ if (matchPath(normalizedPath, publicPath)) {
217
+ return { isProtected: false, reason: `Matches public route pattern: ${publicPath}` }
218
+ }
219
+ }
220
+
221
+ // Check if explicitly protected
222
+ for (const protectedPath of config.protectedPaths) {
223
+ if (matchPath(normalizedPath, protectedPath)) {
224
+ return {
225
+ isProtected: true,
226
+ reason: `Protected by ${config.authType || 'auth'} middleware (matches: ${protectedPath})`,
227
+ }
228
+ }
229
+ }
230
+
231
+ // Check matchers
232
+ for (const matcher of config.matchers) {
233
+ if (matchPath(normalizedPath, matcher)) {
234
+ return {
235
+ isProtected: true,
236
+ reason: `Protected by middleware matcher: ${matcher}`,
237
+ }
238
+ }
239
+ }
240
+
241
+ // Default: if we have auth middleware and no explicit public match,
242
+ // consider API routes as protected
243
+ if (normalizedPath.startsWith('/api/')) {
244
+ return {
245
+ isProtected: true,
246
+ reason: `API route likely protected by ${config.authType || 'auth'} middleware`,
247
+ }
248
+ }
249
+
250
+ return { isProtected: false, reason: 'Route not covered by middleware matchers' }
251
+ }
252
+
253
+ /**
254
+ * Extract route path from a file path (e.g., app/api/users/route.ts -> /api/users)
255
+ */
256
+ export function getRoutePathFromFile(filePath: string): string | null {
257
+ // Next.js App Router: app/api/users/route.ts -> /api/users
258
+ const appRouterMatch = filePath.match(/app(\/.*?)\/route\.(ts|js)$/i)
259
+ if (appRouterMatch) {
260
+ return appRouterMatch[1]
261
+ }
262
+
263
+ // Next.js Pages Router: pages/api/users.ts -> /api/users
264
+ const pagesRouterMatch = filePath.match(/pages(\/api\/.*?)\.(ts|js)$/i)
265
+ if (pagesRouterMatch) {
266
+ return pagesRouterMatch[1]
267
+ }
268
+
269
+ // Express-style routes: routes/users.ts (harder to determine path)
270
+ // Return null for now - would need more context
271
+ return null
272
+ }
273
+
274
+ /**
275
+ * Simple glob-like path matching
276
+ * Supports: ** (any path), * (single segment), :param (path params)
277
+ */
278
+ function matchPath(actualPath: string, pattern: string): boolean {
279
+ // Normalize
280
+ const normActual = actualPath.replace(/\/+/g, '/')
281
+ let normPattern = pattern.replace(/\/+/g, '/')
282
+
283
+ // Handle Next.js :path* patterns
284
+ normPattern = normPattern
285
+ .replace(/:path\*/g, '**')
286
+ .replace(/:\w+/g, '[^/]+')
287
+
288
+ // Convert glob to regex
289
+ const regexPattern = normPattern
290
+ .replace(/\*\*/g, '<<<DOUBLESTAR>>>')
291
+ .replace(/\*/g, '[^/]*')
292
+ .replace(/<<<DOUBLESTAR>>>/g, '.*')
293
+ .replace(/\//g, '\\/')
294
+
295
+ const regex = new RegExp(`^${regexPattern}$`)
296
+ return regex.test(normActual)
297
+ }
298
+
299
+ /**
300
+ * Get user-scoping patterns from file content
301
+ * Detects patterns like: user_id, userId, session.user.id
302
+ */
303
+ export function detectUserScopingPatterns(content: string): {
304
+ hasUserScoping: boolean
305
+ patterns: string[]
306
+ } {
307
+ const patterns: string[] = []
308
+
309
+ // Common user ID patterns in queries/operations
310
+ const userIdPatterns = [
311
+ /\.eq\s*\(\s*['"`]user_id['"`]/i, // Supabase .eq('user_id', ...)
312
+ /\.filter\s*\(\s*user_id\s*=/i, // Django-style
313
+ /where\s*\(\s*['"`]?user_id['"`]?\s*=/i, // SQL-like
314
+ /userId\s*[=:]/i, // Generic userId
315
+ /user\.id\s*[=:]/i, // user.id
316
+ /session\.user\.id/i, // Next.js session
317
+ /req\.user\.id/i, // Express req.user
318
+ /getCurrentUserId\s*\(/i, // Custom helper
319
+ /getAuthUser\s*\(/i, // Auth helper
320
+ /auth\(\).*\.userId/i, // Clerk auth().userId
321
+ ]
322
+
323
+ for (const pattern of userIdPatterns) {
324
+ if (pattern.test(content)) {
325
+ patterns.push(pattern.source)
326
+ }
327
+ }
328
+
329
+ return {
330
+ hasUserScoping: patterns.length > 0,
331
+ patterns,
332
+ }
333
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * OAuth Flow Detector
3
+ * Detects OAuth state generation and validation across multiple files
4
+ * to prevent false positives when state is implemented correctly but split across files.
5
+ */
6
+
7
+ import type { ScanFile } from '../types'
8
+
9
+ export interface OAuthFlowContext {
10
+ /** Whether state generation is detected in any file */
11
+ hasStateGeneration: boolean
12
+ /** File where state is generated */
13
+ stateGenerationFile?: string
14
+ /** Line number where state is generated */
15
+ stateGenerationLine?: number
16
+ /** Whether state validation is detected in any file */
17
+ hasStateValidation: boolean
18
+ /** File where state is validated */
19
+ stateValidationFile?: string
20
+ /** Line number where state is validated */
21
+ stateValidationLine?: number
22
+ /** Whether PKCE code_verifier is used */
23
+ hasCodeVerifier: boolean
24
+ /** File where code_verifier is generated */
25
+ codeVerifierFile?: string
26
+ /** Detected OAuth flow type */
27
+ flowType: 'authorization_code' | 'client_credentials' | 'implicit' | 'pkce' | 'unknown'
28
+ /** OAuth providers detected */
29
+ providers: string[]
30
+ }
31
+
32
+ /**
33
+ * Patterns that indicate OAuth state generation
34
+ */
35
+ const STATE_GENERATION_PATTERNS = [
36
+ /generateState\s*\(/i,
37
+ /crypto\.randomBytes.*state/i,
38
+ /state\s*=\s*.*random/i,
39
+ /setCookie.*oauth.*state/i,
40
+ /cookies?\(\s*\)\.set\s*\([^)]*state/i,
41
+ /state\s*=\s*generateRandomString/i,
42
+ /state\s*=\s*nanoid\s*\(/i,
43
+ /state\s*=\s*uuid/i,
44
+ /state\s*=\s*crypto\.randomUUID/i,
45
+ /createState\s*\(/i,
46
+ /oauth.*state.*=.*crypto/i,
47
+ /\.setState\s*\(/i,
48
+ /stateParam\s*=\s*/i,
49
+ /generateOAuthState/i,
50
+ ]
51
+
52
+ /**
53
+ * Patterns that indicate OAuth state validation
54
+ */
55
+ const STATE_VALIDATION_PATTERNS = [
56
+ /state\s*!==\s*storedState/i,
57
+ /storedState.*!==.*state/i,
58
+ /validateState\s*\(/i,
59
+ /getCookie.*oauth.*state/i,
60
+ /cookies?\(\s*\)\.get\s*\([^)]*state/i,
61
+ /verifyState\s*\(/i,
62
+ /checkState\s*\(/i,
63
+ /state\s*===\s*savedState/i,
64
+ /savedState\s*===\s*state/i,
65
+ /if\s*\(\s*!?\s*state\s*\)/i,
66
+ /state\s*!==\s*.*\.get\s*\(/i,
67
+ /\.getState\s*\(/i,
68
+ /compareState\s*\(/i,
69
+ ]
70
+
71
+ /**
72
+ * Patterns that indicate PKCE code_verifier usage
73
+ */
74
+ const CODE_VERIFIER_PATTERNS = [
75
+ /code_verifier/i,
76
+ /codeVerifier/i,
77
+ /generateCodeVerifier/i,
78
+ /createCodeVerifier/i,
79
+ /pkce/i,
80
+ /code_challenge/i,
81
+ /codeChallenge/i,
82
+ ]
83
+
84
+ /**
85
+ * Patterns that indicate client_credentials flow (no state needed)
86
+ */
87
+ const CLIENT_CREDENTIALS_PATTERNS = [
88
+ /grant_type.*client_credentials/i,
89
+ /client_credentials.*grant/i,
90
+ /\.clientCredentials\s*\(/i,
91
+ /getClientCredentialsToken/i,
92
+ /machine-to-machine/i,
93
+ /m2m.*auth/i,
94
+ ]
95
+
96
+ /**
97
+ * Known OAuth provider patterns
98
+ */
99
+ const OAUTH_PROVIDER_PATTERNS: Record<string, RegExp[]> = {
100
+ google: [/googleapis\.com\/oauth/i, /accounts\.google\.com/i, /google.*oauth/i],
101
+ github: [/github\.com\/login\/oauth/i, /api\.github\.com.*oauth/i],
102
+ microsoft: [/login\.microsoftonline\.com/i, /microsoft.*oauth/i, /azure.*oauth/i],
103
+ facebook: [/facebook\.com\/.*oauth/i, /graph\.facebook\.com/i],
104
+ twitter: [/twitter\.com\/oauth/i, /api\.twitter\.com.*oauth/i],
105
+ apple: [/appleid\.apple\.com/i, /apple.*oauth/i],
106
+ auth0: [/auth0\.com/i, /\.auth0\./i],
107
+ okta: [/okta\.com/i, /\.okta\./i],
108
+ clerk: [/clerk\.dev/i, /clerk\.com/i, /@clerk\//i],
109
+ nextauth: [/next-auth/i, /nextauth/i, /authjs/i],
110
+ lucia: [/lucia-auth/i, /lucia\.ts/i],
111
+ arctic: [/arctic/i, /oslo\/oauth/i],
112
+ }
113
+
114
+ /**
115
+ * Detect OAuth flow patterns across multiple files
116
+ */
117
+ export function detectOAuthFlow(files: ScanFile[]): OAuthFlowContext {
118
+ const context: OAuthFlowContext = {
119
+ hasStateGeneration: false,
120
+ hasStateValidation: false,
121
+ hasCodeVerifier: false,
122
+ flowType: 'unknown',
123
+ providers: [],
124
+ }
125
+
126
+ const detectedProviders = new Set<string>()
127
+
128
+ for (const file of files) {
129
+ const content = file.content
130
+ const lines = content.split('\n')
131
+
132
+ // Check for state generation
133
+ if (!context.hasStateGeneration) {
134
+ for (let i = 0; i < lines.length; i++) {
135
+ const line = lines[i]
136
+ if (STATE_GENERATION_PATTERNS.some(p => p.test(line))) {
137
+ context.hasStateGeneration = true
138
+ context.stateGenerationFile = file.path
139
+ context.stateGenerationLine = i + 1
140
+ break
141
+ }
142
+ }
143
+ }
144
+
145
+ // Check for state validation
146
+ if (!context.hasStateValidation) {
147
+ for (let i = 0; i < lines.length; i++) {
148
+ const line = lines[i]
149
+ if (STATE_VALIDATION_PATTERNS.some(p => p.test(line))) {
150
+ context.hasStateValidation = true
151
+ context.stateValidationFile = file.path
152
+ context.stateValidationLine = i + 1
153
+ break
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check for PKCE
159
+ if (!context.hasCodeVerifier) {
160
+ if (CODE_VERIFIER_PATTERNS.some(p => p.test(content))) {
161
+ context.hasCodeVerifier = true
162
+ context.codeVerifierFile = file.path
163
+ }
164
+ }
165
+
166
+ // Check for client_credentials flow
167
+ if (context.flowType === 'unknown') {
168
+ if (CLIENT_CREDENTIALS_PATTERNS.some(p => p.test(content))) {
169
+ context.flowType = 'client_credentials'
170
+ }
171
+ }
172
+
173
+ // Detect OAuth providers
174
+ for (const [provider, patterns] of Object.entries(OAUTH_PROVIDER_PATTERNS)) {
175
+ if (patterns.some(p => p.test(content))) {
176
+ detectedProviders.add(provider)
177
+ }
178
+ }
179
+ }
180
+
181
+ // Determine flow type
182
+ if (context.flowType === 'unknown') {
183
+ if (context.hasCodeVerifier) {
184
+ context.flowType = 'pkce'
185
+ } else if (context.hasStateGeneration || context.hasStateValidation) {
186
+ context.flowType = 'authorization_code'
187
+ }
188
+ }
189
+
190
+ context.providers = Array.from(detectedProviders)
191
+
192
+ return context
193
+ }
194
+
195
+ /**
196
+ * Check if OAuth state is properly implemented across files
197
+ */
198
+ export function isOAuthStateImplemented(context: OAuthFlowContext): boolean {
199
+ // client_credentials flow doesn't need state
200
+ if (context.flowType === 'client_credentials') {
201
+ return true
202
+ }
203
+
204
+ // PKCE flow with code_verifier is secure even without traditional state
205
+ if (context.hasCodeVerifier) {
206
+ return true
207
+ }
208
+
209
+ // Both generation and validation must be present
210
+ return context.hasStateGeneration && context.hasStateValidation
211
+ }
212
+
213
+ /**
214
+ * Get a summary of the OAuth flow for logging/context
215
+ */
216
+ export function getOAuthFlowSummary(context: OAuthFlowContext): string {
217
+ const parts: string[] = []
218
+
219
+ if (context.flowType !== 'unknown') {
220
+ parts.push(`Flow: ${context.flowType}`)
221
+ }
222
+
223
+ if (context.providers.length > 0) {
224
+ parts.push(`Providers: ${context.providers.join(', ')}`)
225
+ }
226
+
227
+ if (context.hasStateGeneration) {
228
+ parts.push(`State generated in: ${context.stateGenerationFile}`)
229
+ }
230
+
231
+ if (context.hasStateValidation) {
232
+ parts.push(`State validated in: ${context.stateValidationFile}`)
233
+ }
234
+
235
+ if (context.hasCodeVerifier) {
236
+ parts.push(`PKCE enabled (${context.codeVerifierFile})`)
237
+ }
238
+
239
+ if (isOAuthStateImplemented(context)) {
240
+ parts.push('✓ OAuth state properly implemented')
241
+ } else if (context.hasStateGeneration || context.hasStateValidation) {
242
+ parts.push('⚠ Partial OAuth state implementation detected')
243
+ }
244
+
245
+ return parts.join('; ')
246
+ }