@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.
- package/dist/formatters/cli-terminal.d.ts +27 -0
- package/dist/formatters/cli-terminal.d.ts.map +1 -0
- package/dist/formatters/cli-terminal.js +412 -0
- package/dist/formatters/cli-terminal.js.map +1 -0
- package/dist/formatters/github-comment.d.ts +41 -0
- package/dist/formatters/github-comment.d.ts.map +1 -0
- package/dist/formatters/github-comment.js +306 -0
- package/dist/formatters/github-comment.js.map +1 -0
- package/dist/formatters/grouping.d.ts +52 -0
- package/dist/formatters/grouping.d.ts.map +1 -0
- package/dist/formatters/grouping.js +152 -0
- package/dist/formatters/grouping.js.map +1 -0
- package/dist/formatters/index.d.ts +9 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +35 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/vscode-diagnostic.d.ts +103 -0
- package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
- package/dist/formatters/vscode-diagnostic.js +151 -0
- package/dist/formatters/vscode-diagnostic.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/layer1/comments.d.ts +8 -0
- package/dist/layer1/comments.d.ts.map +1 -0
- package/dist/layer1/comments.js +203 -0
- package/dist/layer1/comments.js.map +1 -0
- package/dist/layer1/config-audit.d.ts +8 -0
- package/dist/layer1/config-audit.d.ts.map +1 -0
- package/dist/layer1/config-audit.js +252 -0
- package/dist/layer1/config-audit.js.map +1 -0
- package/dist/layer1/entropy.d.ts +8 -0
- package/dist/layer1/entropy.d.ts.map +1 -0
- package/dist/layer1/entropy.js +500 -0
- package/dist/layer1/entropy.js.map +1 -0
- package/dist/layer1/file-flags.d.ts +7 -0
- package/dist/layer1/file-flags.d.ts.map +1 -0
- package/dist/layer1/file-flags.js +112 -0
- package/dist/layer1/file-flags.js.map +1 -0
- package/dist/layer1/index.d.ts +36 -0
- package/dist/layer1/index.d.ts.map +1 -0
- package/dist/layer1/index.js +132 -0
- package/dist/layer1/index.js.map +1 -0
- package/dist/layer1/patterns.d.ts +8 -0
- package/dist/layer1/patterns.d.ts.map +1 -0
- package/dist/layer1/patterns.js +482 -0
- package/dist/layer1/patterns.js.map +1 -0
- package/dist/layer1/urls.d.ts +8 -0
- package/dist/layer1/urls.d.ts.map +1 -0
- package/dist/layer1/urls.js +296 -0
- package/dist/layer1/urls.js.map +1 -0
- package/dist/layer1/weak-crypto.d.ts +7 -0
- package/dist/layer1/weak-crypto.d.ts.map +1 -0
- package/dist/layer1/weak-crypto.js +291 -0
- package/dist/layer1/weak-crypto.js.map +1 -0
- package/dist/layer2/ai-agent-tools.d.ts +19 -0
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
- package/dist/layer2/ai-agent-tools.js +528 -0
- package/dist/layer2/ai-agent-tools.js.map +1 -0
- package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
- package/dist/layer2/ai-endpoint-protection.js +332 -0
- package/dist/layer2/ai-endpoint-protection.js.map +1 -0
- package/dist/layer2/ai-execution-sinks.d.ts +18 -0
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
- package/dist/layer2/ai-execution-sinks.js +496 -0
- package/dist/layer2/ai-execution-sinks.js.map +1 -0
- package/dist/layer2/ai-fingerprinting.d.ts +7 -0
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
- package/dist/layer2/ai-fingerprinting.js +654 -0
- package/dist/layer2/ai-fingerprinting.js.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.js +356 -0
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
- package/dist/layer2/ai-rag-safety.d.ts +21 -0
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
- package/dist/layer2/ai-rag-safety.js +459 -0
- package/dist/layer2/ai-rag-safety.js.map +1 -0
- package/dist/layer2/ai-schema-validation.d.ts +25 -0
- package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
- package/dist/layer2/ai-schema-validation.js +375 -0
- package/dist/layer2/ai-schema-validation.js.map +1 -0
- package/dist/layer2/auth-antipatterns.d.ts +20 -0
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
- package/dist/layer2/auth-antipatterns.js +333 -0
- package/dist/layer2/auth-antipatterns.js.map +1 -0
- package/dist/layer2/byok-patterns.d.ts +12 -0
- package/dist/layer2/byok-patterns.d.ts.map +1 -0
- package/dist/layer2/byok-patterns.js +299 -0
- package/dist/layer2/byok-patterns.js.map +1 -0
- package/dist/layer2/dangerous-functions.d.ts +7 -0
- package/dist/layer2/dangerous-functions.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions.js +1375 -0
- package/dist/layer2/dangerous-functions.js.map +1 -0
- package/dist/layer2/data-exposure.d.ts +16 -0
- package/dist/layer2/data-exposure.d.ts.map +1 -0
- package/dist/layer2/data-exposure.js +279 -0
- package/dist/layer2/data-exposure.js.map +1 -0
- package/dist/layer2/framework-checks.d.ts +7 -0
- package/dist/layer2/framework-checks.d.ts.map +1 -0
- package/dist/layer2/framework-checks.js +388 -0
- package/dist/layer2/framework-checks.js.map +1 -0
- package/dist/layer2/index.d.ts +58 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +380 -0
- package/dist/layer2/index.js.map +1 -0
- package/dist/layer2/logic-gates.d.ts +7 -0
- package/dist/layer2/logic-gates.d.ts.map +1 -0
- package/dist/layer2/logic-gates.js +182 -0
- package/dist/layer2/logic-gates.js.map +1 -0
- package/dist/layer2/risky-imports.d.ts +7 -0
- package/dist/layer2/risky-imports.d.ts.map +1 -0
- package/dist/layer2/risky-imports.js +161 -0
- package/dist/layer2/risky-imports.js.map +1 -0
- package/dist/layer2/variables.d.ts +8 -0
- package/dist/layer2/variables.d.ts.map +1 -0
- package/dist/layer2/variables.js +152 -0
- package/dist/layer2/variables.js.map +1 -0
- package/dist/layer3/anthropic.d.ts +83 -0
- package/dist/layer3/anthropic.d.ts.map +1 -0
- package/dist/layer3/anthropic.js +1745 -0
- package/dist/layer3/anthropic.js.map +1 -0
- package/dist/layer3/index.d.ts +24 -0
- package/dist/layer3/index.d.ts.map +1 -0
- package/dist/layer3/index.js +119 -0
- package/dist/layer3/index.js.map +1 -0
- package/dist/layer3/openai.d.ts +25 -0
- package/dist/layer3/openai.d.ts.map +1 -0
- package/dist/layer3/openai.js +238 -0
- package/dist/layer3/openai.js.map +1 -0
- package/dist/layer3/package-check.d.ts +63 -0
- package/dist/layer3/package-check.d.ts.map +1 -0
- package/dist/layer3/package-check.js +508 -0
- package/dist/layer3/package-check.js.map +1 -0
- package/dist/modes/incremental.d.ts +66 -0
- package/dist/modes/incremental.d.ts.map +1 -0
- package/dist/modes/incremental.js +200 -0
- package/dist/modes/incremental.js.map +1 -0
- package/dist/tiers.d.ts +125 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +234 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth-helper-detector.d.ts +56 -0
- package/dist/utils/auth-helper-detector.d.ts.map +1 -0
- package/dist/utils/auth-helper-detector.js +360 -0
- package/dist/utils/auth-helper-detector.js.map +1 -0
- package/dist/utils/context-helpers.d.ts +96 -0
- package/dist/utils/context-helpers.d.ts.map +1 -0
- package/dist/utils/context-helpers.js +493 -0
- package/dist/utils/context-helpers.js.map +1 -0
- package/dist/utils/diff-detector.d.ts +53 -0
- package/dist/utils/diff-detector.d.ts.map +1 -0
- package/dist/utils/diff-detector.js +104 -0
- package/dist/utils/diff-detector.js.map +1 -0
- package/dist/utils/diff-parser.d.ts +80 -0
- package/dist/utils/diff-parser.d.ts.map +1 -0
- package/dist/utils/diff-parser.js +202 -0
- package/dist/utils/diff-parser.js.map +1 -0
- package/dist/utils/imported-auth-detector.d.ts +37 -0
- package/dist/utils/imported-auth-detector.d.ts.map +1 -0
- package/dist/utils/imported-auth-detector.js +251 -0
- package/dist/utils/imported-auth-detector.js.map +1 -0
- package/dist/utils/middleware-detector.d.ts +55 -0
- package/dist/utils/middleware-detector.d.ts.map +1 -0
- package/dist/utils/middleware-detector.js +260 -0
- package/dist/utils/middleware-detector.js.map +1 -0
- package/dist/utils/oauth-flow-detector.d.ts +41 -0
- package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
- package/dist/utils/oauth-flow-detector.js +202 -0
- package/dist/utils/oauth-flow-detector.js.map +1 -0
- package/dist/utils/path-exclusions.d.ts +55 -0
- package/dist/utils/path-exclusions.d.ts.map +1 -0
- package/dist/utils/path-exclusions.js +222 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/project-context-builder.d.ts +119 -0
- package/dist/utils/project-context-builder.d.ts.map +1 -0
- package/dist/utils/project-context-builder.js +534 -0
- package/dist/utils/project-context-builder.js.map +1 -0
- package/dist/utils/registry-clients.d.ts +93 -0
- package/dist/utils/registry-clients.d.ts.map +1 -0
- package/dist/utils/registry-clients.js +273 -0
- package/dist/utils/registry-clients.js.map +1 -0
- package/dist/utils/trpc-analyzer.d.ts +78 -0
- package/dist/utils/trpc-analyzer.d.ts.map +1 -0
- package/dist/utils/trpc-analyzer.js +297 -0
- package/dist/utils/trpc-analyzer.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
- package/src/__tests__/benchmark/fixtures/index.ts +68 -0
- package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
- package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
- package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
- package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
- package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
- package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
- package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
- package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
- package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
- package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
- package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
- package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
- package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
- package/src/__tests__/benchmark/index.ts +29 -0
- package/src/__tests__/benchmark/run-benchmark.ts +144 -0
- package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
- package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
- package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
- package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
- package/src/__tests__/benchmark/types.ts +144 -0
- package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
- package/src/__tests__/regression/known-false-positives.test.ts +467 -0
- package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
- package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
- package/src/__tests__/validation/analyze-results.ts +542 -0
- package/src/__tests__/validation/extract-for-triage.ts +146 -0
- package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
- package/src/__tests__/validation/run-validation.ts +364 -0
- package/src/__tests__/validation/triage-template.md +132 -0
- package/src/formatters/cli-terminal.ts +446 -0
- package/src/formatters/github-comment.ts +382 -0
- package/src/formatters/grouping.ts +190 -0
- package/src/formatters/index.ts +47 -0
- package/src/formatters/vscode-diagnostic.ts +243 -0
- package/src/index.ts +823 -0
- package/src/layer1/comments.ts +218 -0
- package/src/layer1/config-audit.ts +289 -0
- package/src/layer1/entropy.ts +583 -0
- package/src/layer1/file-flags.ts +127 -0
- package/src/layer1/index.ts +181 -0
- package/src/layer1/patterns.ts +516 -0
- package/src/layer1/urls.ts +334 -0
- package/src/layer1/weak-crypto.ts +328 -0
- package/src/layer2/ai-agent-tools.ts +601 -0
- package/src/layer2/ai-endpoint-protection.ts +387 -0
- package/src/layer2/ai-execution-sinks.ts +580 -0
- package/src/layer2/ai-fingerprinting.ts +758 -0
- package/src/layer2/ai-prompt-hygiene.ts +411 -0
- package/src/layer2/ai-rag-safety.ts +511 -0
- package/src/layer2/ai-schema-validation.ts +421 -0
- package/src/layer2/auth-antipatterns.ts +394 -0
- package/src/layer2/byok-patterns.ts +336 -0
- package/src/layer2/dangerous-functions.ts +1563 -0
- package/src/layer2/data-exposure.ts +315 -0
- package/src/layer2/framework-checks.ts +433 -0
- package/src/layer2/index.ts +473 -0
- package/src/layer2/logic-gates.ts +206 -0
- package/src/layer2/risky-imports.ts +186 -0
- package/src/layer2/variables.ts +166 -0
- package/src/layer3/anthropic.ts +2030 -0
- package/src/layer3/index.ts +130 -0
- package/src/layer3/package-check.ts +604 -0
- package/src/modes/incremental.ts +293 -0
- package/src/tiers.ts +318 -0
- package/src/types.ts +284 -0
- package/src/utils/auth-helper-detector.ts +443 -0
- package/src/utils/context-helpers.ts +535 -0
- package/src/utils/diff-detector.ts +135 -0
- package/src/utils/diff-parser.ts +272 -0
- package/src/utils/imported-auth-detector.ts +320 -0
- package/src/utils/middleware-detector.ts +333 -0
- package/src/utils/oauth-flow-detector.ts +246 -0
- package/src/utils/path-exclusions.ts +266 -0
- package/src/utils/project-context-builder.ts +707 -0
- package/src/utils/registry-clients.ts +351 -0
- 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
|
+
}
|