@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,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2: AI Prompt Hygiene Detection
|
|
3
|
+
* Detects prompt injection vulnerabilities and secrets in LLM prompts
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - B1: Prompt & template hygiene (LLM01)
|
|
7
|
+
* - B3: Secrets & sensitive data in prompts (LLM06)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Vulnerability, VulnerabilitySeverity } from '../types'
|
|
11
|
+
import {
|
|
12
|
+
isComment,
|
|
13
|
+
isTestOrMockFile,
|
|
14
|
+
isDocumentationFile,
|
|
15
|
+
isScannerOrFixtureFile,
|
|
16
|
+
} from '../utils/context-helpers'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a file is in an LLM/AI context based on path and content
|
|
20
|
+
*/
|
|
21
|
+
function isLLMContextFile(filePath: string, content: string): boolean {
|
|
22
|
+
// File path indicators of AI/LLM code
|
|
23
|
+
const llmPathPatterns = [
|
|
24
|
+
/\/(ai|llm|chat|openai|anthropic|gpt|claude)\//i,
|
|
25
|
+
/\/(assistants?|agents?|prompts?)\//i,
|
|
26
|
+
/(chat|ai|llm|prompt|assistant|agent).*\.(ts|js|tsx|jsx|py)$/i,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if (llmPathPatterns.some(p => p.test(filePath))) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Content patterns suggesting LLM API usage
|
|
34
|
+
const llmContentPatterns = [
|
|
35
|
+
/\.create\s*\(\s*\{[^}]*messages\s*:/i, // OpenAI/Anthropic SDK
|
|
36
|
+
/from\s+['"](@anthropic-ai|openai|langchain|llama[-_]?index)/i, // Imports
|
|
37
|
+
/\bsystem\s*:\s*['"`]/i, // System message definition
|
|
38
|
+
/role:\s*['"`](user|assistant|system)['"`]/i, // Message roles
|
|
39
|
+
/\b(systemPrompt|userPrompt|assistantPrompt)\b/i, // Prompt variables
|
|
40
|
+
/messages\s*:\s*\[/i, // Messages array
|
|
41
|
+
/\.chat\.completions?\.create/i, // OpenAI chat completion
|
|
42
|
+
/\.messages\.create/i, // Anthropic messages
|
|
43
|
+
/ChatCompletion|MessageCreate/i, // SDK types
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
return llmContentPatterns.some(p => p.test(content))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if user input delimiter/fence patterns are present
|
|
51
|
+
*/
|
|
52
|
+
function hasPromptDelimiters(lineContent: string, contextLines: string[]): boolean {
|
|
53
|
+
const context = [lineContent, ...contextLines].join('\n')
|
|
54
|
+
|
|
55
|
+
const delimiterPatterns = [
|
|
56
|
+
/```/, // Triple backticks
|
|
57
|
+
/<user>|<\/user>/i, // XML-style user tags
|
|
58
|
+
/<human>|<\/human>/i, // Human tags
|
|
59
|
+
/---+/, // Horizontal rules
|
|
60
|
+
/\[USER\]|\[\/USER\]/i, // Bracket tags
|
|
61
|
+
/\{\{user\}\}/i, // Template variable
|
|
62
|
+
/###\s*User|###\s*Input/i, // Markdown headers
|
|
63
|
+
/INPUT:|OUTPUT:/i, // Section markers
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return delimiterPatterns.some(p => p.test(context))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if content looks like proper parameterization rather than concatenation
|
|
71
|
+
*/
|
|
72
|
+
function isProperlyParameterized(lineContent: string): boolean {
|
|
73
|
+
const safePatterns = [
|
|
74
|
+
/\{\{.*\}\}/, // Handlebars/mustache templates
|
|
75
|
+
/\{[a-zA-Z_]+\}/, // Python format strings (positional)
|
|
76
|
+
/\$\{.*\}.*sanitize|escape/i, // Template with sanitization
|
|
77
|
+
/placeholder|PLACEHOLDER/, // Explicit placeholders
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
return safePatterns.some(p => p.test(lineContent))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Pattern Definitions
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
interface PromptHygienePattern {
|
|
88
|
+
name: string
|
|
89
|
+
pattern: RegExp
|
|
90
|
+
severity: VulnerabilitySeverity
|
|
91
|
+
description: string
|
|
92
|
+
suggestedFix: string
|
|
93
|
+
checkDelimiters?: boolean // If true, downgrade if delimiters found
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* B1: Unsafe prompt interpolation patterns
|
|
98
|
+
*/
|
|
99
|
+
const UNSAFE_INTERPOLATION_PATTERNS: PromptHygienePattern[] = [
|
|
100
|
+
// Template literals with user input in system prompts
|
|
101
|
+
{
|
|
102
|
+
name: 'User input in system prompt',
|
|
103
|
+
pattern: /system\s*[=:]\s*`[^`]*\$\{.*(?:user|input|req|request|body|query|params|data).*\}[^`]*`/gi,
|
|
104
|
+
severity: 'high',
|
|
105
|
+
description: 'User input is directly interpolated into a system prompt. This creates a prompt injection vulnerability where attackers can manipulate the AI\'s behavior.',
|
|
106
|
+
suggestedFix: 'Use clear delimiters (```, <user>, ---) between system instructions and user content. Consider using structured input rather than string interpolation.',
|
|
107
|
+
checkDelimiters: true,
|
|
108
|
+
},
|
|
109
|
+
// String concatenation in prompt building
|
|
110
|
+
{
|
|
111
|
+
name: 'Prompt string concatenation with user input',
|
|
112
|
+
pattern: /(?:system|prompt|instruction)\s*[=+]\s*.*\+\s*(?:user|input|req|request|body|query|params)(?:\.|Input|\[)/gi,
|
|
113
|
+
severity: 'high',
|
|
114
|
+
description: 'User input is concatenated into prompt strings. Attackers can inject malicious instructions.',
|
|
115
|
+
suggestedFix: 'Use delimiters to clearly separate system instructions from user content. Example: ```user input here```',
|
|
116
|
+
checkDelimiters: true,
|
|
117
|
+
},
|
|
118
|
+
// Messages array with dynamic user content in system role
|
|
119
|
+
{
|
|
120
|
+
name: 'Dynamic content in system message',
|
|
121
|
+
pattern: /role:\s*['"`]system['"`]\s*,\s*content:\s*`[^`]*\$\{/gi,
|
|
122
|
+
severity: 'medium',
|
|
123
|
+
description: 'System message content includes dynamic values. If user-controlled, this enables prompt injection.',
|
|
124
|
+
suggestedFix: 'Keep system messages static. Place user input in messages with role: "user" instead.',
|
|
125
|
+
checkDelimiters: true,
|
|
126
|
+
},
|
|
127
|
+
// f-strings in Python with user input
|
|
128
|
+
{
|
|
129
|
+
name: 'Python f-string prompt with user input',
|
|
130
|
+
pattern: /f['"][^'"]*\{.*(?:user|input|request|body).*\}[^'"]*['"]/gi,
|
|
131
|
+
severity: 'high',
|
|
132
|
+
description: 'User input in Python f-string prompt creates prompt injection risk.',
|
|
133
|
+
suggestedFix: 'Use explicit delimiters: f"System instructions...\n---\n{user_input}\n---"',
|
|
134
|
+
checkDelimiters: true,
|
|
135
|
+
},
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* B3: Secrets in prompt context patterns
|
|
140
|
+
*/
|
|
141
|
+
const SECRETS_IN_PROMPTS_PATTERNS: PromptHygienePattern[] = [
|
|
142
|
+
// API keys in message content
|
|
143
|
+
{
|
|
144
|
+
name: 'API key in prompt content',
|
|
145
|
+
pattern: /(?:messages|prompt|system|content)\s*[=:][^;]*(?:sk-[a-zA-Z0-9]{20,}|api[_-]?key\s*[:=]\s*['"][^'"]{16,}['"])/gi,
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
description: 'API key appears to be hardcoded in prompt content. Keys in prompts may be logged, cached, or sent to model providers.',
|
|
148
|
+
suggestedFix: 'Never include API keys in prompts. Use environment variables and keep them server-side only.',
|
|
149
|
+
},
|
|
150
|
+
// AWS keys in prompts
|
|
151
|
+
{
|
|
152
|
+
name: 'AWS credentials in prompt',
|
|
153
|
+
pattern: /(?:messages|prompt|system|content)\s*[=:][^;]*(?:AKIA[A-Z0-9]{16}|aws[_-]?(?:secret|access)[_-]?key)/gi,
|
|
154
|
+
severity: 'critical',
|
|
155
|
+
description: 'AWS credentials detected in prompt content.',
|
|
156
|
+
suggestedFix: 'Remove credentials from prompts. Use IAM roles or environment variables instead.',
|
|
157
|
+
},
|
|
158
|
+
// Database URLs with credentials
|
|
159
|
+
{
|
|
160
|
+
name: 'Database credentials in prompt',
|
|
161
|
+
pattern: /(?:messages|prompt|system|content).*(?:mongodb|postgres|mysql|redis):\/\/[^:]+:[^@]+@/gi,
|
|
162
|
+
severity: 'critical',
|
|
163
|
+
description: 'Database connection string with credentials in prompt. This exposes database access.',
|
|
164
|
+
suggestedFix: 'Never include connection strings in prompts. Reference data by ID instead.',
|
|
165
|
+
},
|
|
166
|
+
// Passwords in prompt context
|
|
167
|
+
{
|
|
168
|
+
name: 'Password in prompt content',
|
|
169
|
+
pattern: /(?:messages|prompt|content)\s*[=:].*(?:password|passwd|pwd)\s*[:=]\s*['"`][^'"`]{8,}/gi,
|
|
170
|
+
severity: 'high',
|
|
171
|
+
description: 'Password appears in prompt content. This may be logged or exposed to model providers.',
|
|
172
|
+
suggestedFix: 'Remove passwords from prompts. Use authentication tokens or session references instead.',
|
|
173
|
+
},
|
|
174
|
+
// Private keys
|
|
175
|
+
{
|
|
176
|
+
name: 'Private key in prompt',
|
|
177
|
+
pattern: /(?:messages|prompt|content).*(?:-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----)/gi,
|
|
178
|
+
severity: 'critical',
|
|
179
|
+
description: 'Private key material detected in prompt context.',
|
|
180
|
+
suggestedFix: 'Never include private keys in prompts. Sign data server-side instead.',
|
|
181
|
+
},
|
|
182
|
+
// Generic token patterns
|
|
183
|
+
{
|
|
184
|
+
name: 'Access token in prompt',
|
|
185
|
+
pattern: /(?:messages|prompt|content)\s*[=:].*(?:access[_-]?token|auth[_-]?token|bearer)\s*[:=]\s*['"`][a-zA-Z0-9_.-]{20,}/gi,
|
|
186
|
+
severity: 'high',
|
|
187
|
+
description: 'Access token detected in prompt content. Tokens in prompts risk exposure.',
|
|
188
|
+
suggestedFix: 'Do not include tokens in prompts. Pass token context through secure server-side channels.',
|
|
189
|
+
},
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Missing boundary patterns - prompts without clear user/system separation
|
|
194
|
+
*/
|
|
195
|
+
const MISSING_BOUNDARY_PATTERNS: PromptHygienePattern[] = [
|
|
196
|
+
// Direct concatenation without any markers
|
|
197
|
+
{
|
|
198
|
+
name: 'Missing prompt boundaries',
|
|
199
|
+
pattern: /(?:content|prompt)\s*[:=]\s*(?:systemInstructions?|instructions?)\s*\+\s*(?:userMessage|userInput|input)/gi,
|
|
200
|
+
severity: 'medium',
|
|
201
|
+
description: 'Prompt concatenates system instructions with user input without clear boundaries.',
|
|
202
|
+
suggestedFix: 'Add delimiters between instructions and user content: "Instructions...\n---\n" + userInput + "\n---"',
|
|
203
|
+
},
|
|
204
|
+
// Template literals building prompts without delimiters
|
|
205
|
+
{
|
|
206
|
+
name: 'Unbounded template prompt',
|
|
207
|
+
pattern: /`(?:You are|As an|Your task)[^`]{20,}\$\{(?!.*(?:```|<user|---|\[USER))/gi,
|
|
208
|
+
severity: 'medium',
|
|
209
|
+
description: 'Prompt template interpolates values without clear delimiter boundaries.',
|
|
210
|
+
suggestedFix: 'Wrap interpolated user content with delimiters: ```${userInput}```',
|
|
211
|
+
},
|
|
212
|
+
// M5: RAG-specific prompt injection patterns
|
|
213
|
+
{
|
|
214
|
+
name: 'Retrieved context in system prompt',
|
|
215
|
+
pattern: /role:\s*['"`]system['"`]\s*,\s*content:\s*`[^`]*\$\{.*(?:context|chunks|documents|retrieved|sources)/gi,
|
|
216
|
+
severity: 'high',
|
|
217
|
+
description: 'Retrieved documents injected into system prompt. Poisoned documents could hijack model behavior.',
|
|
218
|
+
suggestedFix: 'Place retrieved context in user messages with clear delimiters. Use structured prompts separating instructions from data.',
|
|
219
|
+
checkDelimiters: true,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Mixed user input and retrieved context',
|
|
223
|
+
pattern: /\$\{.*(?:userInput|query|question).*\}[^`]*\$\{.*(?:context|chunks|documents).*\}|\$\{.*(?:context|chunks|documents).*\}[^`]*\$\{.*(?:userInput|query|question).*\}/gi,
|
|
224
|
+
severity: 'medium',
|
|
225
|
+
description: 'User input and retrieved context concatenated without clear separation. Both could contain injection attempts.',
|
|
226
|
+
suggestedFix: 'Clearly separate user input from retrieved context using XML tags or delimiters: <user_query>...</user_query><context>...</context>',
|
|
227
|
+
checkDelimiters: true,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'RAG context directly interpolated',
|
|
231
|
+
pattern: /(?:system|prompt)\s*[:=].*(?:retrievedContext|ragContext|documentContext|knowledgeBase)\s*(?:\+|,)/gi,
|
|
232
|
+
severity: 'medium',
|
|
233
|
+
description: 'RAG context directly concatenated into prompt. Could enable data poisoning attacks.',
|
|
234
|
+
suggestedFix: 'Use structured prompt format with clear boundaries between instructions, context, and user input.',
|
|
235
|
+
checkDelimiters: true,
|
|
236
|
+
},
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// Detection Functions
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get surrounding context lines for analysis
|
|
245
|
+
*/
|
|
246
|
+
function getSurroundingContext(content: string, lineIndex: number, windowSize: number = 10): string[] {
|
|
247
|
+
const lines = content.split('\n')
|
|
248
|
+
const start = Math.max(0, lineIndex - windowSize)
|
|
249
|
+
const end = Math.min(lines.length, lineIndex + windowSize)
|
|
250
|
+
return lines.slice(start, end)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Main detection function for AI prompt hygiene issues
|
|
255
|
+
*/
|
|
256
|
+
export function detectAIPromptHygiene(
|
|
257
|
+
content: string,
|
|
258
|
+
filePath: string
|
|
259
|
+
): Vulnerability[] {
|
|
260
|
+
const vulnerabilities: Vulnerability[] = []
|
|
261
|
+
|
|
262
|
+
// Skip non-applicable files
|
|
263
|
+
if (isScannerOrFixtureFile(filePath)) return vulnerabilities
|
|
264
|
+
if (isDocumentationFile(filePath)) return vulnerabilities
|
|
265
|
+
|
|
266
|
+
// Only scan files that appear to be in LLM context
|
|
267
|
+
if (!isLLMContextFile(filePath, content)) {
|
|
268
|
+
return vulnerabilities
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const lines = content.split('\n')
|
|
272
|
+
const isTestFile = isTestOrMockFile(filePath)
|
|
273
|
+
|
|
274
|
+
// Scan for unsafe interpolation patterns (B1)
|
|
275
|
+
for (const pattern of UNSAFE_INTERPOLATION_PATTERNS) {
|
|
276
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags)
|
|
277
|
+
let match
|
|
278
|
+
|
|
279
|
+
while ((match = regex.exec(content)) !== null) {
|
|
280
|
+
const lineNumber = content.substring(0, match.index).split('\n').length
|
|
281
|
+
const lineContent = lines[lineNumber - 1]?.trim() || ''
|
|
282
|
+
|
|
283
|
+
// Skip comments
|
|
284
|
+
if (isComment(lineContent)) continue
|
|
285
|
+
|
|
286
|
+
// Skip if properly parameterized
|
|
287
|
+
if (isProperlyParameterized(lineContent)) continue
|
|
288
|
+
|
|
289
|
+
// Check for delimiters if applicable
|
|
290
|
+
let severity = pattern.severity
|
|
291
|
+
let description = pattern.description
|
|
292
|
+
const contextLines = getSurroundingContext(content, lineNumber - 1, 15)
|
|
293
|
+
|
|
294
|
+
if (pattern.checkDelimiters && hasPromptDelimiters(lineContent, contextLines)) {
|
|
295
|
+
// Delimiters present - downgrade severity
|
|
296
|
+
severity = 'info'
|
|
297
|
+
description += ' (Note: Delimiters detected in context, which mitigates this risk.)'
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Downgrade test files
|
|
301
|
+
if (isTestFile) {
|
|
302
|
+
severity = 'info'
|
|
303
|
+
description += ' (in test file)'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
vulnerabilities.push({
|
|
307
|
+
id: `ai-prompt-${filePath}-${lineNumber}-${pattern.name.replace(/\s+/g, '-')}`,
|
|
308
|
+
filePath,
|
|
309
|
+
lineNumber,
|
|
310
|
+
lineContent,
|
|
311
|
+
severity,
|
|
312
|
+
category: 'ai_prompt_injection',
|
|
313
|
+
title: pattern.name,
|
|
314
|
+
description,
|
|
315
|
+
suggestedFix: pattern.suggestedFix,
|
|
316
|
+
confidence: severity === 'info' ? 'low' : 'medium',
|
|
317
|
+
layer: 2,
|
|
318
|
+
requiresAIValidation: severity !== 'info',
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Scan for secrets in prompts (B3)
|
|
324
|
+
for (const pattern of SECRETS_IN_PROMPTS_PATTERNS) {
|
|
325
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags)
|
|
326
|
+
let match
|
|
327
|
+
|
|
328
|
+
while ((match = regex.exec(content)) !== null) {
|
|
329
|
+
const lineNumber = content.substring(0, match.index).split('\n').length
|
|
330
|
+
const lineContent = lines[lineNumber - 1]?.trim() || ''
|
|
331
|
+
|
|
332
|
+
// Skip comments
|
|
333
|
+
if (isComment(lineContent)) continue
|
|
334
|
+
|
|
335
|
+
// Check if it's an env var reference (safe pattern)
|
|
336
|
+
const isEnvRef = /process\.env|import\.meta\.env|os\.environ|getenv/i.test(lineContent)
|
|
337
|
+
if (isEnvRef) continue
|
|
338
|
+
|
|
339
|
+
let severity = pattern.severity
|
|
340
|
+
let description = pattern.description
|
|
341
|
+
|
|
342
|
+
// Downgrade test files but still flag
|
|
343
|
+
if (isTestFile) {
|
|
344
|
+
severity = severity === 'critical' ? 'medium' : 'low'
|
|
345
|
+
description += ' (in test file - still review for accidental commits)'
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
vulnerabilities.push({
|
|
349
|
+
id: `ai-secret-prompt-${filePath}-${lineNumber}-${pattern.name.replace(/\s+/g, '-')}`,
|
|
350
|
+
filePath,
|
|
351
|
+
lineNumber,
|
|
352
|
+
lineContent,
|
|
353
|
+
severity,
|
|
354
|
+
category: 'hardcoded_secret', // Use existing category for consistency
|
|
355
|
+
title: pattern.name + ' (in LLM context)',
|
|
356
|
+
description: description + ' Secrets in prompts are especially risky as they may be logged, shared, or sent to external AI providers.',
|
|
357
|
+
suggestedFix: pattern.suggestedFix,
|
|
358
|
+
confidence: 'high',
|
|
359
|
+
layer: 2,
|
|
360
|
+
requiresAIValidation: false, // Secrets don't need AI validation - they're definitive
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Scan for missing boundary patterns (B1 continued)
|
|
366
|
+
for (const pattern of MISSING_BOUNDARY_PATTERNS) {
|
|
367
|
+
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags)
|
|
368
|
+
let match
|
|
369
|
+
|
|
370
|
+
while ((match = regex.exec(content)) !== null) {
|
|
371
|
+
const lineNumber = content.substring(0, match.index).split('\n').length
|
|
372
|
+
const lineContent = lines[lineNumber - 1]?.trim() || ''
|
|
373
|
+
|
|
374
|
+
// Skip comments
|
|
375
|
+
if (isComment(lineContent)) continue
|
|
376
|
+
|
|
377
|
+
const contextLines = getSurroundingContext(content, lineNumber - 1, 10)
|
|
378
|
+
|
|
379
|
+
// Skip if delimiters are present
|
|
380
|
+
if (hasPromptDelimiters(lineContent, contextLines)) continue
|
|
381
|
+
|
|
382
|
+
let severity = pattern.severity
|
|
383
|
+
let description = pattern.description
|
|
384
|
+
|
|
385
|
+
if (isTestFile) {
|
|
386
|
+
severity = 'info'
|
|
387
|
+
description += ' (in test file)'
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
vulnerabilities.push({
|
|
391
|
+
id: `ai-boundary-${filePath}-${lineNumber}-${pattern.name.replace(/\s+/g, '-')}`,
|
|
392
|
+
filePath,
|
|
393
|
+
lineNumber,
|
|
394
|
+
lineContent,
|
|
395
|
+
severity,
|
|
396
|
+
category: 'ai_prompt_injection',
|
|
397
|
+
title: pattern.name,
|
|
398
|
+
description,
|
|
399
|
+
suggestedFix: pattern.suggestedFix,
|
|
400
|
+
confidence: 'medium',
|
|
401
|
+
layer: 2,
|
|
402
|
+
requiresAIValidation: true,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return vulnerabilities
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Export helper for use in other modules
|
|
411
|
+
export { isLLMContextFile }
|