muaddib-scanner 2.4.3 → 2.4.5
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/LICENSE +20 -20
- package/README.md +15 -1
- package/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -3
- package/src/canary-tokens.js +184 -184
- package/src/ioc/bootstrap.js +181 -181
- package/src/ioc/yaml-loader.js +223 -223
- package/src/maintainer-change.js +224 -224
- package/src/output-formatter.js +192 -192
- package/src/publish-anomaly.js +206 -206
- package/src/report.js +230 -230
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +40 -17
- package/src/scanner/ast.js +1 -0
- package/src/scanner/dataflow.js +14 -2
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/entropy.js +7 -0
- package/src/scanner/hash.js +118 -118
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +3 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
package/src/scanner/ai-config.js
CHANGED
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI Config Injection Scanner
|
|
3
|
-
*
|
|
4
|
-
* Detects prompt injection attacks hidden in AI agent configuration files:
|
|
5
|
-
* .cursorrules, CLAUDE.md, AGENT.md, .github/copilot-instructions.md,
|
|
6
|
-
* copilot-setup-steps.yml, .cursorignore, .windsurfrules, etc.
|
|
7
|
-
*
|
|
8
|
-
* These files are designed to be read by AI coding assistants and may contain
|
|
9
|
-
* hidden instructions to execute shell commands, exfiltrate secrets, or
|
|
10
|
-
* download and run remote payloads.
|
|
11
|
-
*
|
|
12
|
-
* References:
|
|
13
|
-
* - ToxicSkills (Snyk, Feb 2026)
|
|
14
|
-
* - NVIDIA AI agent security guidance
|
|
15
|
-
* - arxiv 2601.17548 (prompt injection in AI agents)
|
|
16
|
-
* - Clinejection (Snyk, Feb 2026)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const fs = require('fs');
|
|
20
|
-
const path = require('path');
|
|
21
|
-
|
|
22
|
-
// AI agent config files to scan (relative to project root)
|
|
23
|
-
const AI_CONFIG_FILES = [
|
|
24
|
-
'.cursorrules',
|
|
25
|
-
'.cursorignore',
|
|
26
|
-
'.windsurfrules',
|
|
27
|
-
'CLAUDE.md',
|
|
28
|
-
'AGENT.md',
|
|
29
|
-
'.github/copilot-instructions.md',
|
|
30
|
-
'copilot-setup-steps.yml',
|
|
31
|
-
'.github/copilot-setup-steps.yml'
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
// Dangerous shell command patterns in AI config files
|
|
35
|
-
const SHELL_COMMAND_PATTERNS = [
|
|
36
|
-
// Download and execute
|
|
37
|
-
{ regex: /curl\s+[^\n]*\|\s*(sh|bash|zsh)\b/i, label: 'curl pipe to shell', critical: true },
|
|
38
|
-
{ regex: /wget\s+[^\n]*\|\s*(sh|bash|zsh)\b/i, label: 'wget pipe to shell', critical: true },
|
|
39
|
-
{ regex: /curl\s+-[sS]*L?\s+https?:\/\/[^\s"']+\s*\|\s*(sh|bash)/i, label: 'curl download and execute', critical: true },
|
|
40
|
-
{ regex: /wget\s+-[qQ]*O?-?\s+https?:\/\/[^\s"']+\s*\|\s*(sh|bash)/i, label: 'wget download and execute', critical: true },
|
|
41
|
-
|
|
42
|
-
// Direct shell execution
|
|
43
|
-
{ regex: /\beval\s*\(/i, label: 'eval() call', critical: false },
|
|
44
|
-
{ regex: /\bexec\s*\(/i, label: 'exec() call', critical: false },
|
|
45
|
-
{ regex: /\bsource\s+\.env\b/i, label: 'source .env', critical: false },
|
|
46
|
-
{ regex: /\bsh\s+-c\s+["']/i, label: 'sh -c execution', critical: false },
|
|
47
|
-
{ regex: /\bbash\s+-c\s+["']/i, label: 'bash -c execution', critical: false },
|
|
48
|
-
{ regex: /\bnode\s+-e\s+["']/i, label: 'node -e inline execution', critical: false },
|
|
49
|
-
{ regex: /\bpython[3]?\s+-c\s+["']/i, label: 'python -c inline execution', critical: false }
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
// Exfiltration patterns — sending data to external endpoints
|
|
53
|
-
const EXFIL_PATTERNS = [
|
|
54
|
-
{ regex: /curl\s+[^\n]*-X\s*POST\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)[^\s"']+/i, label: 'curl POST to external endpoint' },
|
|
55
|
-
{ regex: /curl\s+[^\n]*-d\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)[^\s"']+/i, label: 'curl data upload to external endpoint' },
|
|
56
|
-
{ regex: /curl\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)[^\s"']+[^\n]*-d\s/i, label: 'curl data upload to external endpoint' },
|
|
57
|
-
{ regex: /\|\s*curl\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)/i, label: 'pipe output to curl' },
|
|
58
|
-
{ regex: /\|\s*base64\s*\|\s*curl/i, label: 'base64 encode and send via curl' }
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
// Credential access patterns — reading sensitive files/vars
|
|
62
|
-
const CREDENTIAL_ACCESS_PATTERNS = [
|
|
63
|
-
{ regex: /cat\s+~?\/?\.ssh\/id_/i, label: 'read SSH private key' },
|
|
64
|
-
{ regex: /cat\s+~?\/?\.npmrc/i, label: 'read .npmrc tokens' },
|
|
65
|
-
{ regex: /cat\s+~?\/?\.aws\/credentials/i, label: 'read AWS credentials' },
|
|
66
|
-
{ regex: /cat\s+~?\/?\.env\b/i, label: 'read .env file' },
|
|
67
|
-
{ regex: /cat\s+~?\/?\.gnupg\//i, label: 'read GPG keys' },
|
|
68
|
-
{ regex: /\$GITHUB_TOKEN|\$GH_TOKEN|\$NPM_TOKEN|\$AWS_SECRET_ACCESS_KEY|\$DISCORD_TOKEN/i, label: 'reference to secret env var' },
|
|
69
|
-
{ regex: /grep\s+[^\n]*(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)[^\n]*/i, label: 'grep for secrets' },
|
|
70
|
-
{ regex: /env\s*\|\s*grep\s+[^\n]*(TOKEN|KEY|SECRET|PASSWORD)/i, label: 'env grep for secrets' }
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
// Instruction patterns — AI prompt injection directives
|
|
74
|
-
const INJECTION_INSTRUCTION_PATTERNS = [
|
|
75
|
-
{ regex: /before\s+(reviewing|running|any|code|generating)[^\n]*(run|execute|source):/i, label: 'instruction to execute before review' },
|
|
76
|
-
{ regex: /always\s+run\s+[^\n]*(before|first|initially)/i, label: 'instruction to always run command' },
|
|
77
|
-
{ regex: /send\s+(contents?|data|output|results?)\s+(to|via)\s+https?:\/\//i, label: 'instruction to send data to URL' },
|
|
78
|
-
{ regex: /upload\s+[^\n]*(to|via)\s+https?:\/\//i, label: 'instruction to upload to URL' },
|
|
79
|
-
{ regex: /do\s+not\s+(display|show|output|mention|tell)/i, label: 'instruction to hide activity' }
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Scan AI config files for prompt injection
|
|
84
|
-
*/
|
|
85
|
-
function scanAIConfig(targetPath) {
|
|
86
|
-
const threats = [];
|
|
87
|
-
|
|
88
|
-
for (const configFile of AI_CONFIG_FILES) {
|
|
89
|
-
const filePath = path.join(targetPath, configFile);
|
|
90
|
-
|
|
91
|
-
if (!fs.existsSync(filePath)) continue;
|
|
92
|
-
|
|
93
|
-
let content;
|
|
94
|
-
try {
|
|
95
|
-
const stat = fs.statSync(filePath);
|
|
96
|
-
if (stat.size > 1024 * 1024) continue; // Skip files > 1MB
|
|
97
|
-
content = fs.readFileSync(filePath, 'utf8');
|
|
98
|
-
} catch {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const relPath = configFile;
|
|
103
|
-
const fileThreats = analyzeAIConfigFile(content, relPath);
|
|
104
|
-
threats.push(...fileThreats);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return threats;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Analyze a single AI config file for prompt injection patterns
|
|
112
|
-
*/
|
|
113
|
-
function analyzeAIConfigFile(content, relPath) {
|
|
114
|
-
const threats = [];
|
|
115
|
-
let hasShellCommand = false;
|
|
116
|
-
let hasExfiltration = false;
|
|
117
|
-
let hasCredentialAccess = false;
|
|
118
|
-
|
|
119
|
-
// Check shell command patterns
|
|
120
|
-
for (const pattern of SHELL_COMMAND_PATTERNS) {
|
|
121
|
-
if (pattern.regex.test(content)) {
|
|
122
|
-
hasShellCommand = true;
|
|
123
|
-
threats.push({
|
|
124
|
-
type: pattern.critical ? 'ai_config_injection_critical' : 'ai_config_injection',
|
|
125
|
-
severity: pattern.critical ? 'CRITICAL' : 'HIGH',
|
|
126
|
-
message: `AI config prompt injection: ${pattern.label} in ${relPath}`,
|
|
127
|
-
file: relPath
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check exfiltration patterns
|
|
133
|
-
for (const pattern of EXFIL_PATTERNS) {
|
|
134
|
-
if (pattern.regex.test(content)) {
|
|
135
|
-
hasExfiltration = true;
|
|
136
|
-
threats.push({
|
|
137
|
-
type: 'ai_config_injection_critical',
|
|
138
|
-
severity: 'CRITICAL',
|
|
139
|
-
message: `AI config exfiltration: ${pattern.label} in ${relPath}`,
|
|
140
|
-
file: relPath
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Check credential access patterns
|
|
146
|
-
for (const pattern of CREDENTIAL_ACCESS_PATTERNS) {
|
|
147
|
-
if (pattern.regex.test(content)) {
|
|
148
|
-
hasCredentialAccess = true;
|
|
149
|
-
threats.push({
|
|
150
|
-
type: 'ai_config_injection',
|
|
151
|
-
severity: 'HIGH',
|
|
152
|
-
message: `AI config credential access: ${pattern.label} in ${relPath}`,
|
|
153
|
-
file: relPath
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Check injection instruction patterns
|
|
159
|
-
for (const pattern of INJECTION_INSTRUCTION_PATTERNS) {
|
|
160
|
-
if (pattern.regex.test(content)) {
|
|
161
|
-
threats.push({
|
|
162
|
-
type: 'ai_config_injection',
|
|
163
|
-
severity: 'HIGH',
|
|
164
|
-
message: `AI config prompt injection: ${pattern.label} in ${relPath}`,
|
|
165
|
-
file: relPath
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Compound detection: shell + exfil or credential access → escalate
|
|
171
|
-
if (hasShellCommand && (hasExfiltration || hasCredentialAccess)) {
|
|
172
|
-
threats.push({
|
|
173
|
-
type: 'ai_config_injection_critical',
|
|
174
|
-
severity: 'CRITICAL',
|
|
175
|
-
message: `AI config compound attack: shell commands + ${hasExfiltration ? 'exfiltration' : 'credential access'} in ${relPath} — ToxicSkills/Clinejection pattern.`,
|
|
176
|
-
file: relPath
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return threats;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
module.exports = { scanAIConfig };
|
|
1
|
+
/**
|
|
2
|
+
* AI Config Injection Scanner
|
|
3
|
+
*
|
|
4
|
+
* Detects prompt injection attacks hidden in AI agent configuration files:
|
|
5
|
+
* .cursorrules, CLAUDE.md, AGENT.md, .github/copilot-instructions.md,
|
|
6
|
+
* copilot-setup-steps.yml, .cursorignore, .windsurfrules, etc.
|
|
7
|
+
*
|
|
8
|
+
* These files are designed to be read by AI coding assistants and may contain
|
|
9
|
+
* hidden instructions to execute shell commands, exfiltrate secrets, or
|
|
10
|
+
* download and run remote payloads.
|
|
11
|
+
*
|
|
12
|
+
* References:
|
|
13
|
+
* - ToxicSkills (Snyk, Feb 2026)
|
|
14
|
+
* - NVIDIA AI agent security guidance
|
|
15
|
+
* - arxiv 2601.17548 (prompt injection in AI agents)
|
|
16
|
+
* - Clinejection (Snyk, Feb 2026)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// AI agent config files to scan (relative to project root)
|
|
23
|
+
const AI_CONFIG_FILES = [
|
|
24
|
+
'.cursorrules',
|
|
25
|
+
'.cursorignore',
|
|
26
|
+
'.windsurfrules',
|
|
27
|
+
'CLAUDE.md',
|
|
28
|
+
'AGENT.md',
|
|
29
|
+
'.github/copilot-instructions.md',
|
|
30
|
+
'copilot-setup-steps.yml',
|
|
31
|
+
'.github/copilot-setup-steps.yml'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Dangerous shell command patterns in AI config files
|
|
35
|
+
const SHELL_COMMAND_PATTERNS = [
|
|
36
|
+
// Download and execute
|
|
37
|
+
{ regex: /curl\s+[^\n]*\|\s*(sh|bash|zsh)\b/i, label: 'curl pipe to shell', critical: true },
|
|
38
|
+
{ regex: /wget\s+[^\n]*\|\s*(sh|bash|zsh)\b/i, label: 'wget pipe to shell', critical: true },
|
|
39
|
+
{ regex: /curl\s+-[sS]*L?\s+https?:\/\/[^\s"']+\s*\|\s*(sh|bash)/i, label: 'curl download and execute', critical: true },
|
|
40
|
+
{ regex: /wget\s+-[qQ]*O?-?\s+https?:\/\/[^\s"']+\s*\|\s*(sh|bash)/i, label: 'wget download and execute', critical: true },
|
|
41
|
+
|
|
42
|
+
// Direct shell execution
|
|
43
|
+
{ regex: /\beval\s*\(/i, label: 'eval() call', critical: false },
|
|
44
|
+
{ regex: /\bexec\s*\(/i, label: 'exec() call', critical: false },
|
|
45
|
+
{ regex: /\bsource\s+\.env\b/i, label: 'source .env', critical: false },
|
|
46
|
+
{ regex: /\bsh\s+-c\s+["']/i, label: 'sh -c execution', critical: false },
|
|
47
|
+
{ regex: /\bbash\s+-c\s+["']/i, label: 'bash -c execution', critical: false },
|
|
48
|
+
{ regex: /\bnode\s+-e\s+["']/i, label: 'node -e inline execution', critical: false },
|
|
49
|
+
{ regex: /\bpython[3]?\s+-c\s+["']/i, label: 'python -c inline execution', critical: false }
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Exfiltration patterns — sending data to external endpoints
|
|
53
|
+
const EXFIL_PATTERNS = [
|
|
54
|
+
{ regex: /curl\s+[^\n]*-X\s*POST\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)[^\s"']+/i, label: 'curl POST to external endpoint' },
|
|
55
|
+
{ regex: /curl\s+[^\n]*-d\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)[^\s"']+/i, label: 'curl data upload to external endpoint' },
|
|
56
|
+
{ regex: /curl\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)[^\s"']+[^\n]*-d\s/i, label: 'curl data upload to external endpoint' },
|
|
57
|
+
{ regex: /\|\s*curl\s+[^\n]*https?:\/\/(?!api\.github\.com|registry\.npmjs\.org)/i, label: 'pipe output to curl' },
|
|
58
|
+
{ regex: /\|\s*base64\s*\|\s*curl/i, label: 'base64 encode and send via curl' }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Credential access patterns — reading sensitive files/vars
|
|
62
|
+
const CREDENTIAL_ACCESS_PATTERNS = [
|
|
63
|
+
{ regex: /cat\s+~?\/?\.ssh\/id_/i, label: 'read SSH private key' },
|
|
64
|
+
{ regex: /cat\s+~?\/?\.npmrc/i, label: 'read .npmrc tokens' },
|
|
65
|
+
{ regex: /cat\s+~?\/?\.aws\/credentials/i, label: 'read AWS credentials' },
|
|
66
|
+
{ regex: /cat\s+~?\/?\.env\b/i, label: 'read .env file' },
|
|
67
|
+
{ regex: /cat\s+~?\/?\.gnupg\//i, label: 'read GPG keys' },
|
|
68
|
+
{ regex: /\$GITHUB_TOKEN|\$GH_TOKEN|\$NPM_TOKEN|\$AWS_SECRET_ACCESS_KEY|\$DISCORD_TOKEN/i, label: 'reference to secret env var' },
|
|
69
|
+
{ regex: /grep\s+[^\n]*(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)[^\n]*/i, label: 'grep for secrets' },
|
|
70
|
+
{ regex: /env\s*\|\s*grep\s+[^\n]*(TOKEN|KEY|SECRET|PASSWORD)/i, label: 'env grep for secrets' }
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// Instruction patterns — AI prompt injection directives
|
|
74
|
+
const INJECTION_INSTRUCTION_PATTERNS = [
|
|
75
|
+
{ regex: /before\s+(reviewing|running|any|code|generating)[^\n]*(run|execute|source):/i, label: 'instruction to execute before review' },
|
|
76
|
+
{ regex: /always\s+run\s+[^\n]*(before|first|initially)/i, label: 'instruction to always run command' },
|
|
77
|
+
{ regex: /send\s+(contents?|data|output|results?)\s+(to|via)\s+https?:\/\//i, label: 'instruction to send data to URL' },
|
|
78
|
+
{ regex: /upload\s+[^\n]*(to|via)\s+https?:\/\//i, label: 'instruction to upload to URL' },
|
|
79
|
+
{ regex: /do\s+not\s+(display|show|output|mention|tell)/i, label: 'instruction to hide activity' }
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Scan AI config files for prompt injection
|
|
84
|
+
*/
|
|
85
|
+
function scanAIConfig(targetPath) {
|
|
86
|
+
const threats = [];
|
|
87
|
+
|
|
88
|
+
for (const configFile of AI_CONFIG_FILES) {
|
|
89
|
+
const filePath = path.join(targetPath, configFile);
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(filePath)) continue;
|
|
92
|
+
|
|
93
|
+
let content;
|
|
94
|
+
try {
|
|
95
|
+
const stat = fs.statSync(filePath);
|
|
96
|
+
if (stat.size > 1024 * 1024) continue; // Skip files > 1MB
|
|
97
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const relPath = configFile;
|
|
103
|
+
const fileThreats = analyzeAIConfigFile(content, relPath);
|
|
104
|
+
threats.push(...fileThreats);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return threats;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Analyze a single AI config file for prompt injection patterns
|
|
112
|
+
*/
|
|
113
|
+
function analyzeAIConfigFile(content, relPath) {
|
|
114
|
+
const threats = [];
|
|
115
|
+
let hasShellCommand = false;
|
|
116
|
+
let hasExfiltration = false;
|
|
117
|
+
let hasCredentialAccess = false;
|
|
118
|
+
|
|
119
|
+
// Check shell command patterns
|
|
120
|
+
for (const pattern of SHELL_COMMAND_PATTERNS) {
|
|
121
|
+
if (pattern.regex.test(content)) {
|
|
122
|
+
hasShellCommand = true;
|
|
123
|
+
threats.push({
|
|
124
|
+
type: pattern.critical ? 'ai_config_injection_critical' : 'ai_config_injection',
|
|
125
|
+
severity: pattern.critical ? 'CRITICAL' : 'HIGH',
|
|
126
|
+
message: `AI config prompt injection: ${pattern.label} in ${relPath}`,
|
|
127
|
+
file: relPath
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check exfiltration patterns
|
|
133
|
+
for (const pattern of EXFIL_PATTERNS) {
|
|
134
|
+
if (pattern.regex.test(content)) {
|
|
135
|
+
hasExfiltration = true;
|
|
136
|
+
threats.push({
|
|
137
|
+
type: 'ai_config_injection_critical',
|
|
138
|
+
severity: 'CRITICAL',
|
|
139
|
+
message: `AI config exfiltration: ${pattern.label} in ${relPath}`,
|
|
140
|
+
file: relPath
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check credential access patterns
|
|
146
|
+
for (const pattern of CREDENTIAL_ACCESS_PATTERNS) {
|
|
147
|
+
if (pattern.regex.test(content)) {
|
|
148
|
+
hasCredentialAccess = true;
|
|
149
|
+
threats.push({
|
|
150
|
+
type: 'ai_config_injection',
|
|
151
|
+
severity: 'HIGH',
|
|
152
|
+
message: `AI config credential access: ${pattern.label} in ${relPath}`,
|
|
153
|
+
file: relPath
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check injection instruction patterns
|
|
159
|
+
for (const pattern of INJECTION_INSTRUCTION_PATTERNS) {
|
|
160
|
+
if (pattern.regex.test(content)) {
|
|
161
|
+
threats.push({
|
|
162
|
+
type: 'ai_config_injection',
|
|
163
|
+
severity: 'HIGH',
|
|
164
|
+
message: `AI config prompt injection: ${pattern.label} in ${relPath}`,
|
|
165
|
+
file: relPath
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Compound detection: shell + exfil or credential access → escalate
|
|
171
|
+
if (hasShellCommand && (hasExfiltration || hasCredentialAccess)) {
|
|
172
|
+
threats.push({
|
|
173
|
+
type: 'ai_config_injection_critical',
|
|
174
|
+
severity: 'CRITICAL',
|
|
175
|
+
message: `AI config compound attack: shell commands + ${hasExfiltration ? 'exfiltration' : 'credential access'} in ${relPath} — ToxicSkills/Clinejection pattern.`,
|
|
176
|
+
file: relPath
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return threats;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { scanAIConfig };
|
|
@@ -221,8 +221,27 @@ function containsDecodePattern(node) {
|
|
|
221
221
|
// workflowPathVars, execPathVars, globalThisAliases,
|
|
222
222
|
// hasFromCharCode, hasEvalInFile (mutable)
|
|
223
223
|
|
|
224
|
+
function isStaticValue(node) {
|
|
225
|
+
if (!node) return false;
|
|
226
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return true;
|
|
227
|
+
if (node.type === 'ArrayExpression') {
|
|
228
|
+
return node.elements.every(el => el && el.type === 'Literal' && typeof el.value === 'string');
|
|
229
|
+
}
|
|
230
|
+
if (node.type === 'ObjectExpression') {
|
|
231
|
+
return node.properties.every(p =>
|
|
232
|
+
p.value && p.value.type === 'Literal' && typeof p.value.value === 'string'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
224
238
|
function handleVariableDeclarator(node, ctx) {
|
|
225
239
|
if (node.id?.type === 'Identifier') {
|
|
240
|
+
// Track statically-assigned variables for dynamic_require qualification
|
|
241
|
+
if (node.init && isStaticValue(node.init)) {
|
|
242
|
+
ctx.staticAssignments.add(node.id.name);
|
|
243
|
+
}
|
|
244
|
+
|
|
226
245
|
// Track dynamic require vars
|
|
227
246
|
if (node.init?.type === 'CallExpression') {
|
|
228
247
|
const initCallName = getCallName(node.init);
|
|
@@ -305,10 +324,15 @@ function handleCallExpression(node, ctx) {
|
|
|
305
324
|
});
|
|
306
325
|
}
|
|
307
326
|
} else if (arg.type === 'Identifier') {
|
|
327
|
+
// If the variable was assigned from a static value (string literal,
|
|
328
|
+
// array of strings, object with string values), it's a plugin loader pattern
|
|
329
|
+
const severity = ctx.staticAssignments.has(arg.name) ? 'LOW' : 'HIGH';
|
|
308
330
|
ctx.threats.push({
|
|
309
331
|
type: 'dynamic_require',
|
|
310
|
-
severity
|
|
311
|
-
message:
|
|
332
|
+
severity,
|
|
333
|
+
message: severity === 'LOW'
|
|
334
|
+
? `Dynamic require() with statically-assigned variable "${arg.name}" (plugin loader pattern).`
|
|
335
|
+
: 'Dynamic require() with variable argument (module name obfuscation).',
|
|
312
336
|
file: ctx.relFile
|
|
313
337
|
});
|
|
314
338
|
}
|
|
@@ -717,14 +741,13 @@ function handleCallExpression(node, ctx) {
|
|
|
717
741
|
message: 'Function() with decode argument (atob/Buffer.from base64) — staged payload execution.',
|
|
718
742
|
file: ctx.relFile
|
|
719
743
|
});
|
|
720
|
-
} else {
|
|
721
|
-
|
|
744
|
+
} else if (!hasOnlyStringLiteralArgs(node)) {
|
|
745
|
+
// Only flag dynamic Function() calls — string literal args (e.g. Function('return this'))
|
|
746
|
+
// are zero-risk globalThis polyfills used by every bundler
|
|
722
747
|
ctx.threats.push({
|
|
723
748
|
type: 'dangerous_call_function',
|
|
724
|
-
severity:
|
|
725
|
-
message:
|
|
726
|
-
? 'Function() with constant string literal (low risk, globalThis polyfill pattern).'
|
|
727
|
-
: 'Function() with dynamic expression (template/factory pattern).',
|
|
749
|
+
severity: 'MEDIUM',
|
|
750
|
+
message: 'Function() with dynamic expression (template/factory pattern).',
|
|
728
751
|
file: ctx.relFile
|
|
729
752
|
});
|
|
730
753
|
}
|
|
@@ -901,15 +924,15 @@ function handleImportExpression(node, ctx) {
|
|
|
901
924
|
|
|
902
925
|
function handleNewExpression(node, ctx) {
|
|
903
926
|
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
:
|
|
911
|
-
|
|
912
|
-
}
|
|
927
|
+
// Skip string literal args — zero-risk globalThis polyfills used by every bundler
|
|
928
|
+
if (!hasOnlyStringLiteralArgs(node)) {
|
|
929
|
+
ctx.threats.push({
|
|
930
|
+
type: 'dangerous_call_function',
|
|
931
|
+
severity: 'MEDIUM',
|
|
932
|
+
message: 'new Function() with dynamic expression (template/factory pattern).',
|
|
933
|
+
file: ctx.relFile
|
|
934
|
+
});
|
|
935
|
+
}
|
|
913
936
|
}
|
|
914
937
|
|
|
915
938
|
// Detect new Proxy(process.env, handler)
|
package/src/scanner/ast.js
CHANGED
|
@@ -64,6 +64,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
64
64
|
threats,
|
|
65
65
|
relFile: path.relative(basePath, filePath),
|
|
66
66
|
dynamicRequireVars: new Set(),
|
|
67
|
+
staticAssignments: new Set(),
|
|
67
68
|
dangerousCmdVars: new Map(),
|
|
68
69
|
workflowPathVars: new Set(),
|
|
69
70
|
execPathVars: new Map(),
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -114,6 +114,10 @@ async function analyzeDataFlow(targetPath, options = {}) {
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Check if a VariableDeclarator init expression is a source-generating expression.
|
|
119
|
+
* Used to track which variables hold data from sensitive sources.
|
|
120
|
+
*/
|
|
117
121
|
function analyzeFile(content, filePath, basePath) {
|
|
118
122
|
const threats = [];
|
|
119
123
|
let ast;
|
|
@@ -208,6 +212,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
208
212
|
name: callName,
|
|
209
213
|
line: node.loc?.start?.line
|
|
210
214
|
});
|
|
215
|
+
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
if (callName === 'exec' || callName === 'execSync') {
|
|
@@ -219,6 +224,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
219
224
|
name: callName,
|
|
220
225
|
line: node.loc?.start?.line
|
|
221
226
|
});
|
|
227
|
+
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
230
|
}
|
|
@@ -268,18 +274,22 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
268
274
|
// DNS resolution as exfiltration sink
|
|
269
275
|
if (obj.name === 'dns' && ['resolve', 'lookup', 'resolve4', 'resolve6', 'resolveTxt'].includes(prop.name)) {
|
|
270
276
|
sinks.push({ type: 'network_send', name: `dns.${prop.name}`, line: node.loc?.start?.line });
|
|
277
|
+
|
|
271
278
|
}
|
|
272
279
|
// HTTP/HTTPS request/get as network sink
|
|
273
280
|
if ((obj.name === 'http' || obj.name === 'https') && ['request', 'get'].includes(prop.name)) {
|
|
274
281
|
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
282
|
+
|
|
275
283
|
}
|
|
276
284
|
// net.connect / net.createConnection / tls.connect as network sink
|
|
277
285
|
if ((obj.name === 'net' || obj.name === 'tls') && ['connect', 'createConnection'].includes(prop.name)) {
|
|
278
286
|
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
287
|
+
|
|
279
288
|
}
|
|
280
289
|
// Instance socket.connect(port, host) when file imports net/tls
|
|
281
290
|
if (hasRawSocketModule && prop.name === 'connect' && node.arguments.length >= 2) {
|
|
282
291
|
sinks.push({ type: 'network_send', name: 'socket.connect', line: node.loc?.start?.line });
|
|
292
|
+
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
295
|
}
|
|
@@ -322,8 +332,9 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
322
332
|
// Check sink methods
|
|
323
333
|
const sinkMethods = MODULE_SINK_METHODS[moduleName];
|
|
324
334
|
if (sinkMethods && sinkMethods[methodName]) {
|
|
335
|
+
const sinkType = sinkMethods[methodName];
|
|
325
336
|
sinks.push({
|
|
326
|
-
type:
|
|
337
|
+
type: sinkType,
|
|
327
338
|
name: `${moduleName}.${methodName}`,
|
|
328
339
|
line: node.loc?.start?.line,
|
|
329
340
|
taint_tracked: true
|
|
@@ -341,8 +352,9 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
341
352
|
// Check sink methods for destructured calls
|
|
342
353
|
const sinkMethods = MODULE_SINK_METHODS[moduleName];
|
|
343
354
|
if (sinkMethods && sinkMethods[methodName]) {
|
|
355
|
+
const sinkType = sinkMethods[methodName];
|
|
344
356
|
sinks.push({
|
|
345
|
-
type:
|
|
357
|
+
type: sinkType,
|
|
346
358
|
name: `${moduleName}.${methodName}`,
|
|
347
359
|
line: node.loc?.start?.line,
|
|
348
360
|
taint_tracked: true
|