muaddib-scanner 2.3.1 → 2.3.2
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/README.md +17 -16
- package/package.json +1 -1
- package/src/commands/evaluate.js +1 -1
- package/src/scanner/dataflow.js +347 -336
- package/src/scanner/entropy.js +246 -242
- package/src/scanner/obfuscation.js +2 -1
- package/src/scanner/package.js +166 -161
- package/src/scoring.js +18 -0
package/src/scanner/dataflow.js
CHANGED
|
@@ -1,337 +1,348 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const acorn = require('acorn');
|
|
4
|
-
const walk = require('acorn-walk');
|
|
5
|
-
const { getCallName } = require('../utils.js');
|
|
6
|
-
const { ACORN_OPTIONS } = require('../shared/constants.js');
|
|
7
|
-
const { analyzeWithDeobfuscation } = require('../shared/analyze-helper.js');
|
|
8
|
-
|
|
9
|
-
async function analyzeDataFlow(targetPath, options = {}) {
|
|
10
|
-
return analyzeWithDeobfuscation(targetPath, analyzeFile, {
|
|
11
|
-
deobfuscate: options.deobfuscate
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function analyzeFile(content, filePath, basePath) {
|
|
16
|
-
const threats = [];
|
|
17
|
-
let ast;
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
ast = acorn.parse(content, { ...ACORN_OPTIONS, locations: true });
|
|
21
|
-
} catch {
|
|
22
|
-
return threats;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const sources = [];
|
|
26
|
-
const sinks = [];
|
|
27
|
-
|
|
28
|
-
// Pre-scan: detect raw socket module import (net/tls) for instance .connect() detection
|
|
29
|
-
const hasRawSocketModule = /require\s*\(\s*['"](?:net|tls)['"]\s*\)/.test(content);
|
|
30
|
-
|
|
31
|
-
// Track variables assigned from sensitive path expressions
|
|
32
|
-
const sensitivePathVars = new Set();
|
|
33
|
-
|
|
34
|
-
walk.simple(ast, {
|
|
35
|
-
VariableDeclarator(node) {
|
|
36
|
-
if (node.id?.type === 'Identifier' && node.init) {
|
|
37
|
-
if (containsSensitiveLiteral(node.init)) {
|
|
38
|
-
sensitivePathVars.add(node.id.name);
|
|
39
|
-
}
|
|
40
|
-
// Propagate sensitive vars through path.join/resolve
|
|
41
|
-
if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
|
|
42
|
-
const obj = node.init.callee.object;
|
|
43
|
-
const prop = node.init.callee.property;
|
|
44
|
-
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
45
|
-
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
46
|
-
if (node.init.arguments.some(a =>
|
|
47
|
-
(a.type === 'Identifier' && sensitivePathVars.has(a.name)) ||
|
|
48
|
-
(a.type === 'MemberExpression' && a.object?.type === 'Identifier' && sensitivePathVars.has(a.object.name))
|
|
49
|
-
)) {
|
|
50
|
-
sensitivePathVars.add(node.id.name);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
CallExpression(node) {
|
|
58
|
-
const callName = getCallName(node);
|
|
59
|
-
|
|
60
|
-
if (callName === 'readFileSync' || callName === 'readFile' ||
|
|
61
|
-
callName === 'fs.readFileSync' || callName === 'fs.readFile') {
|
|
62
|
-
const arg = node.arguments[0];
|
|
63
|
-
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
64
|
-
sources.push({
|
|
65
|
-
type: 'credential_read',
|
|
66
|
-
name: callName,
|
|
67
|
-
line: node.loc?.start?.line
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (callName === 'request' || callName === 'fetch' || callName === 'post' || callName === 'get') {
|
|
73
|
-
sinks.push({
|
|
74
|
-
type: 'network_send',
|
|
75
|
-
name: callName,
|
|
76
|
-
line: node.loc?.start?.line
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (callName === 'exec' || callName === 'execSync') {
|
|
81
|
-
const arg = node.arguments[0];
|
|
82
|
-
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
83
|
-
if (arg.value.includes('curl') || arg.value.includes('wget')) {
|
|
84
|
-
sinks.push({
|
|
85
|
-
type: 'exec_network',
|
|
86
|
-
name: callName,
|
|
87
|
-
line: node.loc?.start?.line
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// os.hostname(), os.networkInterfaces(), os.userInfo() as fingerprint sources
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (node
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const acorn = require('acorn');
|
|
4
|
+
const walk = require('acorn-walk');
|
|
5
|
+
const { getCallName } = require('../utils.js');
|
|
6
|
+
const { ACORN_OPTIONS } = require('../shared/constants.js');
|
|
7
|
+
const { analyzeWithDeobfuscation } = require('../shared/analyze-helper.js');
|
|
8
|
+
|
|
9
|
+
async function analyzeDataFlow(targetPath, options = {}) {
|
|
10
|
+
return analyzeWithDeobfuscation(targetPath, analyzeFile, {
|
|
11
|
+
deobfuscate: options.deobfuscate
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function analyzeFile(content, filePath, basePath) {
|
|
16
|
+
const threats = [];
|
|
17
|
+
let ast;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
ast = acorn.parse(content, { ...ACORN_OPTIONS, locations: true });
|
|
21
|
+
} catch {
|
|
22
|
+
return threats;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sources = [];
|
|
26
|
+
const sinks = [];
|
|
27
|
+
|
|
28
|
+
// Pre-scan: detect raw socket module import (net/tls) for instance .connect() detection
|
|
29
|
+
const hasRawSocketModule = /require\s*\(\s*['"](?:net|tls)['"]\s*\)/.test(content);
|
|
30
|
+
|
|
31
|
+
// Track variables assigned from sensitive path expressions
|
|
32
|
+
const sensitivePathVars = new Set();
|
|
33
|
+
|
|
34
|
+
walk.simple(ast, {
|
|
35
|
+
VariableDeclarator(node) {
|
|
36
|
+
if (node.id?.type === 'Identifier' && node.init) {
|
|
37
|
+
if (containsSensitiveLiteral(node.init)) {
|
|
38
|
+
sensitivePathVars.add(node.id.name);
|
|
39
|
+
}
|
|
40
|
+
// Propagate sensitive vars through path.join/resolve
|
|
41
|
+
if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
|
|
42
|
+
const obj = node.init.callee.object;
|
|
43
|
+
const prop = node.init.callee.property;
|
|
44
|
+
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
45
|
+
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
46
|
+
if (node.init.arguments.some(a =>
|
|
47
|
+
(a.type === 'Identifier' && sensitivePathVars.has(a.name)) ||
|
|
48
|
+
(a.type === 'MemberExpression' && a.object?.type === 'Identifier' && sensitivePathVars.has(a.object.name))
|
|
49
|
+
)) {
|
|
50
|
+
sensitivePathVars.add(node.id.name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
CallExpression(node) {
|
|
58
|
+
const callName = getCallName(node);
|
|
59
|
+
|
|
60
|
+
if (callName === 'readFileSync' || callName === 'readFile' ||
|
|
61
|
+
callName === 'fs.readFileSync' || callName === 'fs.readFile') {
|
|
62
|
+
const arg = node.arguments[0];
|
|
63
|
+
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
64
|
+
sources.push({
|
|
65
|
+
type: 'credential_read',
|
|
66
|
+
name: callName,
|
|
67
|
+
line: node.loc?.start?.line
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (callName === 'request' || callName === 'fetch' || callName === 'post' || callName === 'get') {
|
|
73
|
+
sinks.push({
|
|
74
|
+
type: 'network_send',
|
|
75
|
+
name: callName,
|
|
76
|
+
line: node.loc?.start?.line
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (callName === 'exec' || callName === 'execSync') {
|
|
81
|
+
const arg = node.arguments[0];
|
|
82
|
+
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
83
|
+
if (arg.value.includes('curl') || arg.value.includes('wget')) {
|
|
84
|
+
sinks.push({
|
|
85
|
+
type: 'exec_network',
|
|
86
|
+
name: callName,
|
|
87
|
+
line: node.loc?.start?.line
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// os.hostname(), os.networkInterfaces(), os.userInfo(), os.homedir() as fingerprint sources
|
|
94
|
+
// os.platform(), os.arch() as telemetry sources (lower severity)
|
|
95
|
+
if (node.callee.type === 'MemberExpression') {
|
|
96
|
+
const obj = node.callee.object;
|
|
97
|
+
const prop = node.callee.property;
|
|
98
|
+
if (obj?.type === 'Identifier' && obj.name === 'os' && prop?.type === 'Identifier') {
|
|
99
|
+
if (['hostname', 'networkInterfaces', 'userInfo', 'homedir'].includes(prop.name)) {
|
|
100
|
+
sources.push({
|
|
101
|
+
type: 'fingerprint_read',
|
|
102
|
+
name: `os.${prop.name}`,
|
|
103
|
+
line: node.loc?.start?.line
|
|
104
|
+
});
|
|
105
|
+
} else if (['platform', 'arch'].includes(prop.name)) {
|
|
106
|
+
sources.push({
|
|
107
|
+
type: 'telemetry_read',
|
|
108
|
+
name: `os.${prop.name}`,
|
|
109
|
+
line: node.loc?.start?.line
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// fs.readdirSync as credential source when reading sensitive directories
|
|
116
|
+
if (node.callee.type === 'MemberExpression') {
|
|
117
|
+
const prop = node.callee.property;
|
|
118
|
+
if (prop?.type === 'Identifier' && (prop.name === 'readdirSync' || prop.name === 'readdir')) {
|
|
119
|
+
const arg = node.arguments[0];
|
|
120
|
+
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
121
|
+
sources.push({
|
|
122
|
+
type: 'credential_read',
|
|
123
|
+
name: prop.name,
|
|
124
|
+
line: node.loc?.start?.line
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MemberExpression network sinks: http.request, https.get, dns.resolve, net.connect, etc.
|
|
131
|
+
if (node.callee.type === 'MemberExpression') {
|
|
132
|
+
const obj = node.callee.object;
|
|
133
|
+
const prop = node.callee.property;
|
|
134
|
+
if (obj.type === 'Identifier' && prop.type === 'Identifier') {
|
|
135
|
+
// DNS resolution as exfiltration sink
|
|
136
|
+
if (obj.name === 'dns' && ['resolve', 'lookup', 'resolve4', 'resolve6', 'resolveTxt'].includes(prop.name)) {
|
|
137
|
+
sinks.push({ type: 'network_send', name: `dns.${prop.name}`, line: node.loc?.start?.line });
|
|
138
|
+
}
|
|
139
|
+
// HTTP/HTTPS request/get as network sink
|
|
140
|
+
if ((obj.name === 'http' || obj.name === 'https') && ['request', 'get'].includes(prop.name)) {
|
|
141
|
+
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
142
|
+
}
|
|
143
|
+
// net.connect / net.createConnection / tls.connect as network sink
|
|
144
|
+
if ((obj.name === 'net' || obj.name === 'tls') && ['connect', 'createConnection'].includes(prop.name)) {
|
|
145
|
+
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
146
|
+
}
|
|
147
|
+
// Instance socket.connect(port, host) when file imports net/tls
|
|
148
|
+
if (hasRawSocketModule && prop.name === 'connect' && node.arguments.length >= 2) {
|
|
149
|
+
sinks.push({ type: 'network_send', name: 'socket.connect', line: node.loc?.start?.line });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Detect writeFileSync/writeFile on sensitive paths → cache poisoning / credential tampering
|
|
155
|
+
if (node.callee.type === 'MemberExpression') {
|
|
156
|
+
const prop = node.callee.property;
|
|
157
|
+
if (prop?.type === 'Identifier' && (prop.name === 'writeFileSync' || prop.name === 'writeFile')) {
|
|
158
|
+
const arg = node.arguments[0];
|
|
159
|
+
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
160
|
+
sinks.push({
|
|
161
|
+
type: 'file_tamper',
|
|
162
|
+
name: prop.name,
|
|
163
|
+
line: node.loc?.start?.line
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Track eval calls for staged payload detection
|
|
170
|
+
if (callName === 'eval') {
|
|
171
|
+
sinks.push({
|
|
172
|
+
type: 'eval_exec',
|
|
173
|
+
name: 'eval',
|
|
174
|
+
line: node.loc?.start?.line
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
MemberExpression(node) {
|
|
180
|
+
if (
|
|
181
|
+
node.object?.object?.name === 'process' &&
|
|
182
|
+
node.object?.property?.name === 'env'
|
|
183
|
+
) {
|
|
184
|
+
// Dynamic bracket access: process.env[variable]
|
|
185
|
+
if (node.computed) {
|
|
186
|
+
sources.push({
|
|
187
|
+
type: 'env_read',
|
|
188
|
+
name: 'process.env[dynamic]',
|
|
189
|
+
line: node.loc?.start?.line
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const envVar = node.property?.name || '';
|
|
194
|
+
if (isSensitiveEnv(envVar)) {
|
|
195
|
+
sources.push({
|
|
196
|
+
type: 'env_read',
|
|
197
|
+
name: envVar,
|
|
198
|
+
line: node.loc?.start?.line
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Detect property access to secret key material
|
|
204
|
+
const propName = node.property?.type === 'Identifier' ? node.property.name :
|
|
205
|
+
(node.property?.type === 'Literal' ? node.property.value : null);
|
|
206
|
+
if (propName && ['secretKey', '_secretKey', 'privateKey', '_privateKey', 'mnemonic', 'seedPhrase'].includes(propName)) {
|
|
207
|
+
sources.push({
|
|
208
|
+
type: 'credential_read',
|
|
209
|
+
name: propName,
|
|
210
|
+
line: node.loc?.start?.line
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Detect staged payload: network fetch + eval in same file (no credential source needed)
|
|
217
|
+
const hasNetworkSink = sinks.some(s => s.type === 'network_send' || s.type === 'exec_network');
|
|
218
|
+
const hasEvalSink = sinks.some(s => s.type === 'eval_exec');
|
|
219
|
+
if (hasNetworkSink && hasEvalSink) {
|
|
220
|
+
threats.push({
|
|
221
|
+
type: 'staged_payload',
|
|
222
|
+
severity: 'CRITICAL',
|
|
223
|
+
message: 'Network fetch + eval() in same file (staged payload execution).',
|
|
224
|
+
file: path.relative(basePath, filePath)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Separate exfiltration sinks from file tampering sinks
|
|
229
|
+
const exfilSinks = sinks.filter(s => s.type !== 'file_tamper');
|
|
230
|
+
const fileTamperSinks = sinks.filter(s => s.type === 'file_tamper');
|
|
231
|
+
|
|
232
|
+
if (sources.length > 0 && exfilSinks.length > 0) {
|
|
233
|
+
// Determine severity by scope proximity: if source and sink are < 50 lines apart -> CRITICAL, else HIGH
|
|
234
|
+
let severity = 'HIGH';
|
|
235
|
+
for (const src of sources) {
|
|
236
|
+
for (const sink of exfilSinks) {
|
|
237
|
+
if (src.line && sink.line && Math.abs(src.line - sink.line) < 50) {
|
|
238
|
+
severity = 'CRITICAL';
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (severity === 'CRITICAL') break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Downgrade: if ALL sources are pure telemetry (os.platform, os.arch), cap at HIGH
|
|
246
|
+
const allTelemetryOnly = sources.every(s => s.type === 'telemetry_read');
|
|
247
|
+
if (allTelemetryOnly && severity === 'CRITICAL') severity = 'HIGH';
|
|
248
|
+
|
|
249
|
+
threats.push({
|
|
250
|
+
type: 'suspicious_dataflow',
|
|
251
|
+
severity: severity,
|
|
252
|
+
message: `Suspicious flow: credentials read (${sources.map(s => s.name).join(', ')}) + network send (${exfilSinks.map(s => s.name).join(', ')})`,
|
|
253
|
+
file: path.relative(basePath, filePath)
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Detect cache poisoning: credential source + write to sensitive path
|
|
258
|
+
if (sources.length > 0 && fileTamperSinks.length > 0) {
|
|
259
|
+
threats.push({
|
|
260
|
+
type: 'credential_tampering',
|
|
261
|
+
severity: 'CRITICAL',
|
|
262
|
+
message: `Cache poisoning: sensitive data access (${sources.map(s => s.name).join(', ')}) + write to sensitive path (${fileTamperSinks.map(s => s.name).join(', ')})`,
|
|
263
|
+
file: path.relative(basePath, filePath)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return threats;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
271
|
+
'.npmrc', '.ssh', '.aws', '.gitconfig', '.env',
|
|
272
|
+
'/etc/passwd', '/etc/shadow', '/etc/hosts',
|
|
273
|
+
'.ethereum', '.electrum', '.config/solana', '.exodus',
|
|
274
|
+
'.atomic', '.metamask', '.ledger-live', '.trezor',
|
|
275
|
+
'.bitcoin', '.monero', '.gnupg',
|
|
276
|
+
'_cacache', '.cache/yarn', '.cache/pip',
|
|
277
|
+
'discord', 'leveldb'
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
function isSensitivePath(val) {
|
|
281
|
+
const lower = val.toLowerCase();
|
|
282
|
+
return SENSITIVE_PATH_PATTERNS.some(p => lower.includes(p));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Checks if an expression tree contains any sensitive path literal.
|
|
287
|
+
* Used to determine if a variable assignment should be tracked.
|
|
288
|
+
*/
|
|
289
|
+
function containsSensitiveLiteral(node) {
|
|
290
|
+
if (!node || typeof node !== 'object') return false;
|
|
291
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
292
|
+
return isSensitivePath(node.value);
|
|
293
|
+
}
|
|
294
|
+
if (node.type === 'TemplateLiteral') {
|
|
295
|
+
const quasiText = (node.quasis || []).map(q => q.value.raw).join('');
|
|
296
|
+
return isSensitivePath(quasiText);
|
|
297
|
+
}
|
|
298
|
+
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
299
|
+
return containsSensitiveLiteral(node.left) || containsSensitiveLiteral(node.right);
|
|
300
|
+
}
|
|
301
|
+
if (node.type === 'CallExpression' && node.arguments) {
|
|
302
|
+
return node.arguments.some(a => containsSensitiveLiteral(a));
|
|
303
|
+
}
|
|
304
|
+
if (node.type === 'ObjectExpression' && node.properties) {
|
|
305
|
+
return node.properties.some(p => p.value && containsSensitiveLiteral(p.value));
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isCredentialPath(arg, sensitivePathVars) {
|
|
311
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
312
|
+
return isSensitivePath(arg.value);
|
|
313
|
+
}
|
|
314
|
+
if (arg.type === 'TemplateLiteral') {
|
|
315
|
+
const quasiText = (arg.quasis || []).map(q => q.value.raw).join('');
|
|
316
|
+
return isSensitivePath(quasiText);
|
|
317
|
+
}
|
|
318
|
+
// Handle string concatenation: homedir() + '/.npmrc'
|
|
319
|
+
if (arg.type === 'BinaryExpression' && arg.operator === '+') {
|
|
320
|
+
return isCredentialPath(arg.left, sensitivePathVars) || isCredentialPath(arg.right, sensitivePathVars);
|
|
321
|
+
}
|
|
322
|
+
// Handle variable references: fs.readFileSync(npmrcPath) where npmrcPath was assigned a sensitive path
|
|
323
|
+
if (arg.type === 'Identifier' && sensitivePathVars && sensitivePathVars.has(arg.name)) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
// Handle property access on tracked objects: _0x.a where _0x is tracked as sensitive
|
|
327
|
+
if (arg.type === 'MemberExpression' && arg.object?.type === 'Identifier' &&
|
|
328
|
+
sensitivePathVars && sensitivePathVars.has(arg.object.name)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
// Handle path.join(dir, '.npmrc') or path.join(sshDir, 'id_rsa') where sshDir is tracked
|
|
332
|
+
if (arg.type === 'CallExpression' && arg.callee.type === 'MemberExpression') {
|
|
333
|
+
const obj = arg.callee.object;
|
|
334
|
+
const prop = arg.callee.property;
|
|
335
|
+
if (obj.type === 'Identifier' && obj.name === 'path' &&
|
|
336
|
+
prop.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
337
|
+
return arg.arguments.some(a => isCredentialPath(a, sensitivePathVars));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isSensitiveEnv(name) {
|
|
344
|
+
const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
|
|
345
|
+
return sensitive.some(s => name.toUpperCase().includes(s));
|
|
346
|
+
}
|
|
347
|
+
|
|
337
348
|
module.exports = { analyzeDataFlow };
|