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,197 @@
1
+ const RULES = {
2
+ // AST detections
3
+ sensitive_string: {
4
+ id: 'MUADDIB-AST-001',
5
+ name: 'Sensitive String Reference',
6
+ severity: 'HIGH',
7
+ confidence: 'medium',
8
+ description: 'Reference a un chemin ou identifiant sensible (.npmrc, .ssh, tokens)',
9
+ references: [
10
+ 'https://blog.phylum.io/shai-hulud-npm-worm',
11
+ 'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack'
12
+ ],
13
+ mitre: 'T1552.001'
14
+ },
15
+ env_access: {
16
+ id: 'MUADDIB-AST-002',
17
+ name: 'Sensitive Environment Variable Access',
18
+ severity: 'HIGH',
19
+ confidence: 'high',
20
+ description: 'Acces a une variable d\'environnement sensible (GITHUB_TOKEN, NPM_TOKEN, AWS_*)',
21
+ references: [
22
+ 'https://blog.phylum.io/shai-hulud-npm-worm',
23
+ 'https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions'
24
+ ],
25
+ mitre: 'T1552.001'
26
+ },
27
+ dangerous_call_exec: {
28
+ id: 'MUADDIB-AST-003',
29
+ name: 'Dangerous Function Call',
30
+ severity: 'MEDIUM',
31
+ confidence: 'medium',
32
+ description: 'Appel a une fonction dangereuse (exec, spawn, eval, Function)',
33
+ references: [
34
+ 'https://owasp.org/www-community/attacks/Command_Injection'
35
+ ],
36
+ mitre: 'T1059'
37
+ },
38
+ dangerous_call_eval: {
39
+ id: 'MUADDIB-AST-004',
40
+ name: 'Eval Usage',
41
+ severity: 'HIGH',
42
+ confidence: 'high',
43
+ description: 'Utilisation de eval() ou new Function() - execution de code dynamique',
44
+ references: [
45
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!'
46
+ ],
47
+ mitre: 'T1059.007'
48
+ },
49
+
50
+ // Shell detections
51
+ curl_exec: {
52
+ id: 'MUADDIB-SHELL-001',
53
+ name: 'Remote Code Execution via Curl',
54
+ severity: 'CRITICAL',
55
+ confidence: 'high',
56
+ description: 'Telecharge et execute du code distant via curl | sh',
57
+ references: [
58
+ 'https://blog.phylum.io/shai-hulud-npm-worm'
59
+ ],
60
+ mitre: 'T1105'
61
+ },
62
+ reverse_shell: {
63
+ id: 'MUADDIB-SHELL-002',
64
+ name: 'Reverse Shell',
65
+ severity: 'CRITICAL',
66
+ confidence: 'high',
67
+ description: 'Tentative de connexion reverse shell',
68
+ references: [
69
+ 'https://attack.mitre.org/techniques/T1059/004/'
70
+ ],
71
+ mitre: 'T1059.004'
72
+ },
73
+ home_deletion: {
74
+ id: 'MUADDIB-SHELL-003',
75
+ name: 'Dead Man\'s Switch',
76
+ severity: 'CRITICAL',
77
+ confidence: 'high',
78
+ description: 'Suppression du repertoire home - dead man\'s switch de Shai-Hulud',
79
+ references: [
80
+ 'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack'
81
+ ],
82
+ mitre: 'T1485'
83
+ },
84
+
85
+ // Package detections
86
+ lifecycle_script: {
87
+ id: 'MUADDIB-PKG-001',
88
+ name: 'Suspicious Lifecycle Script',
89
+ severity: 'MEDIUM',
90
+ confidence: 'medium',
91
+ description: 'Script preinstall/postinstall suspect dans package.json',
92
+ references: [
93
+ 'https://blog.npmjs.org/post/141577284765/kik-left-pad-and-npm'
94
+ ],
95
+ mitre: 'T1195.002'
96
+ },
97
+
98
+ // Obfuscation detections
99
+ obfuscation_detected: {
100
+ id: 'MUADDIB-OBF-001',
101
+ name: 'Code Obfuscation Detected',
102
+ severity: 'HIGH',
103
+ confidence: 'medium',
104
+ description: 'Code fortement obfusque detecte - probablement malveillant',
105
+ references: [
106
+ 'https://blog.phylum.io/shai-hulud-npm-worm'
107
+ ],
108
+ mitre: 'T1027'
109
+ },
110
+
111
+ // Dependency detections
112
+ known_malicious_package: {
113
+ id: 'MUADDIB-DEP-001',
114
+ name: 'Known Malicious Package',
115
+ severity: 'CRITICAL',
116
+ confidence: 'high',
117
+ description: 'Package present dans la base IOC de packages malveillants connus',
118
+ references: [
119
+ 'https://socket.dev/npm/issue'
120
+ ],
121
+ mitre: 'T1195.002'
122
+ },
123
+ suspicious_file: {
124
+ id: 'MUADDIB-DEP-002',
125
+ name: 'Suspicious File in Dependency',
126
+ severity: 'CRITICAL',
127
+ confidence: 'high',
128
+ description: 'Fichier suspect detecte dans une dependance (setup_bun.js, etc.)',
129
+ references: [
130
+ 'https://blog.phylum.io/shai-hulud-npm-worm'
131
+ ],
132
+ mitre: 'T1195.002'
133
+ },
134
+ shai_hulud_marker: {
135
+ id: 'MUADDIB-DEP-003',
136
+ name: 'Shai-Hulud Marker Detected',
137
+ severity: 'CRITICAL',
138
+ confidence: 'high',
139
+ description: 'Marqueur Shai-Hulud detecte dans le code',
140
+ references: [
141
+ 'https://blog.phylum.io/shai-hulud-npm-worm',
142
+ 'https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack'
143
+ ],
144
+ mitre: 'T1195.002'
145
+ },
146
+ lifecycle_script_dependency: {
147
+ id: 'MUADDIB-DEP-004',
148
+ name: 'Lifecycle Script in Dependency',
149
+ severity: 'MEDIUM',
150
+ confidence: 'low',
151
+ description: 'Une dependance a un script preinstall/postinstall',
152
+ references: [
153
+ 'https://docs.npmjs.com/cli/v9/using-npm/scripts#life-cycle-scripts'
154
+ ],
155
+ mitre: 'T1195.002'
156
+ },
157
+
158
+ // Hash detections
159
+ known_malicious_hash: {
160
+ id: 'MUADDIB-HASH-001',
161
+ name: 'Known Malicious File Hash',
162
+ severity: 'CRITICAL',
163
+ confidence: 'high',
164
+ description: 'Hash SHA256 correspond a un fichier malveillant connu',
165
+ references: [
166
+ 'https://www.virustotal.com'
167
+ ],
168
+ mitre: 'T1195.002'
169
+ },
170
+
171
+ // Dataflow detections
172
+ suspicious_dataflow: {
173
+ id: 'MUADDIB-FLOW-001',
174
+ name: 'Suspicious Data Flow',
175
+ severity: 'CRITICAL',
176
+ confidence: 'high',
177
+ description: 'Flux de donnees suspect: lecture de credentials puis envoi reseau',
178
+ references: [
179
+ 'https://blog.phylum.io/shai-hulud-npm-worm'
180
+ ],
181
+ mitre: 'T1041'
182
+ }
183
+ };
184
+
185
+ function getRule(type) {
186
+ return RULES[type] || {
187
+ id: 'MUADDIB-UNK-001',
188
+ name: 'Unknown Threat',
189
+ severity: 'MEDIUM',
190
+ confidence: 'low',
191
+ description: 'Menace non categorisee',
192
+ references: [],
193
+ mitre: null
194
+ };
195
+ }
196
+
197
+ module.exports = { RULES, getRule };
package/src/sarif.js ADDED
@@ -0,0 +1,74 @@
1
+ const { RULES } = require('./rules/index.js');
2
+
3
+ function generateSARIF(results) {
4
+ const sarif = {
5
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
6
+ version: '2.1.0',
7
+ runs: [
8
+ {
9
+ tool: {
10
+ driver: {
11
+ name: 'MUADDIB',
12
+ version: '1.0.0',
13
+ informationUri: 'https://github.com/DNSZLSK/muad-dib',
14
+ rules: Object.values(RULES).map(rule => ({
15
+ id: rule.id,
16
+ name: rule.name,
17
+ shortDescription: { text: rule.description },
18
+ fullDescription: { text: rule.description },
19
+ helpUri: rule.references[0] || '',
20
+ properties: {
21
+ severity: rule.severity,
22
+ confidence: rule.confidence,
23
+ mitre: rule.mitre
24
+ }
25
+ }))
26
+ }
27
+ },
28
+ results: results.threats.map(threat => ({
29
+ ruleId: threat.rule_id,
30
+ level: sarifLevel(threat.severity),
31
+ message: { text: threat.message },
32
+ locations: [
33
+ {
34
+ physicalLocation: {
35
+ artifactLocation: {
36
+ uri: threat.file,
37
+ uriBaseId: '%SRCROOT%'
38
+ },
39
+ region: {
40
+ startLine: threat.line || 1
41
+ }
42
+ }
43
+ }
44
+ ],
45
+ properties: {
46
+ confidence: threat.confidence,
47
+ mitre: threat.mitre
48
+ }
49
+ }))
50
+ }
51
+ ]
52
+ };
53
+
54
+ return sarif;
55
+ }
56
+
57
+ function sarifLevel(severity) {
58
+ switch (severity) {
59
+ case 'CRITICAL': return 'error';
60
+ case 'HIGH': return 'error';
61
+ case 'MEDIUM': return 'warning';
62
+ case 'LOW': return 'note';
63
+ default: return 'note';
64
+ }
65
+ }
66
+
67
+ function saveSARIF(results, outputPath) {
68
+ const fs = require('fs');
69
+ const sarif = generateSARIF(results);
70
+ fs.writeFileSync(outputPath, JSON.stringify(sarif, null, 2));
71
+ return outputPath;
72
+ }
73
+
74
+ module.exports = { generateSARIF, saveSARIF };
@@ -0,0 +1,175 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const acorn = require('acorn');
4
+ const walk = require('acorn-walk');
5
+
6
+ const EXCLUDED_FILES = [
7
+ 'src/scanner/ast.js',
8
+ 'src/scanner/shell.js',
9
+ 'src/scanner/package.js',
10
+ 'src/ioc/feeds.js',
11
+ 'src/response/playbooks.js'
12
+ ];
13
+
14
+ const EXCLUDED_DIRS = ['test', 'node_modules', '.git', 'src'];
15
+
16
+ const DANGEROUS_CALLS = [
17
+ 'eval',
18
+ 'Function',
19
+ 'exec',
20
+ 'execSync',
21
+ 'spawn',
22
+ 'spawnSync'
23
+ ];
24
+
25
+ const SENSITIVE_STRINGS = [
26
+ '.npmrc',
27
+ '.ssh',
28
+ 'GITHUB_TOKEN',
29
+ 'NPM_TOKEN',
30
+ 'AWS_SECRET',
31
+ 'api.github.com',
32
+ 'Shai-Hulud',
33
+ 'The Second Coming',
34
+ 'Goldox-T3chs'
35
+ ];
36
+
37
+ async function analyzeAST(targetPath) {
38
+ const threats = [];
39
+ const files = findJsFiles(targetPath);
40
+
41
+ for (const file of files) {
42
+ const relativePath = path.relative(targetPath, file).replace(/\\/g, '/');
43
+
44
+ if (EXCLUDED_FILES.includes(relativePath)) {
45
+ continue;
46
+ }
47
+
48
+ const content = fs.readFileSync(file, 'utf8');
49
+ const fileThreats = analyzeFile(content, file, targetPath);
50
+ threats.push(...fileThreats);
51
+ }
52
+
53
+ return threats;
54
+ }
55
+
56
+ function analyzeFile(content, filePath, basePath) {
57
+ const threats = [];
58
+ let ast;
59
+
60
+ try {
61
+ ast = acorn.parse(content, {
62
+ ecmaVersion: 2022,
63
+ sourceType: 'module',
64
+ allowHashBang: true
65
+ });
66
+ } catch (e) {
67
+ // Fichier non parseable, peut etre obfusque
68
+ if (content.length > 1000 && content.split('\n').length < 10) {
69
+ threats.push({
70
+ type: 'possible_obfuscation',
71
+ severity: 'MEDIUM',
72
+ message: 'Fichier difficilement parseable, possiblement obfusque.',
73
+ file: path.relative(basePath, filePath)
74
+ });
75
+ }
76
+ return threats;
77
+ }
78
+
79
+ // Analyse des appels de fonction
80
+ walk.simple(ast, {
81
+ CallExpression(node) {
82
+ const callName = getCallName(node);
83
+
84
+ if (DANGEROUS_CALLS.includes(callName)) {
85
+ threats.push({
86
+ type: 'dangerous_call_' + callName.toLowerCase(),
87
+ severity: callName === 'eval' ? 'HIGH' : 'MEDIUM',
88
+ message: `Appel dangereux "${callName}" detecte.`,
89
+ file: path.relative(basePath, filePath)
90
+ });
91
+ }
92
+ },
93
+
94
+ NewExpression(node) {
95
+ if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
96
+ threats.push({
97
+ type: 'dangerous_call_function',
98
+ severity: 'HIGH',
99
+ message: 'Appel dangereux "new Function()" detecte.',
100
+ file: path.relative(basePath, filePath)
101
+ });
102
+ }
103
+ },
104
+
105
+ Literal(node) {
106
+ if (typeof node.value === 'string') {
107
+ for (const sensitive of SENSITIVE_STRINGS) {
108
+ if (node.value.includes(sensitive)) {
109
+ threats.push({
110
+ type: 'sensitive_string',
111
+ severity: 'HIGH',
112
+ message: `Reference a "${sensitive}" detectee.`,
113
+ file: path.relative(basePath, filePath)
114
+ });
115
+ }
116
+ }
117
+ }
118
+ },
119
+
120
+ MemberExpression(node) {
121
+ // Detecte process.env.XXX
122
+ if (
123
+ node.object?.object?.name === 'process' &&
124
+ node.object?.property?.name === 'env'
125
+ ) {
126
+ const envVar = node.property?.name;
127
+ if (envVar && SENSITIVE_STRINGS.some(s => envVar.includes(s.replace('.', '')))) {
128
+ threats.push({
129
+ type: 'env_access',
130
+ severity: 'HIGH',
131
+ message: `Acces a variable sensible process.env.${envVar}.`,
132
+ file: path.relative(basePath, filePath)
133
+ });
134
+ }
135
+ }
136
+ }
137
+ });
138
+
139
+ return threats;
140
+ }
141
+
142
+ function getCallName(node) {
143
+ if (node.callee.type === 'Identifier') {
144
+ return node.callee.name;
145
+ }
146
+ if (node.callee.type === 'MemberExpression' && node.callee.property) {
147
+ return node.callee.property.name;
148
+ }
149
+ return '';
150
+ }
151
+
152
+ function findJsFiles(dir) {
153
+ const results = [];
154
+
155
+ if (!fs.existsSync(dir)) return results;
156
+
157
+ const items = fs.readdirSync(dir);
158
+
159
+ for (const item of items) {
160
+ if (EXCLUDED_DIRS.includes(item)) continue;
161
+
162
+ const fullPath = path.join(dir, item);
163
+ const stat = fs.statSync(fullPath);
164
+
165
+ if (stat.isDirectory()) {
166
+ results.push(...findJsFiles(fullPath));
167
+ } else if (item.endsWith('.js')) {
168
+ results.push(fullPath);
169
+ }
170
+ }
171
+
172
+ return results;
173
+ }
174
+
175
+ module.exports = { analyzeAST };
@@ -0,0 +1,167 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const acorn = require('acorn');
4
+ const walk = require('acorn-walk');
5
+
6
+ const EXCLUDED_DIRS = ['test', 'node_modules', '.git', 'src'];
7
+
8
+ async function analyzeDataFlow(targetPath) {
9
+ const threats = [];
10
+ const files = findJsFiles(targetPath);
11
+
12
+ for (const file of files) {
13
+ const content = fs.readFileSync(file, 'utf8');
14
+ const fileThreats = analyzeFile(content, file, targetPath);
15
+ threats.push(...fileThreats);
16
+ }
17
+
18
+ return threats;
19
+ }
20
+
21
+ function analyzeFile(content, filePath, basePath) {
22
+ const threats = [];
23
+ let ast;
24
+
25
+ try {
26
+ ast = acorn.parse(content, {
27
+ ecmaVersion: 2022,
28
+ sourceType: 'module',
29
+ allowHashBang: true
30
+ });
31
+ } catch (e) {
32
+ return threats;
33
+ }
34
+
35
+ const sources = []; // Ou les donnees sensibles sont lues
36
+ const sinks = []; // Ou les donnees sont envoyees
37
+
38
+ walk.simple(ast, {
39
+ // Detecte les lectures de fichiers sensibles
40
+ CallExpression(node) {
41
+ const callName = getCallName(node);
42
+
43
+ // fs.readFileSync, fs.readFile
44
+ if (callName === 'readFileSync' || callName === 'readFile') {
45
+ const arg = node.arguments[0];
46
+ if (arg && isCredentialPath(arg, content)) {
47
+ sources.push({
48
+ type: 'credential_read',
49
+ name: callName,
50
+ line: node.loc?.start?.line
51
+ });
52
+ }
53
+ }
54
+
55
+ // Detecte les envois reseau
56
+ if (callName === 'request' || callName === 'fetch' || callName === 'post' || callName === 'get') {
57
+ sinks.push({
58
+ type: 'network_send',
59
+ name: callName,
60
+ line: node.loc?.start?.line
61
+ });
62
+ }
63
+
64
+ // exec avec curl/wget
65
+ if (callName === 'exec' || callName === 'execSync') {
66
+ const arg = node.arguments[0];
67
+ if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
68
+ if (arg.value.includes('curl') || arg.value.includes('wget')) {
69
+ sinks.push({
70
+ type: 'exec_network',
71
+ name: callName,
72
+ line: node.loc?.start?.line
73
+ });
74
+ }
75
+ }
76
+ }
77
+ },
78
+
79
+ // Detecte les acces process.env sensibles
80
+ MemberExpression(node) {
81
+ if (
82
+ node.object?.object?.name === 'process' &&
83
+ node.object?.property?.name === 'env'
84
+ ) {
85
+ const envVar = node.property?.name || '';
86
+ if (isSensitiveEnv(envVar)) {
87
+ sources.push({
88
+ type: 'env_read',
89
+ name: envVar,
90
+ line: node.loc?.start?.line
91
+ });
92
+ }
93
+ }
94
+ }
95
+ });
96
+
97
+ // Si on a des sources ET des sinks = flux suspect
98
+ if (sources.length > 0 && sinks.length > 0) {
99
+ threats.push({
100
+ type: 'suspicious_dataflow',
101
+ severity: 'CRITICAL',
102
+ message: `Flux suspect: lecture credentials (${sources.map(s => s.name).join(', ')}) + envoi reseau (${sinks.map(s => s.name).join(', ')})`,
103
+ file: path.relative(basePath, filePath)
104
+ });
105
+ }
106
+
107
+ return threats;
108
+ }
109
+
110
+ function getCallName(node) {
111
+ if (node.callee.type === 'Identifier') {
112
+ return node.callee.name;
113
+ }
114
+ if (node.callee.type === 'MemberExpression' && node.callee.property) {
115
+ return node.callee.property.name;
116
+ }
117
+ return '';
118
+ }
119
+
120
+ function isCredentialPath(arg, content) {
121
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
122
+ const val = arg.value.toLowerCase();
123
+ return val.includes('.npmrc') ||
124
+ val.includes('.ssh') ||
125
+ val.includes('.aws') ||
126
+ val.includes('.gitconfig') ||
127
+ val.includes('.env');
128
+ }
129
+ // Verifie aussi les templates strings et concatenations
130
+ if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
131
+ return content.includes('.npmrc') ||
132
+ content.includes('.ssh') ||
133
+ content.includes('.aws');
134
+ }
135
+ return false;
136
+ }
137
+
138
+ function isSensitiveEnv(name) {
139
+ const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'GITHUB', 'AWS', 'AZURE', 'GCP'];
140
+ return sensitive.some(s => name.toUpperCase().includes(s));
141
+ }
142
+
143
+ function findJsFiles(dir, results = []) {
144
+ if (!fs.existsSync(dir)) return results;
145
+
146
+ const items = fs.readdirSync(dir);
147
+
148
+ for (const item of items) {
149
+ if (EXCLUDED_DIRS.includes(item)) continue;
150
+
151
+ const fullPath = path.join(dir, item);
152
+
153
+ try {
154
+ const stat = fs.statSync(fullPath);
155
+
156
+ if (stat.isDirectory()) {
157
+ findJsFiles(fullPath, results);
158
+ } else if (item.endsWith('.js')) {
159
+ results.push(fullPath);
160
+ }
161
+ } catch (e) {}
162
+ }
163
+
164
+ return results;
165
+ }
166
+
167
+ module.exports = { analyzeDataFlow };