@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,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2: AI Schema/Tooling Validation Detection
|
|
3
|
+
* Detects missing or weak validation of AI-generated structured outputs
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - M5.3: Schema/tooling mismatch
|
|
7
|
+
* - Unvalidated JSON parsing of AI outputs
|
|
8
|
+
* - Weak schema patterns (any, permissive)
|
|
9
|
+
* - Tool invocation parameters not validated
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Vulnerability, VulnerabilitySeverity } from '../types'
|
|
13
|
+
import {
|
|
14
|
+
isComment,
|
|
15
|
+
isTestOrMockFile,
|
|
16
|
+
isDocumentationFile,
|
|
17
|
+
isScannerOrFixtureFile,
|
|
18
|
+
} from '../utils/context-helpers'
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Context Detection
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if file is in an AI/LLM context
|
|
26
|
+
*/
|
|
27
|
+
function isAIContextFile(filePath: string, content: string): boolean {
|
|
28
|
+
// File path patterns
|
|
29
|
+
const aiPathPatterns = [
|
|
30
|
+
/\/(ai|llm|chat|openai|anthropic|agents?|tools?)\//i,
|
|
31
|
+
/(ai|llm|chat|agent|tool|function).*\.(ts|js|tsx|jsx|py)$/i,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if (aiPathPatterns.some(p => p.test(filePath))) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Content patterns
|
|
39
|
+
const aiContentPatterns = [
|
|
40
|
+
/openai\.|anthropic\.|ChatCompletion|MessageCreate/i,
|
|
41
|
+
/\.chat\.completions?\.create/i,
|
|
42
|
+
/\.messages\.create/i,
|
|
43
|
+
/generateText|streamText|generateObject/i,
|
|
44
|
+
/tool_calls?|function_call|functionCall/i,
|
|
45
|
+
/from\s+['"](?:openai|@anthropic-ai|langchain)/i,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
return aiContentPatterns.some(p => p.test(content))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if there's schema validation in the context
|
|
53
|
+
*/
|
|
54
|
+
function hasSchemaValidation(context: string): boolean {
|
|
55
|
+
const validationPatterns = [
|
|
56
|
+
// Zod
|
|
57
|
+
/z\.(?:parse|safeParse|parseAsync)\s*\(/i,
|
|
58
|
+
/\.parse\s*\([^)]*response/i,
|
|
59
|
+
/zodSchema|z\.object/i,
|
|
60
|
+
// Ajv
|
|
61
|
+
/ajv\.(?:compile|validate)/i,
|
|
62
|
+
/\.validate\s*\([^)]*schema/i,
|
|
63
|
+
// Joi
|
|
64
|
+
/joi\.(?:object|string|number).*\.validate/i,
|
|
65
|
+
/\.validate\s*\([^)]*joi/i,
|
|
66
|
+
// Yup
|
|
67
|
+
/yup\.(?:object|string|number).*\.validate/i,
|
|
68
|
+
// TypeBox
|
|
69
|
+
/Type\.(?:Object|String|Number)/i,
|
|
70
|
+
/Value\.(?:Check|Decode)/i,
|
|
71
|
+
// Generic validation
|
|
72
|
+
/validateSchema|schemaValidator/i,
|
|
73
|
+
/JSON\.parse.*try.*catch.*schema/i,
|
|
74
|
+
// OpenAI Structured Outputs
|
|
75
|
+
/response_format.*json_schema/i,
|
|
76
|
+
/json_schema\s*:/i,
|
|
77
|
+
]
|
|
78
|
+
return validationPatterns.some(p => p.test(context))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if there's a security-sensitive sink after parsing
|
|
83
|
+
*/
|
|
84
|
+
function hasSecuritySink(context: string): boolean {
|
|
85
|
+
const sinkPatterns = [
|
|
86
|
+
// Execution sinks
|
|
87
|
+
/eval\s*\(/i,
|
|
88
|
+
/Function\s*\(/i,
|
|
89
|
+
/exec\s*\(|spawn\s*\(/i,
|
|
90
|
+
/child_process/i,
|
|
91
|
+
// Database sinks
|
|
92
|
+
/\.query\s*\(/i,
|
|
93
|
+
/\.execute\s*\(/i,
|
|
94
|
+
/\.raw\s*\(/i,
|
|
95
|
+
/\$\{.*\}.*(?:SELECT|INSERT|UPDATE|DELETE)/i,
|
|
96
|
+
// File system sinks
|
|
97
|
+
/fs\.(?:readFile|writeFile|unlink|mkdir)/i,
|
|
98
|
+
/readFileSync|writeFileSync/i,
|
|
99
|
+
// Network sinks
|
|
100
|
+
/fetch\s*\(|axios\.|request\s*\(/i,
|
|
101
|
+
// DOM sinks (if applicable)
|
|
102
|
+
/innerHTML|dangerouslySetInnerHTML/i,
|
|
103
|
+
]
|
|
104
|
+
return sinkPatterns.some(p => p.test(context))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if there's try-catch around the parsing
|
|
109
|
+
*/
|
|
110
|
+
function hasTryCatch(content: string, lineIndex: number): boolean {
|
|
111
|
+
const lines = content.split('\n')
|
|
112
|
+
const start = Math.max(0, lineIndex - 15)
|
|
113
|
+
const end = Math.min(lines.length, lineIndex + 5)
|
|
114
|
+
const context = lines.slice(start, end).join('\n')
|
|
115
|
+
|
|
116
|
+
// Simple check for try block wrapping
|
|
117
|
+
const tryPattern = /try\s*\{[^}]*$/
|
|
118
|
+
const beforeContext = lines.slice(start, lineIndex).join('\n')
|
|
119
|
+
return tryPattern.test(beforeContext)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get surrounding context
|
|
124
|
+
*/
|
|
125
|
+
function getSurroundingContext(content: string, lineIndex: number, windowSize: number = 20): string {
|
|
126
|
+
const lines = content.split('\n')
|
|
127
|
+
const start = Math.max(0, lineIndex - windowSize)
|
|
128
|
+
const end = Math.min(lines.length, lineIndex + windowSize)
|
|
129
|
+
return lines.slice(start, end).join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Pattern Definitions
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
interface SchemaValidationPattern {
|
|
137
|
+
name: string
|
|
138
|
+
pattern: RegExp
|
|
139
|
+
riskType: 'unvalidated_parse' | 'weak_schema' | 'tool_params'
|
|
140
|
+
baseSeverity: VulnerabilitySeverity
|
|
141
|
+
description: string
|
|
142
|
+
suggestedFix: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Unvalidated AI output parsing patterns
|
|
147
|
+
*/
|
|
148
|
+
const UNVALIDATED_PARSING_PATTERNS: SchemaValidationPattern[] = [
|
|
149
|
+
// JSON.parse on AI response content
|
|
150
|
+
{
|
|
151
|
+
name: 'AI response content parsed without validation',
|
|
152
|
+
pattern: /JSON\.parse\s*\(\s*(?:response|result|completion|output|message)(?:\.\w+)*(?:\.content|\.text|\.message)?/gi,
|
|
153
|
+
riskType: 'unvalidated_parse',
|
|
154
|
+
baseSeverity: 'medium',
|
|
155
|
+
description: 'AI-generated JSON parsed without schema validation. Model may return malformed or malicious structures.',
|
|
156
|
+
suggestedFix: 'Validate with schema: const data = schema.parse(JSON.parse(response.content)) // using zod',
|
|
157
|
+
},
|
|
158
|
+
// OpenAI choices parsing
|
|
159
|
+
{
|
|
160
|
+
name: 'OpenAI response parsed directly',
|
|
161
|
+
pattern: /JSON\.parse\s*\(\s*(?:\w+\.)?choices\[\d+\]\.message\.content/gi,
|
|
162
|
+
riskType: 'unvalidated_parse',
|
|
163
|
+
baseSeverity: 'medium',
|
|
164
|
+
description: 'OpenAI response content parsed without validation. AI output structure should be verified.',
|
|
165
|
+
suggestedFix: 'Use OpenAI Structured Outputs (response_format: { type: "json_schema", json_schema: {...} }) or validate with zod.',
|
|
166
|
+
},
|
|
167
|
+
// Anthropic content parsing
|
|
168
|
+
{
|
|
169
|
+
name: 'Anthropic response parsed directly',
|
|
170
|
+
pattern: /JSON\.parse\s*\(\s*(?:\w+\.)?content\[0\]\.text/gi,
|
|
171
|
+
riskType: 'unvalidated_parse',
|
|
172
|
+
baseSeverity: 'medium',
|
|
173
|
+
description: 'Anthropic response parsed without validation. AI output may not match expected schema.',
|
|
174
|
+
suggestedFix: 'Validate response structure with zod or similar schema validation library.',
|
|
175
|
+
},
|
|
176
|
+
// Tool call arguments parsing
|
|
177
|
+
{
|
|
178
|
+
name: 'Tool call arguments parsed without validation',
|
|
179
|
+
pattern: /JSON\.parse\s*\(\s*(?:\w+\.)?(?:tool_calls?|function_call)(?:\[\d+\])?\.(?:function\.)?arguments/gi,
|
|
180
|
+
riskType: 'unvalidated_parse',
|
|
181
|
+
baseSeverity: 'medium',
|
|
182
|
+
description: 'Function calling arguments parsed without schema validation. Tool parameters should be verified.',
|
|
183
|
+
suggestedFix: 'Define and validate tool argument schema: const args = toolSchema.parse(JSON.parse(toolCall.function.arguments))',
|
|
184
|
+
},
|
|
185
|
+
// Generic AI output parsing
|
|
186
|
+
{
|
|
187
|
+
name: 'AI output assigned without parsing/validation',
|
|
188
|
+
pattern: /(?:const|let|var)\s+\w+\s*=\s*(?:response|completion|result)\.(?:content|text|data)\s*;?\s*(?!.*(?:parse|validate|schema))/gi,
|
|
189
|
+
riskType: 'unvalidated_parse',
|
|
190
|
+
baseSeverity: 'low',
|
|
191
|
+
description: 'AI output assigned directly to variable. If used as structured data, validate first.',
|
|
192
|
+
suggestedFix: 'If expecting JSON: const data = schema.safeParse(JSON.parse(response.content))',
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Weak schema patterns
|
|
198
|
+
*/
|
|
199
|
+
const WEAK_SCHEMA_PATTERNS: SchemaValidationPattern[] = [
|
|
200
|
+
// any type for AI response
|
|
201
|
+
{
|
|
202
|
+
name: 'AI response typed as any',
|
|
203
|
+
pattern: /(?:response|aiResponse|completion|result)\s*:\s*any\b/gi,
|
|
204
|
+
riskType: 'weak_schema',
|
|
205
|
+
baseSeverity: 'low',
|
|
206
|
+
description: 'AI response typed as `any`. TypeScript cannot catch type errors at compile time.',
|
|
207
|
+
suggestedFix: 'Define explicit interface and use runtime validation: interface AIResponse { ... }; const data: AIResponse = schema.parse(response)',
|
|
208
|
+
},
|
|
209
|
+
// Permissive zod schemas
|
|
210
|
+
{
|
|
211
|
+
name: 'Permissive zod schema for AI output',
|
|
212
|
+
pattern: /z\.(?:any|unknown)\s*\(\s*\).*(?:response|output|completion)/gi,
|
|
213
|
+
riskType: 'weak_schema',
|
|
214
|
+
baseSeverity: 'low',
|
|
215
|
+
description: 'Using z.any() or z.unknown() defeats the purpose of schema validation.',
|
|
216
|
+
suggestedFix: 'Define specific schema: z.object({ field: z.string(), ... }).strict()',
|
|
217
|
+
},
|
|
218
|
+
// Passthrough schemas
|
|
219
|
+
{
|
|
220
|
+
name: 'Passthrough schema allowing extra properties',
|
|
221
|
+
pattern: /z\.object\s*\([^)]+\)\.passthrough\s*\(\s*\)/gi,
|
|
222
|
+
riskType: 'weak_schema',
|
|
223
|
+
baseSeverity: 'info',
|
|
224
|
+
description: 'Schema uses passthrough() allowing unexpected properties from AI output.',
|
|
225
|
+
suggestedFix: 'Use .strict() instead to reject unexpected properties from AI responses.',
|
|
226
|
+
},
|
|
227
|
+
// Record<string, any>
|
|
228
|
+
{
|
|
229
|
+
name: 'Record<string, any> for AI data',
|
|
230
|
+
pattern: /Record<string,\s*any>\s*.*(?:response|result|output|completion|aiData)/gi,
|
|
231
|
+
riskType: 'weak_schema',
|
|
232
|
+
baseSeverity: 'low',
|
|
233
|
+
description: 'Generic Record<string, any> type used for AI output provides no type safety.',
|
|
234
|
+
suggestedFix: 'Define specific interface or use zod schema for runtime validation.',
|
|
235
|
+
},
|
|
236
|
+
// Object type without specifics
|
|
237
|
+
{
|
|
238
|
+
name: 'Generic object type for AI response',
|
|
239
|
+
pattern: /(?:response|completion|aiOutput)\s*:\s*(?:object|Object|{})\b/gi,
|
|
240
|
+
riskType: 'weak_schema',
|
|
241
|
+
baseSeverity: 'info',
|
|
242
|
+
description: 'Generic object type for AI response. Consider defining specific interface.',
|
|
243
|
+
suggestedFix: 'Replace with specific interface that documents expected AI output structure.',
|
|
244
|
+
},
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Tool parameter validation issues
|
|
249
|
+
*/
|
|
250
|
+
const TOOL_PARAM_PATTERNS: SchemaValidationPattern[] = [
|
|
251
|
+
// Tool parameters used directly in paths
|
|
252
|
+
{
|
|
253
|
+
name: 'Tool parameter used in file path',
|
|
254
|
+
pattern: /(?:tool|args|parameters)(?:\.\w+)*\.(?:path|filename|filepath|file).*(?:readFile|writeFile|fs\.|path\.)/gi,
|
|
255
|
+
riskType: 'tool_params',
|
|
256
|
+
baseSeverity: 'high',
|
|
257
|
+
description: 'AI-generated file path used without validation. Path traversal attack possible.',
|
|
258
|
+
suggestedFix: 'Validate path against allowlist: if (!allowedPaths.some(p => resolvedPath.startsWith(p))) throw new Error("Invalid path")',
|
|
259
|
+
},
|
|
260
|
+
// Tool parameters in commands
|
|
261
|
+
{
|
|
262
|
+
name: 'Tool parameter used in shell command',
|
|
263
|
+
pattern: /(?:tool|args|parameters)(?:\.\w+)*\.(?:command|cmd|script).*(?:exec|spawn|shell)/gi,
|
|
264
|
+
riskType: 'tool_params',
|
|
265
|
+
baseSeverity: 'critical',
|
|
266
|
+
description: 'AI-generated command executed without validation. Command injection possible.',
|
|
267
|
+
suggestedFix: 'Use allowlist for permitted commands. Never execute arbitrary AI-generated commands.',
|
|
268
|
+
},
|
|
269
|
+
// Tool parameters in URLs
|
|
270
|
+
{
|
|
271
|
+
name: 'Tool parameter used in URL',
|
|
272
|
+
pattern: /(?:tool|args|parameters)(?:\.\w+)*\.(?:url|endpoint|host).*(?:fetch|axios|request|http)/gi,
|
|
273
|
+
riskType: 'tool_params',
|
|
274
|
+
baseSeverity: 'high',
|
|
275
|
+
description: 'AI-generated URL used without validation. SSRF attack possible.',
|
|
276
|
+
suggestedFix: 'Validate URL against allowlist of permitted hosts before making request.',
|
|
277
|
+
},
|
|
278
|
+
// Tool parameters in database queries
|
|
279
|
+
{
|
|
280
|
+
name: 'Tool parameter used in database query',
|
|
281
|
+
pattern: /(?:tool|args|parameters)(?:\.\w+)*\.(?:query|sql|table|column).*(?:\.query|\.execute|\.raw)/gi,
|
|
282
|
+
riskType: 'tool_params',
|
|
283
|
+
baseSeverity: 'high',
|
|
284
|
+
description: 'AI-generated value used in database query. SQL injection possible.',
|
|
285
|
+
suggestedFix: 'Use parameterized queries. Validate table/column names against allowlist.',
|
|
286
|
+
},
|
|
287
|
+
// Function call name used in switch/if
|
|
288
|
+
{
|
|
289
|
+
name: 'Tool/function call routed without validation',
|
|
290
|
+
pattern: /(?:tool_call|function_call|functionCall)\.(?:name|function\.name).*(?:switch|if|\[.*\])/gi,
|
|
291
|
+
riskType: 'tool_params',
|
|
292
|
+
baseSeverity: 'medium',
|
|
293
|
+
description: 'AI-selected function name used for routing. Validate against allowed functions.',
|
|
294
|
+
suggestedFix: 'Check function name against allowlist: const allowedFunctions = ["search", "calculate"]; if (!allowedFunctions.includes(name)) throw',
|
|
295
|
+
},
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Main Detection Function
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Main detection function for AI schema/tooling validation issues
|
|
304
|
+
*/
|
|
305
|
+
export function detectAISchemaValidation(
|
|
306
|
+
content: string,
|
|
307
|
+
filePath: string
|
|
308
|
+
): Vulnerability[] {
|
|
309
|
+
const vulnerabilities: Vulnerability[] = []
|
|
310
|
+
|
|
311
|
+
// Skip non-applicable files
|
|
312
|
+
if (isScannerOrFixtureFile(filePath)) return vulnerabilities
|
|
313
|
+
if (isDocumentationFile(filePath)) return vulnerabilities
|
|
314
|
+
|
|
315
|
+
// Only scan files in AI context
|
|
316
|
+
if (!isAIContextFile(filePath, content)) {
|
|
317
|
+
return vulnerabilities
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const lines = content.split('\n')
|
|
321
|
+
const isTestFile = isTestOrMockFile(filePath)
|
|
322
|
+
|
|
323
|
+
// Check file-level validation presence
|
|
324
|
+
const fileHasValidation = hasSchemaValidation(content)
|
|
325
|
+
|
|
326
|
+
// Process all pattern categories
|
|
327
|
+
const allPatterns: SchemaValidationPattern[] = [
|
|
328
|
+
...UNVALIDATED_PARSING_PATTERNS,
|
|
329
|
+
...WEAK_SCHEMA_PATTERNS,
|
|
330
|
+
...TOOL_PARAM_PATTERNS,
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
for (const pattern of allPatterns) {
|
|
334
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags)
|
|
335
|
+
let match
|
|
336
|
+
|
|
337
|
+
while ((match = regex.exec(content)) !== null) {
|
|
338
|
+
const lineNumber = content.substring(0, match.index).split('\n').length
|
|
339
|
+
const lineContent = lines[lineNumber - 1]?.trim() || ''
|
|
340
|
+
|
|
341
|
+
// Skip comments
|
|
342
|
+
if (isComment(lineContent)) continue
|
|
343
|
+
|
|
344
|
+
// Get surrounding context
|
|
345
|
+
const context = getSurroundingContext(content, lineNumber - 1, 20)
|
|
346
|
+
|
|
347
|
+
// Check for local validation
|
|
348
|
+
const localHasValidation = hasSchemaValidation(context)
|
|
349
|
+
const localHasTryCatch = hasTryCatch(content, lineNumber - 1)
|
|
350
|
+
const localHasSecuritySink = hasSecuritySink(context)
|
|
351
|
+
|
|
352
|
+
// Calculate severity based on context
|
|
353
|
+
let severity = pattern.baseSeverity
|
|
354
|
+
const notes: string[] = []
|
|
355
|
+
|
|
356
|
+
// Apply context-aware adjustments
|
|
357
|
+
if (pattern.riskType === 'unvalidated_parse') {
|
|
358
|
+
if (localHasValidation || fileHasValidation) {
|
|
359
|
+
severity = 'info'
|
|
360
|
+
notes.push('Schema validation detected nearby')
|
|
361
|
+
} else if (localHasSecuritySink) {
|
|
362
|
+
// Elevate if used in security-sensitive context
|
|
363
|
+
if (severity === 'medium') severity = 'high'
|
|
364
|
+
notes.push('Used in security-sensitive context')
|
|
365
|
+
} else if (localHasTryCatch && !localHasSecuritySink) {
|
|
366
|
+
// Try-catch without sink is minor
|
|
367
|
+
severity = 'low'
|
|
368
|
+
notes.push('Has try-catch but no schema validation')
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (pattern.riskType === 'weak_schema') {
|
|
373
|
+
// Weak schemas at API boundaries are more concerning
|
|
374
|
+
if (/export|handler|route|api/i.test(context)) {
|
|
375
|
+
if (severity === 'low') severity = 'medium'
|
|
376
|
+
notes.push('At API boundary')
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (pattern.riskType === 'tool_params') {
|
|
381
|
+
// Already high/critical, check for mitigation
|
|
382
|
+
if (localHasValidation) {
|
|
383
|
+
severity = severity === 'critical' ? 'medium' : 'low'
|
|
384
|
+
notes.push('Validation detected')
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Downgrade test files
|
|
389
|
+
if (isTestFile) {
|
|
390
|
+
severity = 'info'
|
|
391
|
+
notes.push('in test file')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Build final description
|
|
395
|
+
let description = pattern.description
|
|
396
|
+
if (notes.length > 0) {
|
|
397
|
+
description += ` (${notes.join('; ')})`
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
vulnerabilities.push({
|
|
401
|
+
id: `ai-schema-${filePath}-${lineNumber}-${pattern.name.replace(/\s+/g, '-')}`,
|
|
402
|
+
filePath,
|
|
403
|
+
lineNumber,
|
|
404
|
+
lineContent,
|
|
405
|
+
severity,
|
|
406
|
+
category: 'ai_schema_mismatch',
|
|
407
|
+
title: pattern.name,
|
|
408
|
+
description,
|
|
409
|
+
suggestedFix: pattern.suggestedFix,
|
|
410
|
+
confidence: severity === 'info' ? 'low' : 'medium',
|
|
411
|
+
layer: 2,
|
|
412
|
+
requiresAIValidation: true, // Tier B - always validate with AI
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return vulnerabilities
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Export helpers for use in other modules
|
|
421
|
+
export { isAIContextFile, hasSchemaValidation }
|