muaddib-scanner 2.3.3 → 2.4.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/LICENSE +20 -20
- package/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -1
- package/src/canary-tokens.js +184 -184
- package/src/index.js +10 -1
- 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/response/playbooks.js +3 -0
- package/src/rules/index.js +11 -0
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +2 -2
- package/src/scanner/ast.js +3 -4
- package/src/scanner/dataflow.js +12 -7
- package/src/scanner/deobfuscate.js +579 -591
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/hash.js +118 -118
- package/src/scanner/module-graph.js +2 -6
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/package.js +36 -0
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +33 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/shared/constants.js +113 -93
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-ast-diff.js +317 -319
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
package/src/sarif.js
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { RULES } = require('./rules/index.js');
|
|
4
|
-
|
|
5
|
-
const pkgVersion = (() => {
|
|
6
|
-
try {
|
|
7
|
-
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
8
|
-
} catch {
|
|
9
|
-
return '0.0.0';
|
|
10
|
-
}
|
|
11
|
-
})();
|
|
12
|
-
|
|
13
|
-
function generateSARIF(results) {
|
|
14
|
-
const sarif = {
|
|
15
|
-
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
16
|
-
version: '2.1.0',
|
|
17
|
-
runs: [
|
|
18
|
-
{
|
|
19
|
-
tool: {
|
|
20
|
-
driver: {
|
|
21
|
-
name: 'MUADDIB',
|
|
22
|
-
version: pkgVersion,
|
|
23
|
-
informationUri: 'https://github.com/DNSZLSK/muad-dib',
|
|
24
|
-
rules: Object.values(RULES).map(rule => ({
|
|
25
|
-
id: rule.id,
|
|
26
|
-
name: rule.name,
|
|
27
|
-
shortDescription: { text: rule.description },
|
|
28
|
-
fullDescription: { text: rule.description },
|
|
29
|
-
helpUri: rule.references[0] || '',
|
|
30
|
-
properties: {
|
|
31
|
-
severity: rule.severity,
|
|
32
|
-
confidence: rule.confidence,
|
|
33
|
-
mitre: rule.mitre
|
|
34
|
-
}
|
|
35
|
-
}))
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
properties: results.sandbox ? {
|
|
39
|
-
sandbox: {
|
|
40
|
-
score: results.sandbox.score,
|
|
41
|
-
severity: results.sandbox.severity,
|
|
42
|
-
network: results.sandbox.network || {}
|
|
43
|
-
}
|
|
44
|
-
} : {},
|
|
45
|
-
results: (results.threats || []).map(threat => ({
|
|
46
|
-
ruleId: threat.rule_id,
|
|
47
|
-
level: sarifLevel(threat.severity),
|
|
48
|
-
message: { text: threat.message },
|
|
49
|
-
locations: [
|
|
50
|
-
{
|
|
51
|
-
physicalLocation: {
|
|
52
|
-
artifactLocation: {
|
|
53
|
-
uri: encodeURI(threat.file || ''),
|
|
54
|
-
uriBaseId: '%SRCROOT%'
|
|
55
|
-
},
|
|
56
|
-
region: {
|
|
57
|
-
startLine: threat.line || 1
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
],
|
|
62
|
-
properties: {
|
|
63
|
-
confidence: threat.confidence,
|
|
64
|
-
mitre: threat.mitre
|
|
65
|
-
}
|
|
66
|
-
}))
|
|
67
|
-
}
|
|
68
|
-
]
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
return sarif;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function sarifLevel(severity) {
|
|
75
|
-
switch (severity) {
|
|
76
|
-
case 'CRITICAL': return 'error';
|
|
77
|
-
case 'HIGH': return 'error';
|
|
78
|
-
case 'MEDIUM': return 'warning';
|
|
79
|
-
case 'LOW': return 'note';
|
|
80
|
-
default: return 'note';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function saveSARIF(results, outputPath) {
|
|
85
|
-
if (!outputPath || typeof outputPath !== 'string') {
|
|
86
|
-
throw new Error('Invalid output path for SARIF report');
|
|
87
|
-
}
|
|
88
|
-
const sarif = generateSARIF(results);
|
|
89
|
-
try {
|
|
90
|
-
fs.writeFileSync(outputPath, JSON.stringify(sarif, null, 2));
|
|
91
|
-
} catch (e) {
|
|
92
|
-
throw new Error(`Failed to write SARIF report to ${outputPath}: ${e.message}`);
|
|
93
|
-
}
|
|
94
|
-
return outputPath;
|
|
95
|
-
}
|
|
96
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { RULES } = require('./rules/index.js');
|
|
4
|
+
|
|
5
|
+
const pkgVersion = (() => {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
8
|
+
} catch {
|
|
9
|
+
return '0.0.0';
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
|
|
13
|
+
function generateSARIF(results) {
|
|
14
|
+
const sarif = {
|
|
15
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
16
|
+
version: '2.1.0',
|
|
17
|
+
runs: [
|
|
18
|
+
{
|
|
19
|
+
tool: {
|
|
20
|
+
driver: {
|
|
21
|
+
name: 'MUADDIB',
|
|
22
|
+
version: pkgVersion,
|
|
23
|
+
informationUri: 'https://github.com/DNSZLSK/muad-dib',
|
|
24
|
+
rules: Object.values(RULES).map(rule => ({
|
|
25
|
+
id: rule.id,
|
|
26
|
+
name: rule.name,
|
|
27
|
+
shortDescription: { text: rule.description },
|
|
28
|
+
fullDescription: { text: rule.description },
|
|
29
|
+
helpUri: rule.references[0] || '',
|
|
30
|
+
properties: {
|
|
31
|
+
severity: rule.severity,
|
|
32
|
+
confidence: rule.confidence,
|
|
33
|
+
mitre: rule.mitre
|
|
34
|
+
}
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
properties: results.sandbox ? {
|
|
39
|
+
sandbox: {
|
|
40
|
+
score: results.sandbox.score,
|
|
41
|
+
severity: results.sandbox.severity,
|
|
42
|
+
network: results.sandbox.network || {}
|
|
43
|
+
}
|
|
44
|
+
} : {},
|
|
45
|
+
results: (results.threats || []).map(threat => ({
|
|
46
|
+
ruleId: threat.rule_id,
|
|
47
|
+
level: sarifLevel(threat.severity),
|
|
48
|
+
message: { text: threat.message },
|
|
49
|
+
locations: [
|
|
50
|
+
{
|
|
51
|
+
physicalLocation: {
|
|
52
|
+
artifactLocation: {
|
|
53
|
+
uri: encodeURI(threat.file || ''),
|
|
54
|
+
uriBaseId: '%SRCROOT%'
|
|
55
|
+
},
|
|
56
|
+
region: {
|
|
57
|
+
startLine: threat.line || 1
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
properties: {
|
|
63
|
+
confidence: threat.confidence,
|
|
64
|
+
mitre: threat.mitre
|
|
65
|
+
}
|
|
66
|
+
}))
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return sarif;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sarifLevel(severity) {
|
|
75
|
+
switch (severity) {
|
|
76
|
+
case 'CRITICAL': return 'error';
|
|
77
|
+
case 'HIGH': return 'error';
|
|
78
|
+
case 'MEDIUM': return 'warning';
|
|
79
|
+
case 'LOW': return 'note';
|
|
80
|
+
default: return 'note';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function saveSARIF(results, outputPath) {
|
|
85
|
+
if (!outputPath || typeof outputPath !== 'string') {
|
|
86
|
+
throw new Error('Invalid output path for SARIF report');
|
|
87
|
+
}
|
|
88
|
+
const sarif = generateSARIF(results);
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(outputPath, JSON.stringify(sarif, null, 2));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new Error(`Failed to write SARIF report to ${outputPath}: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
return outputPath;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
97
|
module.exports = { generateSARIF, saveSARIF };
|
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 };
|
|
@@ -68,7 +68,7 @@ const CREDENTIAL_CLI_COMMANDS = [
|
|
|
68
68
|
];
|
|
69
69
|
|
|
70
70
|
// Dangerous shell command patterns for variable tracking
|
|
71
|
-
const DANGEROUS_CMD_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bnc\s+-/, /\/dev\/tcp\//, /\bbash\s+-i
|
|
71
|
+
const DANGEROUS_CMD_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bnc\s+-/, /\/dev\/tcp\//, /\bbash\s+-i/, /\bpowershell\b/, /\bpwsh\b/, /\bnslookup\b/, /\bdig\s+/];
|
|
72
72
|
|
|
73
73
|
// Native APIs targeted for prototype hooking (chalk Sept 2025, Sygnia)
|
|
74
74
|
const HOOKABLE_NATIVES = [
|
|
@@ -325,7 +325,7 @@ function handleCallExpression(node, ctx) {
|
|
|
325
325
|
|
|
326
326
|
if (cmdStr) {
|
|
327
327
|
// Check for dangerous shell patterns
|
|
328
|
-
if (/\|\s*(sh|bash)\b/.test(cmdStr) || /nc\s+-[elp]/.test(cmdStr) || /\/dev\/tcp\//.test(cmdStr) || /bash\s+-i/.test(cmdStr) || /\bcurl\b/.test(cmdStr) || /\bwget\b/.test(cmdStr)) {
|
|
328
|
+
if (/\|\s*(sh|bash)\b/.test(cmdStr) || /nc\s+-[elp]/.test(cmdStr) || /\/dev\/tcp\//.test(cmdStr) || /bash\s+-i/.test(cmdStr) || /\bcurl\b/.test(cmdStr) || /\bwget\b/.test(cmdStr) || /\bpowershell\b/.test(cmdStr) || /\bpwsh\b/.test(cmdStr) || /\bnslookup\b/.test(cmdStr) || /\bdig\s+/.test(cmdStr) || /\bhost\s+\S+\.\S+/.test(cmdStr)) {
|
|
329
329
|
ctx.threats.push({
|
|
330
330
|
type: 'dangerous_exec',
|
|
331
331
|
severity: 'CRITICAL',
|
package/src/scanner/ast.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const acorn = require('acorn');
|
|
3
3
|
const walk = require('acorn-walk');
|
|
4
|
-
const { ACORN_OPTIONS } = require('../shared/constants.js');
|
|
4
|
+
const { ACORN_OPTIONS, safeParse } = require('../shared/constants.js');
|
|
5
5
|
const { analyzeWithDeobfuscation } = require('../shared/analyze-helper.js');
|
|
6
6
|
const {
|
|
7
7
|
handleVariableDeclarator,
|
|
@@ -32,9 +32,8 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
32
32
|
const threats = [];
|
|
33
33
|
let ast;
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch {
|
|
35
|
+
ast = safeParse(content);
|
|
36
|
+
if (!ast) {
|
|
38
37
|
// AST parse failed — apply regex fallback for known dangerous patterns
|
|
39
38
|
|
|
40
39
|
// Workflow manipulation: reads + writes to .github/workflows
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const acorn = require('acorn');
|
|
4
4
|
const walk = require('acorn-walk');
|
|
5
5
|
const { getCallName } = require('../utils.js');
|
|
6
|
-
const { ACORN_OPTIONS } = require('../shared/constants.js');
|
|
6
|
+
const { ACORN_OPTIONS, safeParse } = require('../shared/constants.js');
|
|
7
7
|
const { analyzeWithDeobfuscation } = require('../shared/analyze-helper.js');
|
|
8
8
|
|
|
9
9
|
async function analyzeDataFlow(targetPath, options = {}) {
|
|
@@ -16,11 +16,8 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
16
16
|
const threats = [];
|
|
17
17
|
let ast;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} catch {
|
|
22
|
-
return threats;
|
|
23
|
-
}
|
|
19
|
+
ast = safeParse(content, { locations: true });
|
|
20
|
+
if (!ast) return threats;
|
|
24
21
|
|
|
25
22
|
const sources = [];
|
|
26
23
|
const sinks = [];
|
|
@@ -340,9 +337,17 @@ function isCredentialPath(arg, sensitivePathVars) {
|
|
|
340
337
|
return false;
|
|
341
338
|
}
|
|
342
339
|
|
|
340
|
+
// System identity env vars used for fingerprinting/exfiltration
|
|
341
|
+
const SYSTEM_IDENTITY_ENVS = new Set([
|
|
342
|
+
'USER', 'USERNAME', 'LOGNAME', 'HOME', 'HOSTNAME',
|
|
343
|
+
'USERPROFILE', 'COMPUTERNAME', 'WHOAMI'
|
|
344
|
+
]);
|
|
345
|
+
|
|
343
346
|
function isSensitiveEnv(name) {
|
|
347
|
+
const upper = name.toUpperCase();
|
|
348
|
+
if (SYSTEM_IDENTITY_ENVS.has(upper)) return true;
|
|
344
349
|
const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
|
|
345
|
-
return sensitive.some(s =>
|
|
350
|
+
return sensitive.some(s => upper.includes(s));
|
|
346
351
|
}
|
|
347
352
|
|
|
348
353
|
module.exports = { analyzeDataFlow };
|