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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.1.0",
3
- "updated": "2026-01-08T21:13:53.854Z",
3
+ "updated": "2026-01-10T12:17:04.991Z",
4
4
  "description": "IOCs communautaires MUAD'DIB - Contribuez via PR",
5
5
  "packages": [
6
6
  {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 };