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.
- package/.github/workflows/scan.yml +33 -0
- package/LICENSE +21 -0
- package/MUADDIBLOGO.png +0 -0
- package/README.md +218 -0
- package/action/action.yml +28 -0
- package/bin/muaddib.js +84 -0
- package/data/iocs.json +38 -0
- package/docs/threat-model.md +116 -0
- package/iocs/hashes.yaml +220 -0
- package/iocs/packages.yaml +265 -0
- package/package.json +43 -0
- package/results.sarif +379 -0
- package/src/index.js +142 -0
- package/src/ioc/feeds.js +42 -0
- package/src/ioc/updater.js +244 -0
- package/src/ioc/yaml-loader.js +96 -0
- package/src/report.js +152 -0
- package/src/response/playbooks.js +115 -0
- package/src/rules/index.js +197 -0
- package/src/sarif.js +74 -0
- package/src/scanner/ast.js +175 -0
- package/src/scanner/dataflow.js +167 -0
- package/src/scanner/dependencies.js +110 -0
- package/src/scanner/hash.js +68 -0
- package/src/scanner/obfuscation.js +99 -0
- package/src/scanner/package.js +60 -0
- package/src/scanner/shell.js +63 -0
- package/src/watch.js +37 -0
- package/test/samples/malicious.js +20 -0
- package/tests/run-tests.js +363 -0
- package/tests/samples/ast/malicious.js +20 -0
- package/tests/samples/clean/safe.js +14 -0
- package/tests/samples/dataflow/exfiltration.js +20 -0
- package/tests/samples/edge/empty/empty.js +0 -0
- package/tests/samples/edge/invalid-syntax/broken.js +5 -0
- package/tests/samples/edge/large-file/large.js +6 -0
- package/tests/samples/edge/non-js/readme.txt +3 -0
- package/tests/samples/markers/shai-hulud.js +10 -0
- package/tests/samples/obfuscation/obfuscated.js +1 -0
- package/tests/samples/package/package.json +9 -0
- 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 };
|