muaddib-scanner 2.1.5 → 2.2.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/README.fr.md +33 -8
- package/README.md +33 -8
- package/assets/logo2removebg.png +0 -0
- package/bin/muaddib.js +9 -0
- package/datasets/adversarial/README.md +23 -0
- package/datasets/adversarial/ai-agent-weaponization/index.js +5 -0
- package/datasets/adversarial/ai-agent-weaponization/package.json +9 -0
- package/datasets/adversarial/ai-agent-weaponization/setup.js +83 -0
- package/datasets/adversarial/ai-config-injection/.cursorrules +36 -0
- package/datasets/adversarial/ai-config-injection/index.js +16 -0
- package/datasets/adversarial/ai-config-injection/package.json +8 -0
- package/datasets/adversarial/browser-api-hook/index.js +66 -0
- package/datasets/adversarial/browser-api-hook/package.json +6 -0
- package/datasets/adversarial/bun-runtime-evasion/bun_environment.js +23 -0
- package/datasets/adversarial/bun-runtime-evasion/package.json +9 -0
- package/datasets/adversarial/bun-runtime-evasion/setup.js +10 -0
- package/datasets/adversarial/ci-trigger-exfil/index.js +17 -0
- package/datasets/adversarial/ci-trigger-exfil/package.json +9 -0
- package/datasets/adversarial/conditional-chain/index.js +14 -0
- package/datasets/adversarial/conditional-chain/package.json +9 -0
- package/datasets/adversarial/crypto-wallet-harvest/index.js +44 -0
- package/datasets/adversarial/crypto-wallet-harvest/package.json +6 -0
- package/datasets/adversarial/dead-mans-switch/index.js +35 -0
- package/datasets/adversarial/dead-mans-switch/package.json +9 -0
- package/datasets/adversarial/delayed-exfil/index.js +6 -0
- package/datasets/adversarial/delayed-exfil/package.json +6 -0
- package/datasets/adversarial/detached-background/launcher.js +11 -0
- package/datasets/adversarial/detached-background/package.json +9 -0
- package/datasets/adversarial/detached-background/worker.js +26 -0
- package/datasets/adversarial/discord-webhook-exfil/index.js +95 -0
- package/datasets/adversarial/discord-webhook-exfil/package.json +9 -0
- package/datasets/adversarial/dns-chunk-exfil/index.js +10 -0
- package/datasets/adversarial/dns-chunk-exfil/package.json +6 -0
- package/datasets/adversarial/docker-aware/index.js +10 -0
- package/datasets/adversarial/docker-aware/package.json +6 -0
- package/datasets/adversarial/double-base64-exfil/index.js +11 -0
- package/datasets/adversarial/double-base64-exfil/package.json +9 -0
- package/datasets/adversarial/dynamic-import/index.js +21 -0
- package/datasets/adversarial/dynamic-import/package.json +9 -0
- package/datasets/adversarial/dynamic-require/index.js +3 -0
- package/datasets/adversarial/dynamic-require/package.json +9 -0
- package/datasets/adversarial/fake-captcha-fingerprint/index.js +64 -0
- package/datasets/adversarial/fake-captcha-fingerprint/package.json +9 -0
- package/datasets/adversarial/gh-cli-token-steal/index.js +31 -0
- package/datasets/adversarial/gh-cli-token-steal/package.json +6 -0
- package/datasets/adversarial/github-exfil/index.js +33 -0
- package/datasets/adversarial/github-exfil/package.json +9 -0
- package/datasets/adversarial/iife-exfil/index.js +17 -0
- package/datasets/adversarial/iife-exfil/package.json +9 -0
- package/datasets/adversarial/nested-payload/index.js +3 -0
- package/datasets/adversarial/nested-payload/package.json +9 -0
- package/datasets/adversarial/nested-payload/utils/helper.js +6 -0
- package/datasets/adversarial/nested-payload/utils/lib/format.js +23 -0
- package/datasets/adversarial/postinstall-download/package.json +8 -0
- package/datasets/adversarial/preinstall-background-fork/bootstrap.js +16 -0
- package/datasets/adversarial/preinstall-background-fork/index.js +2 -0
- package/datasets/adversarial/preinstall-background-fork/package.json +9 -0
- package/datasets/adversarial/preinstall-background-fork/stealer.js +67 -0
- package/datasets/adversarial/preinstall-exec/package.json +9 -0
- package/datasets/adversarial/preinstall-exec/steal.js +24 -0
- package/datasets/adversarial/proxy-env-intercept/index.js +33 -0
- package/datasets/adversarial/proxy-env-intercept/package.json +9 -0
- package/datasets/adversarial/pyinstaller-dropper/index.js +25 -0
- package/datasets/adversarial/pyinstaller-dropper/package.json +9 -0
- package/datasets/adversarial/rdd-zero-deps/index.js +32 -0
- package/datasets/adversarial/rdd-zero-deps/init.js +15 -0
- package/datasets/adversarial/rdd-zero-deps/package.json +11 -0
- package/datasets/adversarial/remote-dynamic-dependency/index.js +15 -0
- package/datasets/adversarial/remote-dynamic-dependency/package.json +7 -0
- package/datasets/adversarial/self-hosted-runner-backdoor/index.js +28 -0
- package/datasets/adversarial/self-hosted-runner-backdoor/package.json +9 -0
- package/datasets/adversarial/silent-error-swallow/index.js +32 -0
- package/datasets/adversarial/silent-error-swallow/package.json +6 -0
- package/datasets/adversarial/staged-fetch/index.js +9 -0
- package/datasets/adversarial/staged-fetch/package.json +6 -0
- package/datasets/adversarial/string-concat-obfuscation/index.js +6 -0
- package/datasets/adversarial/string-concat-obfuscation/package.json +6 -0
- package/datasets/adversarial/template-literal-obfuscation/index.js +4 -0
- package/datasets/adversarial/template-literal-obfuscation/package.json +9 -0
- package/datasets/adversarial/triple-base64-github-push/index.js +38 -0
- package/datasets/adversarial/triple-base64-github-push/package.json +9 -0
- package/datasets/adversarial/websocket-exfil/index.js +34 -0
- package/datasets/adversarial/websocket-exfil/package.json +9 -0
- package/datasets/benign/README.md +20 -0
- package/datasets/benign/packages-npm.txt +98 -0
- package/datasets/benign/packages-pypi.txt +49 -0
- package/metrics/v2.1.5.json +753 -0
- package/metrics/v2.2.0.json +753 -0
- package/nul +0 -5
- package/package.json +1 -1
- package/src/commands/evaluate.js +270 -0
- package/src/index.js +12 -5
- package/src/ioc/bootstrap.js +1 -0
- package/src/response/playbooks.js +66 -0
- package/src/rules/index.js +181 -0
- package/src/scanner/ai-config.js +183 -0
- package/src/scanner/ast.js +496 -2
- package/src/scanner/dataflow.js +147 -16
- package/src/scanner/package.js +3 -1
- package/src/utils.js +10 -3
package/nul
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUAD'DIB Evaluate — Scanner effectiveness measurement
|
|
3
|
+
*
|
|
4
|
+
* Measures TPR (Ground Truth), FPR (Benign), and ADR (Adversarial).
|
|
5
|
+
* Saves versioned metrics to metrics/v{version}.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { run } = require('../index.js');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..', '..');
|
|
14
|
+
const GT_DIR = path.join(ROOT, 'tests', 'ground-truth');
|
|
15
|
+
const BENIGN_DIR = path.join(ROOT, 'datasets', 'benign');
|
|
16
|
+
const ADVERSARIAL_DIR = path.join(ROOT, 'datasets', 'adversarial');
|
|
17
|
+
const METRICS_DIR = path.join(ROOT, 'metrics');
|
|
18
|
+
|
|
19
|
+
const GT_THRESHOLD = 3;
|
|
20
|
+
const BENIGN_THRESHOLD = 20;
|
|
21
|
+
|
|
22
|
+
const ADVERSARIAL_THRESHOLDS = {
|
|
23
|
+
// Vague 1 (20 samples)
|
|
24
|
+
'ci-trigger-exfil': 35,
|
|
25
|
+
'delayed-exfil': 30,
|
|
26
|
+
'docker-aware': 35,
|
|
27
|
+
'staged-fetch': 35,
|
|
28
|
+
'dns-chunk-exfil': 35,
|
|
29
|
+
'string-concat-obfuscation': 30,
|
|
30
|
+
'postinstall-download': 30,
|
|
31
|
+
'dynamic-require': 40,
|
|
32
|
+
'iife-exfil': 40,
|
|
33
|
+
'conditional-chain': 30,
|
|
34
|
+
'template-literal-obfuscation': 30,
|
|
35
|
+
'proxy-env-intercept': 40,
|
|
36
|
+
'nested-payload': 30,
|
|
37
|
+
'dynamic-import': 30,
|
|
38
|
+
'websocket-exfil': 30,
|
|
39
|
+
'bun-runtime-evasion': 30,
|
|
40
|
+
'preinstall-exec': 35,
|
|
41
|
+
'remote-dynamic-dependency': 35,
|
|
42
|
+
'github-exfil': 30,
|
|
43
|
+
'detached-background': 35,
|
|
44
|
+
// Vague 3 (5 samples)
|
|
45
|
+
'ai-agent-weaponization': 35,
|
|
46
|
+
'ai-config-injection': 30,
|
|
47
|
+
'rdd-zero-deps': 35,
|
|
48
|
+
'discord-webhook-exfil': 30,
|
|
49
|
+
'preinstall-background-fork': 35,
|
|
50
|
+
// Holdout → promoted (10 samples)
|
|
51
|
+
'silent-error-swallow': 25,
|
|
52
|
+
'double-base64-exfil': 30,
|
|
53
|
+
'crypto-wallet-harvest': 25,
|
|
54
|
+
'self-hosted-runner-backdoor': 20,
|
|
55
|
+
'dead-mans-switch': 30,
|
|
56
|
+
'fake-captcha-fingerprint': 20,
|
|
57
|
+
'pyinstaller-dropper': 35,
|
|
58
|
+
'gh-cli-token-steal': 30,
|
|
59
|
+
'triple-base64-github-push': 30,
|
|
60
|
+
'browser-api-hook': 20
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scan a directory silently and return the result
|
|
65
|
+
*/
|
|
66
|
+
async function silentScan(dir) {
|
|
67
|
+
try {
|
|
68
|
+
return await run(dir, { _capture: true });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { summary: { riskScore: 0, total: 0 }, threats: [], error: err.message };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 1. Ground Truth — scan real-world attack samples
|
|
76
|
+
*/
|
|
77
|
+
async function evaluateGroundTruth() {
|
|
78
|
+
const attacksFile = path.join(GT_DIR, 'attacks.json');
|
|
79
|
+
const data = JSON.parse(fs.readFileSync(attacksFile, 'utf8'));
|
|
80
|
+
const attacks = data.attacks.filter(a => a.expected.min_threats > 0);
|
|
81
|
+
|
|
82
|
+
const details = [];
|
|
83
|
+
let detected = 0;
|
|
84
|
+
|
|
85
|
+
for (const attack of attacks) {
|
|
86
|
+
const sampleDir = path.join(GT_DIR, attack.sample_dir);
|
|
87
|
+
const result = await silentScan(sampleDir);
|
|
88
|
+
const score = result.summary.riskScore;
|
|
89
|
+
const isDetected = score >= GT_THRESHOLD;
|
|
90
|
+
if (isDetected) detected++;
|
|
91
|
+
details.push({
|
|
92
|
+
name: attack.name,
|
|
93
|
+
id: attack.id,
|
|
94
|
+
score,
|
|
95
|
+
detected: isDetected,
|
|
96
|
+
threshold: GT_THRESHOLD
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const total = attacks.length;
|
|
101
|
+
const tpr = total > 0 ? detected / total : 0;
|
|
102
|
+
return { detected, total, tpr, details };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 2. Benign — scan popular npm packages for false positives
|
|
107
|
+
*/
|
|
108
|
+
async function evaluateBenign() {
|
|
109
|
+
const listFile = path.join(BENIGN_DIR, 'packages-npm.txt');
|
|
110
|
+
const packages = fs.readFileSync(listFile, 'utf8')
|
|
111
|
+
.split('\n')
|
|
112
|
+
.map(l => l.trim())
|
|
113
|
+
.filter(l => l && !l.startsWith('#'));
|
|
114
|
+
|
|
115
|
+
const details = [];
|
|
116
|
+
let flagged = 0;
|
|
117
|
+
|
|
118
|
+
for (const pkg of packages) {
|
|
119
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'muaddib-eval-'));
|
|
120
|
+
try {
|
|
121
|
+
// Create minimal project with this package as dependency
|
|
122
|
+
const pkgJson = { name: 'eval-project', version: '1.0.0', dependencies: { [pkg]: '*' } };
|
|
123
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(pkgJson));
|
|
124
|
+
|
|
125
|
+
// Create fake node_modules entry so dependency scanner picks it up
|
|
126
|
+
const parts = pkg.split('/');
|
|
127
|
+
const nmDir = path.join(tmpDir, 'node_modules', ...parts);
|
|
128
|
+
fs.mkdirSync(nmDir, { recursive: true });
|
|
129
|
+
fs.writeFileSync(path.join(nmDir, 'package.json'), JSON.stringify({ name: pkg, version: '999.0.0' }));
|
|
130
|
+
|
|
131
|
+
const result = await silentScan(tmpDir);
|
|
132
|
+
const score = result.summary.riskScore;
|
|
133
|
+
const isFlagged = score > BENIGN_THRESHOLD;
|
|
134
|
+
if (isFlagged) flagged++;
|
|
135
|
+
details.push({ name: pkg, score, flagged: isFlagged });
|
|
136
|
+
} finally {
|
|
137
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const total = packages.length;
|
|
142
|
+
const fpr = total > 0 ? flagged / total : 0;
|
|
143
|
+
return { flagged, total, fpr, details };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 3. Adversarial — scan evasive malicious samples
|
|
148
|
+
*/
|
|
149
|
+
async function evaluateAdversarial() {
|
|
150
|
+
const details = [];
|
|
151
|
+
let detected = 0;
|
|
152
|
+
|
|
153
|
+
const sampleNames = Object.keys(ADVERSARIAL_THRESHOLDS);
|
|
154
|
+
for (const name of sampleNames) {
|
|
155
|
+
const sampleDir = path.join(ADVERSARIAL_DIR, name);
|
|
156
|
+
if (!fs.existsSync(sampleDir)) {
|
|
157
|
+
details.push({ name, score: 0, threshold: ADVERSARIAL_THRESHOLDS[name], detected: false, error: 'directory not found' });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await silentScan(sampleDir);
|
|
162
|
+
const score = result.summary.riskScore;
|
|
163
|
+
const threshold = ADVERSARIAL_THRESHOLDS[name];
|
|
164
|
+
const isDetected = score >= threshold;
|
|
165
|
+
if (isDetected) detected++;
|
|
166
|
+
details.push({ name, score, threshold, detected: isDetected });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const total = sampleNames.length;
|
|
170
|
+
const adr = total > 0 ? detected / total : 0;
|
|
171
|
+
return { detected, total, adr, details };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Save metrics to metrics/v{version}.json
|
|
176
|
+
*/
|
|
177
|
+
function saveMetrics(report) {
|
|
178
|
+
if (!fs.existsSync(METRICS_DIR)) {
|
|
179
|
+
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
const filename = `v${report.version}.json`;
|
|
182
|
+
const filepath = path.join(METRICS_DIR, filename);
|
|
183
|
+
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
|
|
184
|
+
return filepath;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Main evaluate function
|
|
189
|
+
*/
|
|
190
|
+
async function evaluate(options = {}) {
|
|
191
|
+
const version = require('../../package.json').version;
|
|
192
|
+
const jsonMode = options.json || false;
|
|
193
|
+
|
|
194
|
+
if (!jsonMode) {
|
|
195
|
+
console.log(`\n MUAD'DIB Evaluation (v${version})\n`);
|
|
196
|
+
console.log(` [1/3] Ground Truth...`);
|
|
197
|
+
}
|
|
198
|
+
const groundTruth = await evaluateGroundTruth();
|
|
199
|
+
|
|
200
|
+
if (!jsonMode) {
|
|
201
|
+
console.log(` [2/3] Benign packages...`);
|
|
202
|
+
}
|
|
203
|
+
const benign = await evaluateBenign();
|
|
204
|
+
|
|
205
|
+
if (!jsonMode) {
|
|
206
|
+
console.log(` [3/3] Adversarial samples...`);
|
|
207
|
+
}
|
|
208
|
+
const adversarial = await evaluateAdversarial();
|
|
209
|
+
|
|
210
|
+
const report = {
|
|
211
|
+
version,
|
|
212
|
+
date: new Date().toISOString(),
|
|
213
|
+
groundTruth,
|
|
214
|
+
benign,
|
|
215
|
+
adversarial
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const metricsPath = saveMetrics(report);
|
|
219
|
+
|
|
220
|
+
if (jsonMode) {
|
|
221
|
+
console.log(JSON.stringify(report, null, 2));
|
|
222
|
+
} else {
|
|
223
|
+
const tprPct = (groundTruth.tpr * 100).toFixed(1);
|
|
224
|
+
const fprPct = (benign.fpr * 100).toFixed(1);
|
|
225
|
+
const adrPct = (adversarial.adr * 100).toFixed(1);
|
|
226
|
+
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(` Ground Truth (TPR): ${groundTruth.detected}/${groundTruth.total} ${tprPct}%`);
|
|
229
|
+
console.log(` Benign (FPR): ${benign.flagged}/${benign.total} ${fprPct}%`);
|
|
230
|
+
console.log(` Adversarial (ADR): ${adversarial.detected}/${adversarial.total} ${adrPct}%`);
|
|
231
|
+
console.log('');
|
|
232
|
+
|
|
233
|
+
// Show failed adversarial samples
|
|
234
|
+
const missed = adversarial.details.filter(d => !d.detected);
|
|
235
|
+
if (missed.length > 0) {
|
|
236
|
+
console.log(' Adversarial misses:');
|
|
237
|
+
for (const m of missed) {
|
|
238
|
+
console.log(` ${m.name}: score ${m.score} < threshold ${m.threshold}`);
|
|
239
|
+
}
|
|
240
|
+
console.log('');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Show false positives
|
|
244
|
+
const fps = benign.details.filter(d => d.flagged);
|
|
245
|
+
if (fps.length > 0) {
|
|
246
|
+
console.log(' False positives:');
|
|
247
|
+
for (const fp of fps) {
|
|
248
|
+
console.log(` ${fp.name}: score ${fp.score}`);
|
|
249
|
+
}
|
|
250
|
+
console.log('');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(` Saved: ${path.relative(ROOT, metricsPath)}`);
|
|
254
|
+
console.log('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return report;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
evaluate,
|
|
262
|
+
evaluateGroundTruth,
|
|
263
|
+
evaluateBenign,
|
|
264
|
+
evaluateAdversarial,
|
|
265
|
+
saveMetrics,
|
|
266
|
+
silentScan,
|
|
267
|
+
ADVERSARIAL_THRESHOLDS,
|
|
268
|
+
GT_THRESHOLD,
|
|
269
|
+
BENIGN_THRESHOLD
|
|
270
|
+
};
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const { detectPythonProject, normalizePythonName } = require('./scanner/python.j
|
|
|
18
18
|
const { loadCachedIOCs } = require('./ioc/updater.js');
|
|
19
19
|
const { ensureIOCs } = require('./ioc/bootstrap.js');
|
|
20
20
|
const { scanEntropy } = require('./scanner/entropy.js');
|
|
21
|
+
const { scanAIConfig } = require('./scanner/ai-config.js');
|
|
21
22
|
const { detectSuddenLifecycleChange } = require('./temporal-analysis.js');
|
|
22
23
|
const { detectSuddenAstChanges } = require('./temporal-ast-diff.js');
|
|
23
24
|
const { detectPublishAnomaly } = require('./publish-anomaly.js');
|
|
@@ -105,7 +106,10 @@ function scanParanoid(targetPath) {
|
|
|
105
106
|
if (stat.isSymbolicLink()) continue;
|
|
106
107
|
|
|
107
108
|
if (stat.isDirectory()) {
|
|
108
|
-
|
|
109
|
+
const rel = path.relative(targetPath, fullPath).replace(/\\/g, '/');
|
|
110
|
+
const isExcluded = excluded.includes(file) ||
|
|
111
|
+
excluded.some(ex => rel === ex || rel.startsWith(ex + '/'));
|
|
112
|
+
if (!isExcluded) {
|
|
109
113
|
walkDir(fullPath, depth + 1);
|
|
110
114
|
}
|
|
111
115
|
} else if (file.endsWith('.js') || file.endsWith('.json') || file.endsWith('.sh')) {
|
|
@@ -204,7 +208,7 @@ async function run(targetPath, options = {}) {
|
|
|
204
208
|
|
|
205
209
|
// Apply --exclude dirs for this scan
|
|
206
210
|
if (options.exclude && options.exclude.length > 0) {
|
|
207
|
-
setExtraExcludes(options.exclude);
|
|
211
|
+
setExtraExcludes(options.exclude, targetPath);
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
// Detect Python project (synchronous, fast file reads)
|
|
@@ -231,7 +235,8 @@ async function run(targetPath, options = {}) {
|
|
|
231
235
|
ghActionsThreats,
|
|
232
236
|
pythonThreats,
|
|
233
237
|
pypiTyposquatThreats,
|
|
234
|
-
entropyThreats
|
|
238
|
+
entropyThreats,
|
|
239
|
+
aiConfigThreats
|
|
235
240
|
] = await Promise.all([
|
|
236
241
|
scanPackageJson(targetPath),
|
|
237
242
|
scanShellScripts(targetPath),
|
|
@@ -244,7 +249,8 @@ async function run(targetPath, options = {}) {
|
|
|
244
249
|
Promise.resolve(scanGitHubActions(targetPath)),
|
|
245
250
|
Promise.resolve(matchPythonIOCs(pythonDeps, targetPath)),
|
|
246
251
|
Promise.resolve(checkPyPITyposquatting(pythonDeps, targetPath)),
|
|
247
|
-
Promise.resolve(scanEntropy(targetPath, { entropyThreshold: options.entropyThreshold || undefined }))
|
|
252
|
+
Promise.resolve(scanEntropy(targetPath, { entropyThreshold: options.entropyThreshold || undefined })),
|
|
253
|
+
Promise.resolve(scanAIConfig(targetPath))
|
|
248
254
|
]);
|
|
249
255
|
|
|
250
256
|
// Stop spinner now that scanning is complete
|
|
@@ -264,7 +270,8 @@ async function run(targetPath, options = {}) {
|
|
|
264
270
|
...ghActionsThreats,
|
|
265
271
|
...pythonThreats,
|
|
266
272
|
...pypiTyposquatThreats,
|
|
267
|
-
...entropyThreats
|
|
273
|
+
...entropyThreats,
|
|
274
|
+
...aiConfigThreats
|
|
268
275
|
];
|
|
269
276
|
|
|
270
277
|
// Paranoid mode
|
package/src/ioc/bootstrap.js
CHANGED
|
@@ -56,6 +56,36 @@ const PLAYBOOKS = {
|
|
|
56
56
|
exec_wget:
|
|
57
57
|
'Execution de wget via child_process. Verifier l\'URL et les donnees.',
|
|
58
58
|
|
|
59
|
+
dynamic_require:
|
|
60
|
+
'require() avec concatenation detecte. Technique d\'obfuscation pour masquer le module charge. Analyser les variables concatenees.',
|
|
61
|
+
|
|
62
|
+
dangerous_exec:
|
|
63
|
+
'CRITIQUE: Execution de commande shell dangereuse detectee. Isoler la machine. Verifier si la commande a ete executee.',
|
|
64
|
+
|
|
65
|
+
staged_payload:
|
|
66
|
+
'CRITIQUE: Code telecharge depuis le reseau et execute via eval(). Payload distant probable. Isoler et analyser le trafic reseau.',
|
|
67
|
+
|
|
68
|
+
network_require:
|
|
69
|
+
'require(https/http) dans un script lifecycle. Le package telecharge du code lors de l\'installation. Verifier l\'URL de destination.',
|
|
70
|
+
|
|
71
|
+
node_inline_exec:
|
|
72
|
+
'node -e dans un script lifecycle. Code inline execute a l\'installation. Analyser le code inline.',
|
|
73
|
+
|
|
74
|
+
dynamic_import:
|
|
75
|
+
'import() dynamique detecte. Technique d\'evasion pour eviter la detection de require(). Verifier quel module est charge et son usage.',
|
|
76
|
+
|
|
77
|
+
env_proxy_intercept:
|
|
78
|
+
'CRITIQUE: new Proxy(process.env) intercepte tous les acces aux variables d\'environnement. Technique d\'exfiltration silencieuse. Isoler la machine, regenerer tous les secrets.',
|
|
79
|
+
|
|
80
|
+
dynamic_require_exec:
|
|
81
|
+
'CRITIQUE: exec/execSync appele sur un module charge via require() obfusque. Le module child_process est dissimule par concatenation/encodage. Isoler la machine, auditer les commandes executees.',
|
|
82
|
+
|
|
83
|
+
sandbox_evasion:
|
|
84
|
+
'Code detecte la presence d\'un sandbox/container (/.dockerenv, /proc/cgroup). Technique anti-analyse: le malware adapte son comportement selon l\'environnement. Analyser les deux branches (sandbox vs production).',
|
|
85
|
+
|
|
86
|
+
detached_process:
|
|
87
|
+
'spawn/fork avec {detached: true} detecte. Le processus enfant survit a la fin de npm install et execute le payload en arriere-plan. Verifier les processus en cours: ps aux | grep node. Tuer le processus suspect.',
|
|
88
|
+
|
|
59
89
|
known_malicious_package:
|
|
60
90
|
'CRITIQUE: Supprimer immediatement. rm -rf node_modules && npm cache clean --force && npm install',
|
|
61
91
|
|
|
@@ -223,6 +253,42 @@ const PLAYBOOKS = {
|
|
|
223
253
|
new_publisher:
|
|
224
254
|
'New publisher detected. Package published by a different user than before. Verify legitimacy by checking the package\'s npm page and changelog.',
|
|
225
255
|
|
|
256
|
+
credential_command_exec:
|
|
257
|
+
'CRITIQUE: Le code utilise un outil CLI legitime (gh, gcloud, aws, az) pour voler des tokens. ' +
|
|
258
|
+
'Verifier: gh auth status, gcloud auth list, aws sts get-caller-identity. ' +
|
|
259
|
+
'Regenerer tous les tokens des outils concernes. Revoquer les sessions actives.',
|
|
260
|
+
|
|
261
|
+
workflow_write:
|
|
262
|
+
'Le code cree un fichier dans .github/workflows/ — injection de workflow GitHub Actions. ' +
|
|
263
|
+
'Supprimer le fichier cree. Auditer les workflows existants. ' +
|
|
264
|
+
'Verifier les GitHub Actions runs recents pour des executions non autorisees.',
|
|
265
|
+
|
|
266
|
+
binary_dropper:
|
|
267
|
+
'CRITIQUE: Pattern dropper detecte — fichier telecharge, rendu executable (chmod), et execute. ' +
|
|
268
|
+
'Verifier /tmp pour des binaires suspects. Tuer les processus inconnus. ' +
|
|
269
|
+
'Considerer la machine compromise si le binaire a ete execute.',
|
|
270
|
+
|
|
271
|
+
prototype_hook:
|
|
272
|
+
'Prototype de fonction native modifie (fetch, XMLHttpRequest, http.request). ' +
|
|
273
|
+
'Technique d\'interception de trafic pour voler des donnees en transit. ' +
|
|
274
|
+
'Supprimer le package. Auditer le trafic reseau recent.',
|
|
275
|
+
|
|
276
|
+
ai_config_injection:
|
|
277
|
+
'Fichier de config d\'agent IA contient des instructions malveillantes. ' +
|
|
278
|
+
'NE PAS ouvrir le projet avec un agent IA sans verifier les fichiers .cursorrules, CLAUDE.md, copilot-instructions.md. ' +
|
|
279
|
+
'Supprimer ou nettoyer ces fichiers avant toute utilisation. Technique ToxicSkills/Clinejection.',
|
|
280
|
+
|
|
281
|
+
ai_config_injection_critical:
|
|
282
|
+
'CRITIQUE: Fichier de config d\'agent IA contient des commandes d\'exfiltration ou un combo shell + credential access. ' +
|
|
283
|
+
'NE PAS ouvrir ce projet avec un agent IA. Supprimer les fichiers de config compromis. ' +
|
|
284
|
+
'Si deja ouvert avec un agent IA, considerer la machine compromise. Regenerer tous les secrets.',
|
|
285
|
+
|
|
286
|
+
ai_agent_abuse:
|
|
287
|
+
'CRITIQUE: Un agent IA (Claude, Gemini, Q) est invoque avec des flags de bypass de securite ' +
|
|
288
|
+
'(--dangerously-skip-permissions, --yolo, --trust-all-tools). Technique s1ngularity/Nx. ' +
|
|
289
|
+
'NE PAS installer. Verifier si l\'agent a ete execute. Si oui, considerer la machine compromise. ' +
|
|
290
|
+
'Auditer les fichiers sensibles (.ssh, .aws, .env) pour des acces non autorises.',
|
|
291
|
+
|
|
226
292
|
canary_exfiltration:
|
|
227
293
|
'CRITIQUE: Le package a tente de voler des credentials (honey tokens). Comportement malveillant confirme. ' +
|
|
228
294
|
'NE PAS installer. Signaler immediatement sur npm/PyPI. ' +
|
package/src/rules/index.js
CHANGED
|
@@ -385,6 +385,96 @@ const RULES = {
|
|
|
385
385
|
references: ['https://attack.mitre.org/techniques/T1027/'],
|
|
386
386
|
mitre: 'T1027'
|
|
387
387
|
},
|
|
388
|
+
dynamic_require: {
|
|
389
|
+
id: 'MUADDIB-AST-006',
|
|
390
|
+
name: 'Dynamic Require with Concatenation',
|
|
391
|
+
severity: 'HIGH',
|
|
392
|
+
confidence: 'high',
|
|
393
|
+
description: 'require() avec concatenation de chaines — technique d\'obfuscation pour masquer le nom du module',
|
|
394
|
+
references: ['https://attack.mitre.org/techniques/T1027/'],
|
|
395
|
+
mitre: 'T1027'
|
|
396
|
+
},
|
|
397
|
+
dangerous_exec: {
|
|
398
|
+
id: 'MUADDIB-AST-007',
|
|
399
|
+
name: 'Dangerous Shell Command Execution',
|
|
400
|
+
severity: 'CRITICAL',
|
|
401
|
+
confidence: 'high',
|
|
402
|
+
description: 'exec() avec commande shell dangereuse (pipe to shell, reverse shell, netcat)',
|
|
403
|
+
references: ['https://owasp.org/www-community/attacks/Command_Injection'],
|
|
404
|
+
mitre: 'T1059.004'
|
|
405
|
+
},
|
|
406
|
+
staged_payload: {
|
|
407
|
+
id: 'MUADDIB-FLOW-002',
|
|
408
|
+
name: 'Staged Payload Execution',
|
|
409
|
+
severity: 'CRITICAL',
|
|
410
|
+
confidence: 'high',
|
|
411
|
+
description: 'Telechargement reseau + eval() dans le meme fichier — execution de payload distant',
|
|
412
|
+
references: ['https://attack.mitre.org/techniques/T1105/'],
|
|
413
|
+
mitre: 'T1105'
|
|
414
|
+
},
|
|
415
|
+
network_require: {
|
|
416
|
+
id: 'MUADDIB-PKG-006',
|
|
417
|
+
name: 'Network Module in Lifecycle Script',
|
|
418
|
+
severity: 'HIGH',
|
|
419
|
+
confidence: 'high',
|
|
420
|
+
description: 'require(https/http) dans un script lifecycle — telechargement au moment de l\'installation',
|
|
421
|
+
references: ['https://blog.phylum.io/shai-hulud-npm-worm'],
|
|
422
|
+
mitre: 'T1105'
|
|
423
|
+
},
|
|
424
|
+
node_inline_exec: {
|
|
425
|
+
id: 'MUADDIB-PKG-007',
|
|
426
|
+
name: 'Node Inline Execution in Lifecycle Script',
|
|
427
|
+
severity: 'HIGH',
|
|
428
|
+
confidence: 'high',
|
|
429
|
+
description: 'node -e dans un script lifecycle — execution de code inline au moment de l\'installation',
|
|
430
|
+
references: ['https://owasp.org/www-community/attacks/Command_Injection'],
|
|
431
|
+
mitre: 'T1059.007'
|
|
432
|
+
},
|
|
433
|
+
dynamic_import: {
|
|
434
|
+
id: 'MUADDIB-AST-008',
|
|
435
|
+
name: 'Dynamic import() of Dangerous Module',
|
|
436
|
+
severity: 'HIGH',
|
|
437
|
+
confidence: 'high',
|
|
438
|
+
description: 'import() dynamique pour charger un module dangereux ou avec argument calcule — technique d\'evasion pour eviter la detection de require()',
|
|
439
|
+
references: ['https://attack.mitre.org/techniques/T1027/'],
|
|
440
|
+
mitre: 'T1027'
|
|
441
|
+
},
|
|
442
|
+
env_proxy_intercept: {
|
|
443
|
+
id: 'MUADDIB-AST-009',
|
|
444
|
+
name: 'Environment Variable Proxy Interception',
|
|
445
|
+
severity: 'CRITICAL',
|
|
446
|
+
confidence: 'high',
|
|
447
|
+
description: 'new Proxy(process.env) detecte — intercepte silencieusement tous les acces aux variables d\'environnement pour exfiltration',
|
|
448
|
+
references: ['https://attack.mitre.org/techniques/T1552/001/'],
|
|
449
|
+
mitre: 'T1552.001'
|
|
450
|
+
},
|
|
451
|
+
dynamic_require_exec: {
|
|
452
|
+
id: 'MUADDIB-AST-010',
|
|
453
|
+
name: 'Command Execution via Dynamic Require',
|
|
454
|
+
severity: 'CRITICAL',
|
|
455
|
+
confidence: 'high',
|
|
456
|
+
description: 'exec/execSync appele sur un module charge dynamiquement (require obfusque) — execution de commandes dissimulees',
|
|
457
|
+
references: ['https://attack.mitre.org/techniques/T1059/007/'],
|
|
458
|
+
mitre: 'T1059.007'
|
|
459
|
+
},
|
|
460
|
+
sandbox_evasion: {
|
|
461
|
+
id: 'MUADDIB-AST-011',
|
|
462
|
+
name: 'Sandbox/Container Evasion',
|
|
463
|
+
severity: 'HIGH',
|
|
464
|
+
confidence: 'high',
|
|
465
|
+
description: 'Detection de sandbox/container (/.dockerenv, /proc/cgroup) — technique anti-analyse pour eviter la detection en environnement controle',
|
|
466
|
+
references: ['https://attack.mitre.org/techniques/T1497/001/'],
|
|
467
|
+
mitre: 'T1497.001'
|
|
468
|
+
},
|
|
469
|
+
detached_process: {
|
|
470
|
+
id: 'MUADDIB-AST-012',
|
|
471
|
+
name: 'Detached Background Process',
|
|
472
|
+
severity: 'HIGH',
|
|
473
|
+
confidence: 'high',
|
|
474
|
+
description: 'spawn/fork avec {detached: true} — le processus survit a la fin de npm install et execute le payload en arriere-plan',
|
|
475
|
+
references: ['https://attack.mitre.org/techniques/T1036/009/'],
|
|
476
|
+
mitre: 'T1036.009'
|
|
477
|
+
},
|
|
388
478
|
dangerous_call_function: {
|
|
389
479
|
id: 'MUADDIB-AST-005',
|
|
390
480
|
name: 'new Function() Constructor',
|
|
@@ -395,6 +485,97 @@ const RULES = {
|
|
|
395
485
|
mitre: 'T1059.007'
|
|
396
486
|
},
|
|
397
487
|
|
|
488
|
+
credential_command_exec: {
|
|
489
|
+
id: 'MUADDIB-AST-014',
|
|
490
|
+
name: 'Credential Theft via CLI Tool',
|
|
491
|
+
severity: 'CRITICAL',
|
|
492
|
+
confidence: 'high',
|
|
493
|
+
description: 'exec/execSync appelle un outil CLI legitime pour voler des tokens d\'authentification (gh auth token, gcloud auth, aws sts). Technique s1ngularity/Nx.',
|
|
494
|
+
references: [
|
|
495
|
+
'https://snyk.io/blog/malicious-npm-packages-abuse-ai-agents/',
|
|
496
|
+
'https://attack.mitre.org/techniques/T1059/'
|
|
497
|
+
],
|
|
498
|
+
mitre: 'T1059'
|
|
499
|
+
},
|
|
500
|
+
workflow_write: {
|
|
501
|
+
id: 'MUADDIB-AST-015',
|
|
502
|
+
name: 'GitHub Actions Workflow Write',
|
|
503
|
+
severity: 'HIGH',
|
|
504
|
+
confidence: 'high',
|
|
505
|
+
description: 'fs.writeFileSync cree un fichier dans .github/workflows — injection de workflow GitHub Actions pour persistence. Technique Shai-Hulud 2.0.',
|
|
506
|
+
references: [
|
|
507
|
+
'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack',
|
|
508
|
+
'https://attack.mitre.org/techniques/T1195/002/'
|
|
509
|
+
],
|
|
510
|
+
mitre: 'T1195.002'
|
|
511
|
+
},
|
|
512
|
+
binary_dropper: {
|
|
513
|
+
id: 'MUADDIB-AST-016',
|
|
514
|
+
name: 'Binary Dropper Pattern',
|
|
515
|
+
severity: 'CRITICAL',
|
|
516
|
+
confidence: 'high',
|
|
517
|
+
description: 'fs.chmodSync avec permissions executables (0o755/0o777) — pattern de dropper binaire: telecharge, ecrit, chmod, execute.',
|
|
518
|
+
references: [
|
|
519
|
+
'https://www.sonatype.com/blog/phantomraven-supply-chain-attack',
|
|
520
|
+
'https://attack.mitre.org/techniques/T1105/'
|
|
521
|
+
],
|
|
522
|
+
mitre: 'T1105'
|
|
523
|
+
},
|
|
524
|
+
prototype_hook: {
|
|
525
|
+
id: 'MUADDIB-AST-017',
|
|
526
|
+
name: 'Native API Prototype Hooking',
|
|
527
|
+
severity: 'HIGH',
|
|
528
|
+
confidence: 'high',
|
|
529
|
+
description: 'Modification du prototype ou remplacement de fonctions natives du navigateur/Node.js (fetch, XMLHttpRequest, http.request). Technique chalk/debug (Sygnia, sept 2025) pour intercepter du trafic.',
|
|
530
|
+
references: [
|
|
531
|
+
'https://www.sygnia.co/blog/malicious-chalk-debug-npm-packages/',
|
|
532
|
+
'https://attack.mitre.org/techniques/T1557/'
|
|
533
|
+
],
|
|
534
|
+
mitre: 'T1557'
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
ai_config_injection: {
|
|
538
|
+
id: 'MUADDIB-AICONF-001',
|
|
539
|
+
name: 'AI Config Prompt Injection',
|
|
540
|
+
severity: 'HIGH',
|
|
541
|
+
confidence: 'high',
|
|
542
|
+
description: 'Fichier de configuration d\'agent IA (.cursorrules, CLAUDE.md, copilot-instructions.md) contient des instructions d\'execution de commandes shell ou d\'acces a des credentials. Technique ToxicSkills/Clinejection.',
|
|
543
|
+
references: [
|
|
544
|
+
'https://snyk.io/blog/toxicskills-prompt-injection-ai-agents/',
|
|
545
|
+
'https://snyk.io/blog/clinejection-ai-config-prompt-injection/',
|
|
546
|
+
'https://arxiv.org/abs/2601.17548'
|
|
547
|
+
],
|
|
548
|
+
mitre: 'T1059'
|
|
549
|
+
},
|
|
550
|
+
ai_config_injection_critical: {
|
|
551
|
+
id: 'MUADDIB-AICONF-002',
|
|
552
|
+
name: 'AI Config Prompt Injection (Critical)',
|
|
553
|
+
severity: 'CRITICAL',
|
|
554
|
+
confidence: 'high',
|
|
555
|
+
description: 'Fichier de configuration d\'agent IA contient des commandes d\'exfiltration (curl POST vers un domaine externe, pipe vers shell) ou une combinaison commande shell + acces credentials. Attaque confirmee.',
|
|
556
|
+
references: [
|
|
557
|
+
'https://snyk.io/blog/toxicskills-prompt-injection-ai-agents/',
|
|
558
|
+
'https://snyk.io/blog/clinejection-ai-config-prompt-injection/',
|
|
559
|
+
'https://arxiv.org/abs/2601.17548',
|
|
560
|
+
'https://developer.nvidia.com/blog/ai-agent-security-guidance/'
|
|
561
|
+
],
|
|
562
|
+
mitre: 'T1059'
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
ai_agent_abuse: {
|
|
566
|
+
id: 'MUADDIB-AST-013',
|
|
567
|
+
name: 'AI Agent Weaponization',
|
|
568
|
+
severity: 'CRITICAL',
|
|
569
|
+
confidence: 'high',
|
|
570
|
+
description: 'Invocation d\'un agent IA (Claude, Gemini, Q, Aider) avec des flags qui desactivent les controles de securite (--dangerously-skip-permissions, --yolo, --trust-all-tools). Technique s1ngularity/Nx (aout 2025).',
|
|
571
|
+
references: [
|
|
572
|
+
'https://snyk.io/blog/malicious-npm-packages-abuse-ai-agents/',
|
|
573
|
+
'https://stepsecurity.io/blog/ai-agent-weaponization-supply-chain',
|
|
574
|
+
'https://attack.mitre.org/techniques/T1059/'
|
|
575
|
+
],
|
|
576
|
+
mitre: 'T1059'
|
|
577
|
+
},
|
|
578
|
+
|
|
398
579
|
// GitHub Actions patterns
|
|
399
580
|
shai_hulud_backdoor: {
|
|
400
581
|
id: 'MUADDIB-GHA-001',
|