@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,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2: AI Endpoint Protection Detection
|
|
3
|
+
* Detects AI/LLM endpoints without proper authentication or rate limiting
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - M5.2: AI endpoints without auth/rate limiting
|
|
7
|
+
* - Cost-bearing AI endpoints exposed publicly
|
|
8
|
+
* - Missing rate limiting on AI routes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Vulnerability, VulnerabilitySeverity } from '../types'
|
|
12
|
+
import type { MiddlewareAuthConfig } from '../utils/middleware-detector'
|
|
13
|
+
import {
|
|
14
|
+
isComment,
|
|
15
|
+
isTestOrMockFile,
|
|
16
|
+
isDocumentationFile,
|
|
17
|
+
isScannerOrFixtureFile,
|
|
18
|
+
isExampleDirectory,
|
|
19
|
+
} from '../utils/context-helpers'
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Context Detection
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if file is a route/API handler
|
|
27
|
+
*/
|
|
28
|
+
function isRouteFile(filePath: string): boolean {
|
|
29
|
+
const routePatterns = [
|
|
30
|
+
/\/api\/.*\.(ts|js)$/i,
|
|
31
|
+
/\/routes?\/.*\.(ts|js)$/i,
|
|
32
|
+
/route\.(ts|js)$/i,
|
|
33
|
+
/\/pages\/api\/.*\.(ts|js)$/i,
|
|
34
|
+
/\/app\/.*\/route\.(ts|js)$/i,
|
|
35
|
+
/\.(controller|handler)\.(ts|js)$/i,
|
|
36
|
+
]
|
|
37
|
+
return routePatterns.some(p => p.test(filePath))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if content contains AI/LLM API calls
|
|
42
|
+
*/
|
|
43
|
+
function hasAIApiCalls(content: string): boolean {
|
|
44
|
+
const aiPatterns = [
|
|
45
|
+
// OpenAI
|
|
46
|
+
/openai\.chat\.completions?\.create/i,
|
|
47
|
+
/openai\.completions?\.create/i,
|
|
48
|
+
/openai\.embeddings?\.create/i,
|
|
49
|
+
// Anthropic
|
|
50
|
+
/anthropic\.messages\.create/i,
|
|
51
|
+
/anthropic\.completions?\.create/i,
|
|
52
|
+
// Vercel AI SDK
|
|
53
|
+
/\bgenerateText\s*\(/i,
|
|
54
|
+
/\bstreamText\s*\(/i,
|
|
55
|
+
/\bgenerateObject\s*\(/i,
|
|
56
|
+
/\bstreamObject\s*\(/i,
|
|
57
|
+
// Generic patterns
|
|
58
|
+
/\.create\s*\(\s*\{[^}]*model\s*:/i,
|
|
59
|
+
/ChatCompletion|MessageCreate/i,
|
|
60
|
+
// LangChain
|
|
61
|
+
/\.invoke\s*\([^)]*\)/i,
|
|
62
|
+
/ChatOpenAI|ChatAnthropic|ChatModel/i,
|
|
63
|
+
// Other providers
|
|
64
|
+
/replicate\.run/i,
|
|
65
|
+
/cohere\.(?:generate|chat)/i,
|
|
66
|
+
/mistral\.chat/i,
|
|
67
|
+
]
|
|
68
|
+
return aiPatterns.some(p => p.test(content))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if there's authentication in the route
|
|
73
|
+
*/
|
|
74
|
+
function hasAuthentication(content: string): boolean {
|
|
75
|
+
const authPatterns = [
|
|
76
|
+
// Session/user checks
|
|
77
|
+
/getSession|getServerSession|getCurrentUser/i,
|
|
78
|
+
/auth\(\)|requireAuth|withAuth|ensureAuth/i,
|
|
79
|
+
/verifyToken|validateToken|checkToken/i,
|
|
80
|
+
/req\.user|request\.user|context\.user/i,
|
|
81
|
+
/isAuthenticated|checkAuth/i,
|
|
82
|
+
// Header checks
|
|
83
|
+
/Authorization.*Bearer/i,
|
|
84
|
+
/headers\[['"`]authorization['"`]\]/i,
|
|
85
|
+
/getHeader\(['"`]authorization['"`]\)/i,
|
|
86
|
+
// API key validation
|
|
87
|
+
/apiKey|api_key|x-api-key/i,
|
|
88
|
+
// Clerk/NextAuth/Auth0
|
|
89
|
+
/currentUser\(\)|auth\(\)\.protect/i,
|
|
90
|
+
/getAuth\(\)|clerkClient/i,
|
|
91
|
+
// User ID extraction (implies auth)
|
|
92
|
+
/userId|user\.id|currentUserId/i,
|
|
93
|
+
// 401/403 responses
|
|
94
|
+
/status\(401\)|status\(403\)/i,
|
|
95
|
+
/Unauthorized|Forbidden/i,
|
|
96
|
+
]
|
|
97
|
+
return authPatterns.some(p => p.test(content))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if there's rate limiting
|
|
102
|
+
*/
|
|
103
|
+
function hasRateLimiting(content: string): boolean {
|
|
104
|
+
const rateLimitPatterns = [
|
|
105
|
+
// Rate limit middleware/libraries
|
|
106
|
+
/rateLimit|rateLimiter/i,
|
|
107
|
+
/express-rate-limit/i,
|
|
108
|
+
/upstash.*ratelimit|@upstash\/ratelimit/i,
|
|
109
|
+
/rate-limiter-flexible/i,
|
|
110
|
+
// Custom rate limiting
|
|
111
|
+
/throttle|Throttle/i,
|
|
112
|
+
/requestsPerMinute|requestsPerHour/i,
|
|
113
|
+
/maxRequests|requestLimit/i,
|
|
114
|
+
// Rate limit headers
|
|
115
|
+
/X-RateLimit|x-ratelimit/i,
|
|
116
|
+
/Retry-After/i,
|
|
117
|
+
// 429 responses
|
|
118
|
+
/status\(429\)|\.status === 429/i,
|
|
119
|
+
/Too Many Requests|TooManyRequests/i,
|
|
120
|
+
// Sliding window/token bucket
|
|
121
|
+
/slidingWindow|tokenBucket|fixedWindow/i,
|
|
122
|
+
]
|
|
123
|
+
return rateLimitPatterns.some(p => p.test(content))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if this is a BYOK (Bring Your Own Key) endpoint
|
|
128
|
+
* BYOK endpoints have lower risk since user pays for their own usage
|
|
129
|
+
*/
|
|
130
|
+
function isBYOKEndpoint(content: string): boolean {
|
|
131
|
+
const byokPatterns = [
|
|
132
|
+
/req\.body\.(?:apiKey|api_key|openaiKey|anthropicKey)/i,
|
|
133
|
+
/request\.(?:apiKey|api_key|openaiKey|anthropicKey)/i,
|
|
134
|
+
/userApiKey|user_api_key|clientApiKey/i,
|
|
135
|
+
/['"`](?:apiKey|api_key|openai_key|anthropic_key)['"`]\s*:/i,
|
|
136
|
+
// Headers with user's key
|
|
137
|
+
/headers\[['"`]x-openai-key['"`]\]/i,
|
|
138
|
+
/headers\[['"`]x-api-key['"`]\]/i,
|
|
139
|
+
]
|
|
140
|
+
return byokPatterns.some(p => p.test(content))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if route is protected by middleware (from config)
|
|
145
|
+
*/
|
|
146
|
+
function isProtectedByMiddleware(
|
|
147
|
+
filePath: string,
|
|
148
|
+
middlewareConfig?: MiddlewareAuthConfig
|
|
149
|
+
): boolean {
|
|
150
|
+
if (!middlewareConfig?.protectedPaths) return false
|
|
151
|
+
|
|
152
|
+
// Extract route path from file path
|
|
153
|
+
const routePath = filePath
|
|
154
|
+
.replace(/.*\/(pages|app)\/api/, '/api')
|
|
155
|
+
.replace(/.*\/routes?/, '')
|
|
156
|
+
.replace(/\/route\.(ts|js)$/, '')
|
|
157
|
+
.replace(/\.(ts|js)$/, '')
|
|
158
|
+
.replace(/\[([^\]]+)\]/g, ':$1')
|
|
159
|
+
|
|
160
|
+
return middlewareConfig.protectedPaths.some(p => routePath.startsWith(p))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if this is an internal/admin-only route
|
|
165
|
+
*/
|
|
166
|
+
function isInternalRoute(filePath: string, content: string): boolean {
|
|
167
|
+
const internalPatterns = [
|
|
168
|
+
/\/internal\//i,
|
|
169
|
+
/\/admin\//i,
|
|
170
|
+
/\/_internal\//i,
|
|
171
|
+
// Content checks
|
|
172
|
+
/adminOnly|internalOnly|isAdmin/i,
|
|
173
|
+
/process\.env\.INTERNAL_SECRET/i,
|
|
174
|
+
]
|
|
175
|
+
return internalPatterns.some(p => p.test(filePath) || p.test(content))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get surrounding context
|
|
180
|
+
*/
|
|
181
|
+
function getSurroundingContext(content: string, lineIndex: number, windowSize: number = 50): string {
|
|
182
|
+
const lines = content.split('\n')
|
|
183
|
+
const start = Math.max(0, lineIndex - windowSize)
|
|
184
|
+
const end = Math.min(lines.length, lineIndex + windowSize)
|
|
185
|
+
return lines.slice(start, end).join('\n')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Pattern Definitions
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
interface EndpointProtectionPattern {
|
|
193
|
+
name: string
|
|
194
|
+
pattern: RegExp
|
|
195
|
+
framework: 'nextjs_app' | 'nextjs_pages' | 'express' | 'fastify' | 'generic'
|
|
196
|
+
description: string
|
|
197
|
+
suggestedFix: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Route handler patterns by framework
|
|
202
|
+
*/
|
|
203
|
+
const ROUTE_HANDLER_PATTERNS: EndpointProtectionPattern[] = [
|
|
204
|
+
// Next.js App Router
|
|
205
|
+
{
|
|
206
|
+
name: 'Next.js App Router AI endpoint',
|
|
207
|
+
pattern: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/gi,
|
|
208
|
+
framework: 'nextjs_app',
|
|
209
|
+
description: 'Next.js App Router endpoint with AI API calls lacks protection.',
|
|
210
|
+
suggestedFix: 'Add authentication check at start of handler. Consider using middleware for auth and rate limiting.',
|
|
211
|
+
},
|
|
212
|
+
// Next.js Pages Router
|
|
213
|
+
{
|
|
214
|
+
name: 'Next.js Pages API route',
|
|
215
|
+
pattern: /export\s+default\s+(?:async\s+)?function\s*(?:\w+\s*)?\([^)]*req/gi,
|
|
216
|
+
framework: 'nextjs_pages',
|
|
217
|
+
description: 'Next.js Pages API route with AI calls lacks protection.',
|
|
218
|
+
suggestedFix: 'Add authentication: const session = await getServerSession(req, res, authOptions); if (!session) return res.status(401).json({error: "Unauthorized"})',
|
|
219
|
+
},
|
|
220
|
+
// Express
|
|
221
|
+
{
|
|
222
|
+
name: 'Express AI route',
|
|
223
|
+
pattern: /(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"`][^'"`]+['"`]\s*,/gi,
|
|
224
|
+
framework: 'express',
|
|
225
|
+
description: 'Express route with AI API calls lacks middleware protection.',
|
|
226
|
+
suggestedFix: 'Add authentication and rate limiting middleware: app.post("/api/chat", authMiddleware, rateLimiter, handler)',
|
|
227
|
+
},
|
|
228
|
+
// Fastify
|
|
229
|
+
{
|
|
230
|
+
name: 'Fastify AI route',
|
|
231
|
+
pattern: /fastify\.(get|post|put|patch|delete)\s*\(\s*['"`][^'"`]+['"`]/gi,
|
|
232
|
+
framework: 'fastify',
|
|
233
|
+
description: 'Fastify route with AI calls lacks protection.',
|
|
234
|
+
suggestedFix: 'Add preHandler hooks for auth and rate limiting: { preHandler: [authenticate, rateLimit] }',
|
|
235
|
+
},
|
|
236
|
+
// Generic handler exports
|
|
237
|
+
{
|
|
238
|
+
name: 'API handler with AI calls',
|
|
239
|
+
pattern: /export\s+(?:const|async\s+function)\s+(?:handler|apiHandler)\s*[=:]/gi,
|
|
240
|
+
framework: 'generic',
|
|
241
|
+
description: 'API handler with AI calls may lack protection.',
|
|
242
|
+
suggestedFix: 'Ensure authentication and rate limiting are applied before AI API calls.',
|
|
243
|
+
},
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Main Detection Function
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
export interface EndpointProtectionOptions {
|
|
251
|
+
middlewareConfig?: MiddlewareAuthConfig
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Main detection function for AI endpoint protection issues
|
|
256
|
+
*/
|
|
257
|
+
export function detectAIEndpointProtection(
|
|
258
|
+
content: string,
|
|
259
|
+
filePath: string,
|
|
260
|
+
options: EndpointProtectionOptions = {}
|
|
261
|
+
): Vulnerability[] {
|
|
262
|
+
const vulnerabilities: Vulnerability[] = []
|
|
263
|
+
|
|
264
|
+
// Skip non-applicable files
|
|
265
|
+
if (isScannerOrFixtureFile(filePath)) return vulnerabilities
|
|
266
|
+
if (isDocumentationFile(filePath)) return vulnerabilities
|
|
267
|
+
|
|
268
|
+
// Only scan route files that have AI API calls
|
|
269
|
+
if (!isRouteFile(filePath) || !hasAIApiCalls(content)) {
|
|
270
|
+
return vulnerabilities
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const lines = content.split('\n')
|
|
274
|
+
const isTestFile = isTestOrMockFile(filePath)
|
|
275
|
+
const isExample = isExampleDirectory(filePath)
|
|
276
|
+
|
|
277
|
+
// Check file-level protection
|
|
278
|
+
const fileHasAuth = hasAuthentication(content)
|
|
279
|
+
const fileHasRateLimit = hasRateLimiting(content)
|
|
280
|
+
const isByok = isBYOKEndpoint(content)
|
|
281
|
+
const isInternal = isInternalRoute(filePath, content)
|
|
282
|
+
const isMiddlewareProtected = isProtectedByMiddleware(filePath, options.middlewareConfig)
|
|
283
|
+
|
|
284
|
+
// Scan for route handler patterns
|
|
285
|
+
for (const pattern of ROUTE_HANDLER_PATTERNS) {
|
|
286
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags)
|
|
287
|
+
let match
|
|
288
|
+
|
|
289
|
+
while ((match = regex.exec(content)) !== null) {
|
|
290
|
+
const lineNumber = content.substring(0, match.index).split('\n').length
|
|
291
|
+
const lineContent = lines[lineNumber - 1]?.trim() || ''
|
|
292
|
+
|
|
293
|
+
// Skip comments
|
|
294
|
+
if (isComment(lineContent)) continue
|
|
295
|
+
|
|
296
|
+
// Get surrounding context
|
|
297
|
+
const context = getSurroundingContext(content, lineNumber - 1, 50)
|
|
298
|
+
|
|
299
|
+
// Check for local protection patterns (within handler)
|
|
300
|
+
const handlerHasAuth = hasAuthentication(context)
|
|
301
|
+
const handlerHasRateLimit = hasRateLimiting(context)
|
|
302
|
+
|
|
303
|
+
// Calculate severity based on protection status
|
|
304
|
+
let severity: VulnerabilitySeverity
|
|
305
|
+
const notes: string[] = []
|
|
306
|
+
|
|
307
|
+
if (isMiddlewareProtected || isInternal) {
|
|
308
|
+
severity = 'info'
|
|
309
|
+
notes.push(isMiddlewareProtected ? 'Protected by middleware' : 'Internal route')
|
|
310
|
+
} else if (handlerHasAuth || fileHasAuth) {
|
|
311
|
+
if (handlerHasRateLimit || fileHasRateLimit) {
|
|
312
|
+
// Has both auth and rate limiting
|
|
313
|
+
severity = 'info'
|
|
314
|
+
notes.push('Has authentication and rate limiting')
|
|
315
|
+
} else {
|
|
316
|
+
// Has auth but no rate limiting
|
|
317
|
+
severity = 'low'
|
|
318
|
+
notes.push('Has authentication but missing rate limiting')
|
|
319
|
+
}
|
|
320
|
+
} else if (handlerHasRateLimit || fileHasRateLimit) {
|
|
321
|
+
// Has rate limiting but no auth
|
|
322
|
+
severity = 'medium'
|
|
323
|
+
notes.push('Has rate limiting but missing authentication')
|
|
324
|
+
} else {
|
|
325
|
+
// No protection at all
|
|
326
|
+
severity = 'high'
|
|
327
|
+
notes.push('Missing authentication and rate limiting')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// BYOK endpoints have lower risk
|
|
331
|
+
if (isByok && severity !== 'info') {
|
|
332
|
+
severity = severity === 'high' ? 'medium' : (severity === 'medium' ? 'low' : severity)
|
|
333
|
+
notes.push('BYOK endpoint - user provides API key')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Downgrade test files
|
|
337
|
+
if (isTestFile) {
|
|
338
|
+
severity = 'info'
|
|
339
|
+
notes.push('in test file')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Downgrade example/demo directories - these are tutorials, not production code
|
|
343
|
+
if (isExample && severity !== 'info') {
|
|
344
|
+
severity = 'info'
|
|
345
|
+
notes.push('in example/demo directory - tutorial code')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Build description
|
|
349
|
+
let description = pattern.description
|
|
350
|
+
if (notes.length > 0) {
|
|
351
|
+
description += ` (${notes.join('; ')})`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Determine suggested fix based on what's missing
|
|
355
|
+
let suggestedFix = pattern.suggestedFix
|
|
356
|
+
if (handlerHasAuth && !handlerHasRateLimit) {
|
|
357
|
+
suggestedFix = 'Add rate limiting to prevent cost abuse: npm install express-rate-limit or use Upstash ratelimit for serverless.'
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
vulnerabilities.push({
|
|
361
|
+
id: `ai-endpoint-${filePath}-${lineNumber}-${pattern.name.replace(/\s+/g, '-')}`,
|
|
362
|
+
filePath,
|
|
363
|
+
lineNumber,
|
|
364
|
+
lineContent,
|
|
365
|
+
severity,
|
|
366
|
+
category: 'ai_endpoint_unprotected',
|
|
367
|
+
title: pattern.name,
|
|
368
|
+
description,
|
|
369
|
+
suggestedFix,
|
|
370
|
+
confidence: severity === 'info' ? 'low' : 'medium',
|
|
371
|
+
layer: 2,
|
|
372
|
+
requiresAIValidation: severity !== 'info',
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// Only report one finding per file (file-level issue)
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Only process if we found a route handler (avoid duplicate patterns)
|
|
380
|
+
if (vulnerabilities.length > 0) break
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return vulnerabilities
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Export helpers for use in other modules
|
|
387
|
+
export { isRouteFile, hasAIApiCalls, hasAuthentication, hasRateLimiting }
|