muaddib-scanner 1.1.6 → 1.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/bin/muaddib.js +35 -1
- package/data/iocs.json +1 -1
- package/docker/Dockerfile +19 -0
- package/docker/sandbox-runner.sh +26 -0
- package/package.json +1 -1
- package/src/index.js +5 -1
- package/src/ioc/feeds.js +23 -2
- package/src/sandbox.js +154 -0
- package/src/scanner/github-actions.js +46 -0
package/bin/muaddib.js
CHANGED
|
@@ -5,6 +5,7 @@ const { watch } = require('../src/watch.js');
|
|
|
5
5
|
const { startDaemon } = require('../src/daemon.js');
|
|
6
6
|
const { runScraper } = require('../src/ioc/scraper.js');
|
|
7
7
|
const { safeInstall } = require('../src/safe-install.js');
|
|
8
|
+
const { buildSandboxImage, runSandbox } = require('../src/sandbox.js');
|
|
8
9
|
|
|
9
10
|
const args = process.argv.slice(2);
|
|
10
11
|
const command = args[0];
|
|
@@ -65,6 +66,7 @@ async function interactiveMenu() {
|
|
|
65
66
|
{ name: 'Start daemon', value: 'daemon' },
|
|
66
67
|
{ name: 'Update IOCs', value: 'update' },
|
|
67
68
|
{ name: 'Scrape new IOCs', value: 'scrape' },
|
|
69
|
+
{ name: 'Sandbox analysis', value: 'sandbox' },
|
|
68
70
|
{ name: 'Quit', value: 'quit' }
|
|
69
71
|
]
|
|
70
72
|
});
|
|
@@ -151,6 +153,21 @@ async function interactiveMenu() {
|
|
|
151
153
|
console.log(`[OK] ${result.added} new IOCs (total: ${result.total})`);
|
|
152
154
|
process.exit(0);
|
|
153
155
|
}
|
|
156
|
+
|
|
157
|
+
if (action === 'sandbox') {
|
|
158
|
+
const packageName = await input({
|
|
159
|
+
message: 'Package name to analyze:'
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!packageName.trim()) {
|
|
163
|
+
console.log('No package specified.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await buildSandboxImage();
|
|
168
|
+
const results = await runSandbox(packageName.trim());
|
|
169
|
+
process.exit(results.suspicious ? 1 : 0);
|
|
170
|
+
}
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
const helpText = `
|
|
@@ -164,6 +181,7 @@ const helpText = `
|
|
|
164
181
|
muaddib daemon [options] Start daemon
|
|
165
182
|
muaddib update Update IOCs
|
|
166
183
|
muaddib scrape Scrape new IOCs
|
|
184
|
+
muaddib sandbox <pkg> Analyse un package dans un container Docker isole
|
|
167
185
|
|
|
168
186
|
Options:
|
|
169
187
|
--json JSON output
|
|
@@ -238,7 +256,23 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
238
256
|
}).catch(err => {
|
|
239
257
|
console.error('[ERROR]', err.message);
|
|
240
258
|
process.exit(1);
|
|
241
|
-
});
|
|
259
|
+
});
|
|
260
|
+
} else if (command === 'sandbox') {
|
|
261
|
+
const packageName = options[0];
|
|
262
|
+
if (!packageName) {
|
|
263
|
+
console.log('Usage: muaddib sandbox <package-name>');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
buildSandboxImage()
|
|
268
|
+
.then(() => runSandbox(packageName))
|
|
269
|
+
.then((results) => {
|
|
270
|
+
process.exit(results.suspicious ? 1 : 0);
|
|
271
|
+
})
|
|
272
|
+
.catch((err) => {
|
|
273
|
+
console.error('[ERROR]', err.message);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
|
242
276
|
} else if (command === 'help') {
|
|
243
277
|
console.log(helpText);
|
|
244
278
|
process.exit(0);
|
package/data/iocs.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM node:20-alpine
|
|
2
|
+
|
|
3
|
+
# Outils de monitoring
|
|
4
|
+
RUN apk add --no-cache strace curl tcpdump
|
|
5
|
+
|
|
6
|
+
# User non-root
|
|
7
|
+
RUN adduser -D sandboxuser
|
|
8
|
+
|
|
9
|
+
# Dossier de travail avec bonnes permissions
|
|
10
|
+
WORKDIR /sandbox
|
|
11
|
+
RUN chown sandboxuser:sandboxuser /sandbox
|
|
12
|
+
|
|
13
|
+
# Script d'analyse
|
|
14
|
+
COPY sandbox-runner.sh /sandbox/
|
|
15
|
+
RUN chmod +x /sandbox/sandbox-runner.sh
|
|
16
|
+
|
|
17
|
+
USER sandboxuser
|
|
18
|
+
|
|
19
|
+
ENTRYPOINT ["/sandbox/sandbox-runner.sh"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
PACKAGE=$1
|
|
3
|
+
|
|
4
|
+
echo "[SANDBOX] Installing $PACKAGE..."
|
|
5
|
+
|
|
6
|
+
# Capturer les connexions réseau en background
|
|
7
|
+
tcpdump -i any -w /tmp/network.pcap 2>/dev/null &
|
|
8
|
+
TCPDUMP_PID=$!
|
|
9
|
+
|
|
10
|
+
# Installer le package avec strace pour capturer les appels système
|
|
11
|
+
strace -f -e trace=network,process,file -o /tmp/strace.log npm install "$PACKAGE" --ignore-scripts=false 2>&1
|
|
12
|
+
|
|
13
|
+
# Arrêter tcpdump
|
|
14
|
+
kill $TCPDUMP_PID 2>/dev/null
|
|
15
|
+
|
|
16
|
+
# Analyser les résultats
|
|
17
|
+
echo "[SANDBOX] === NETWORK CONNECTIONS ==="
|
|
18
|
+
grep -E "connect|sendto" /tmp/strace.log | head -20
|
|
19
|
+
|
|
20
|
+
echo "[SANDBOX] === PROCESS SPAWNS ==="
|
|
21
|
+
grep -E "execve|clone" /tmp/strace.log | head -20
|
|
22
|
+
|
|
23
|
+
echo "[SANDBOX] === FILE ACCESS ==="
|
|
24
|
+
grep -E "openat.*npmrc|openat.*ssh|openat.*aws" /tmp/strace.log | head -20
|
|
25
|
+
|
|
26
|
+
echo "[SANDBOX] Done."
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const { scanTyposquatting } = require('./scanner/typosquat.js');
|
|
|
13
13
|
const { sendWebhook } = require('./webhook.js');
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { scanGitHubActions } = require('./scanner/github-actions.js');
|
|
16
17
|
|
|
17
18
|
// Scan paranoid mode
|
|
18
19
|
function scanParanoid(targetPath) {
|
|
@@ -95,6 +96,9 @@ async function run(targetPath, options = {}) {
|
|
|
95
96
|
const typosquatThreats = await scanTyposquatting(targetPath);
|
|
96
97
|
threats.push(...typosquatThreats);
|
|
97
98
|
|
|
99
|
+
const ghActionsThreats = scanGitHubActions(targetPath);
|
|
100
|
+
threats.push(...ghActionsThreats);
|
|
101
|
+
|
|
98
102
|
// Paranoid mode
|
|
99
103
|
if (options.paranoid) {
|
|
100
104
|
console.log('[PARANOID] Mode ultra-strict active\n');
|
|
@@ -215,7 +219,7 @@ async function run(targetPath, options = {}) {
|
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
// Envoyer webhook si configure
|
|
218
|
-
if (options.webhook) {
|
|
222
|
+
if (options.webhook && threats.length > 0) {
|
|
219
223
|
try {
|
|
220
224
|
await sendWebhook(options.webhook, result);
|
|
221
225
|
console.log(`[OK] Alerte envoyee au webhook`);
|
package/src/ioc/feeds.js
CHANGED
|
@@ -15,7 +15,22 @@ const KNOWN_MALICIOUS_HASHES = [
|
|
|
15
15
|
const SUSPICIOUS_REPO_MARKERS = [
|
|
16
16
|
'Sha1-Hulud',
|
|
17
17
|
'Shai-Hulud',
|
|
18
|
-
'The Second Coming'
|
|
18
|
+
'The Second Coming',
|
|
19
|
+
'The Continued Coming',
|
|
20
|
+
'F**K Guillermo',
|
|
21
|
+
'F**K VERCEL',
|
|
22
|
+
'SHA1HULUD',
|
|
23
|
+
'Only Happy Girl',
|
|
24
|
+
'Goldox-T3chs',
|
|
25
|
+
'Free AI at api.airforce'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SUSPICIOUS_FILES = [
|
|
29
|
+
'setup_bun.js',
|
|
30
|
+
'bun_environment.js',
|
|
31
|
+
'bun_installer.js',
|
|
32
|
+
'environment_source.js',
|
|
33
|
+
'.github/workflows/discussion.yaml'
|
|
19
34
|
];
|
|
20
35
|
|
|
21
36
|
function isKnownMalicious(packageName) {
|
|
@@ -32,11 +47,17 @@ function hasSuspiciousMarker(text) {
|
|
|
32
47
|
);
|
|
33
48
|
}
|
|
34
49
|
|
|
50
|
+
function isSuspiciousFile(filename) {
|
|
51
|
+
return SUSPICIOUS_FILES.some(f => filename.includes(f));
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
module.exports = {
|
|
36
55
|
isKnownMalicious,
|
|
37
56
|
isKnownMaliciousHash,
|
|
38
57
|
hasSuspiciousMarker,
|
|
58
|
+
isSuspiciousFile,
|
|
39
59
|
KNOWN_MALICIOUS_PACKAGES,
|
|
40
60
|
KNOWN_MALICIOUS_HASHES,
|
|
41
|
-
SUSPICIOUS_REPO_MARKERS
|
|
61
|
+
SUSPICIOUS_REPO_MARKERS,
|
|
62
|
+
SUSPICIOUS_FILES
|
|
42
63
|
};
|
package/src/sandbox.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DOCKER_IMAGE = 'muaddib-sandbox';
|
|
5
|
+
|
|
6
|
+
async function buildSandboxImage() {
|
|
7
|
+
console.log('[SANDBOX] Building Docker image...');
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const dockerfilePath = path.join(__dirname, '..', 'docker');
|
|
11
|
+
const proc = spawn('docker', ['build', '-t', DOCKER_IMAGE, dockerfilePath], {
|
|
12
|
+
stdio: 'inherit'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
proc.on('close', (code) => {
|
|
16
|
+
if (code === 0) {
|
|
17
|
+
console.log('[SANDBOX] Image built successfully.');
|
|
18
|
+
resolve();
|
|
19
|
+
} else {
|
|
20
|
+
reject(new Error(`Docker build failed with code ${code}`));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
proc.on('error', reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function runSandbox(packageName) {
|
|
29
|
+
console.log(`\n[SANDBOX] Analyzing "${packageName}" in isolated container...\n`);
|
|
30
|
+
|
|
31
|
+
const results = {
|
|
32
|
+
package: packageName,
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
network: [],
|
|
35
|
+
processes: [],
|
|
36
|
+
fileAccess: [],
|
|
37
|
+
suspicious: false,
|
|
38
|
+
threats: []
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const proc = spawn('docker', [
|
|
43
|
+
'run',
|
|
44
|
+
'--rm',
|
|
45
|
+
'--network=bridge',
|
|
46
|
+
'--memory=512m',
|
|
47
|
+
'--cpus=1',
|
|
48
|
+
'--pids-limit=100',
|
|
49
|
+
DOCKER_IMAGE,
|
|
50
|
+
packageName
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
let output = '';
|
|
54
|
+
let currentSection = null;
|
|
55
|
+
|
|
56
|
+
proc.stdout.on('data', (data) => {
|
|
57
|
+
const text = data.toString();
|
|
58
|
+
output += text;
|
|
59
|
+
process.stdout.write(text);
|
|
60
|
+
|
|
61
|
+
// Parse sections
|
|
62
|
+
if (text.includes('=== NETWORK CONNECTIONS ===')) {
|
|
63
|
+
currentSection = 'network';
|
|
64
|
+
} else if (text.includes('=== PROCESS SPAWNS ===')) {
|
|
65
|
+
currentSection = 'processes';
|
|
66
|
+
} else if (text.includes('=== FILE ACCESS ===')) {
|
|
67
|
+
currentSection = 'fileAccess';
|
|
68
|
+
} else if (currentSection && text.trim()) {
|
|
69
|
+
results[currentSection].push(text.trim());
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
proc.stderr.on('data', (data) => {
|
|
74
|
+
process.stderr.write(data);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
proc.on('close', (code) => {
|
|
78
|
+
// Analyze results
|
|
79
|
+
results.suspicious = analyzeResults(results);
|
|
80
|
+
|
|
81
|
+
if (results.suspicious) {
|
|
82
|
+
console.log('\n[SANDBOX] ⚠️ SUSPICIOUS BEHAVIOR DETECTED!\n');
|
|
83
|
+
for (const threat of results.threats) {
|
|
84
|
+
console.log(` [${threat.severity}] ${threat.message}`);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.log('\n[SANDBOX] ✓ No suspicious behavior detected.\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
resolve(results);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
proc.on('error', (err) => {
|
|
94
|
+
if (err.message.includes('ENOENT')) {
|
|
95
|
+
reject(new Error('Docker not found. Please install Docker Desktop.'));
|
|
96
|
+
} else {
|
|
97
|
+
reject(err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function analyzeResults(results) {
|
|
104
|
+
let suspicious = false;
|
|
105
|
+
|
|
106
|
+
// Check for suspicious network connections
|
|
107
|
+
const suspiciousHosts = ['pastebin', 'discord.com/api/webhooks', 'ngrok', 'burpcollaborator'];
|
|
108
|
+
for (const conn of results.network) {
|
|
109
|
+
for (const host of suspiciousHosts) {
|
|
110
|
+
if (conn.toLowerCase().includes(host)) {
|
|
111
|
+
results.threats.push({
|
|
112
|
+
severity: 'CRITICAL',
|
|
113
|
+
type: 'suspicious_network',
|
|
114
|
+
message: `Connection to suspicious host: ${host}`
|
|
115
|
+
});
|
|
116
|
+
suspicious = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for credential file access
|
|
122
|
+
const sensitiveFiles = ['.npmrc', '.ssh', '.aws', '.gitconfig', '.env'];
|
|
123
|
+
for (const access of results.fileAccess) {
|
|
124
|
+
for (const file of sensitiveFiles) {
|
|
125
|
+
if (access.includes(file)) {
|
|
126
|
+
results.threats.push({
|
|
127
|
+
severity: 'HIGH',
|
|
128
|
+
type: 'credential_access',
|
|
129
|
+
message: `Access to sensitive file: ${file}`
|
|
130
|
+
});
|
|
131
|
+
suspicious = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for suspicious process spawns
|
|
137
|
+
const suspiciousProcesses = ['curl ', 'wget ', '/nc ', 'netcat', 'bash -c', 'sh -c', 'powershell'];
|
|
138
|
+
for (const proc of results.processes) {
|
|
139
|
+
for (const suspicious_proc of suspiciousProcesses) {
|
|
140
|
+
if (proc.toLowerCase().includes(suspicious_proc)) {
|
|
141
|
+
results.threats.push({
|
|
142
|
+
severity: 'HIGH',
|
|
143
|
+
type: 'suspicious_process',
|
|
144
|
+
message: `Suspicious process spawn: ${suspicious_proc}`
|
|
145
|
+
});
|
|
146
|
+
suspicious = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return suspicious;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { buildSandboxImage, runSandbox };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function scanGitHubActions(targetPath) {
|
|
5
|
+
const threats = [];
|
|
6
|
+
const workflowsPath = path.join(targetPath, '.github', 'workflows');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(workflowsPath)) {
|
|
9
|
+
return threats;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const files = fs.readdirSync(workflowsPath);
|
|
13
|
+
|
|
14
|
+
for (const file of files) {
|
|
15
|
+
const filePath = path.join(workflowsPath, file);
|
|
16
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
17
|
+
|
|
18
|
+
// Détection du backdoor Shai-Hulud discussion.yaml
|
|
19
|
+
if (file === 'discussion.yaml' || file === 'discussion.yml') {
|
|
20
|
+
if (content.includes('runs-on: self-hosted') && content.includes('github.event.discussion.body')) {
|
|
21
|
+
threats.push({
|
|
22
|
+
type: 'shai_hulud_backdoor',
|
|
23
|
+
severity: 'CRITICAL',
|
|
24
|
+
message: 'Backdoor Shai-Hulud détecté: workflow discussion.yaml avec injection via self-hosted runner',
|
|
25
|
+
file: `.github/workflows/${file}`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Détection générique de workflows suspects
|
|
31
|
+
if (content.includes('runs-on: self-hosted')) {
|
|
32
|
+
if (content.includes('${{ github.event.') && (content.includes('.body') || content.includes('.title'))) {
|
|
33
|
+
threats.push({
|
|
34
|
+
type: 'workflow_injection',
|
|
35
|
+
severity: 'HIGH',
|
|
36
|
+
message: 'Injection potentielle dans GitHub Actions: input non sanitisé sur self-hosted runner',
|
|
37
|
+
file: `.github/workflows/${file}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return threats;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { scanGitHubActions };
|