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.
@@ -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
- if (node.callee.type === 'MemberExpression') {
95
- const obj = node.callee.object;
96
- const prop = node.callee.property;
97
- if (obj?.type === 'Identifier' && obj.name === 'os' && prop?.type === 'Identifier') {
98
- if (['hostname', 'networkInterfaces', 'userInfo', 'cpus', 'totalmem', 'platform', 'arch', 'homedir'].includes(prop.name)) {
99
- sources.push({
100
- type: 'fingerprint_read',
101
- name: `os.${prop.name}`,
102
- line: node.loc?.start?.line
103
- });
104
- }
105
- }
106
- }
107
-
108
- // fs.readdirSync as credential source when reading sensitive directories
109
- if (node.callee.type === 'MemberExpression') {
110
- const prop = node.callee.property;
111
- if (prop?.type === 'Identifier' && (prop.name === 'readdirSync' || prop.name === 'readdir')) {
112
- const arg = node.arguments[0];
113
- if (arg && isCredentialPath(arg, sensitivePathVars)) {
114
- sources.push({
115
- type: 'credential_read',
116
- name: prop.name,
117
- line: node.loc?.start?.line
118
- });
119
- }
120
- }
121
- }
122
-
123
- // MemberExpression network sinks: http.request, https.get, dns.resolve, net.connect, etc.
124
- if (node.callee.type === 'MemberExpression') {
125
- const obj = node.callee.object;
126
- const prop = node.callee.property;
127
- if (obj.type === 'Identifier' && prop.type === 'Identifier') {
128
- // DNS resolution as exfiltration sink
129
- if (obj.name === 'dns' && ['resolve', 'lookup', 'resolve4', 'resolve6', 'resolveTxt'].includes(prop.name)) {
130
- sinks.push({ type: 'network_send', name: `dns.${prop.name}`, line: node.loc?.start?.line });
131
- }
132
- // HTTP/HTTPS request/get as network sink
133
- if ((obj.name === 'http' || obj.name === 'https') && ['request', 'get'].includes(prop.name)) {
134
- sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
135
- }
136
- // net.connect / net.createConnection / tls.connect as network sink
137
- if ((obj.name === 'net' || obj.name === 'tls') && ['connect', 'createConnection'].includes(prop.name)) {
138
- sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
139
- }
140
- // Instance socket.connect(port, host) when file imports net/tls
141
- if (hasRawSocketModule && prop.name === 'connect' && node.arguments.length >= 2) {
142
- sinks.push({ type: 'network_send', name: 'socket.connect', line: node.loc?.start?.line });
143
- }
144
- }
145
- }
146
-
147
- // Detect writeFileSync/writeFile on sensitive paths cache poisoning / credential tampering
148
- if (node.callee.type === 'MemberExpression') {
149
- const prop = node.callee.property;
150
- if (prop?.type === 'Identifier' && (prop.name === 'writeFileSync' || prop.name === 'writeFile')) {
151
- const arg = node.arguments[0];
152
- if (arg && isCredentialPath(arg, sensitivePathVars)) {
153
- sinks.push({
154
- type: 'file_tamper',
155
- name: prop.name,
156
- line: node.loc?.start?.line
157
- });
158
- }
159
- }
160
- }
161
-
162
- // Track eval calls for staged payload detection
163
- if (callName === 'eval') {
164
- sinks.push({
165
- type: 'eval_exec',
166
- name: 'eval',
167
- line: node.loc?.start?.line
168
- });
169
- }
170
- },
171
-
172
- MemberExpression(node) {
173
- if (
174
- node.object?.object?.name === 'process' &&
175
- node.object?.property?.name === 'env'
176
- ) {
177
- // Dynamic bracket access: process.env[variable]
178
- if (node.computed) {
179
- sources.push({
180
- type: 'env_read',
181
- name: 'process.env[dynamic]',
182
- line: node.loc?.start?.line
183
- });
184
- return;
185
- }
186
- const envVar = node.property?.name || '';
187
- if (isSensitiveEnv(envVar)) {
188
- sources.push({
189
- type: 'env_read',
190
- name: envVar,
191
- line: node.loc?.start?.line
192
- });
193
- }
194
- }
195
-
196
- // Detect property access to secret key material
197
- const propName = node.property?.type === 'Identifier' ? node.property.name :
198
- (node.property?.type === 'Literal' ? node.property.value : null);
199
- if (propName && ['secretKey', '_secretKey', 'privateKey', '_privateKey', 'mnemonic', 'seedPhrase'].includes(propName)) {
200
- sources.push({
201
- type: 'credential_read',
202
- name: propName,
203
- line: node.loc?.start?.line
204
- });
205
- }
206
- }
207
- });
208
-
209
- // Detect staged payload: network fetch + eval in same file (no credential source needed)
210
- const hasNetworkSink = sinks.some(s => s.type === 'network_send' || s.type === 'exec_network');
211
- const hasEvalSink = sinks.some(s => s.type === 'eval_exec');
212
- if (hasNetworkSink && hasEvalSink) {
213
- threats.push({
214
- type: 'staged_payload',
215
- severity: 'CRITICAL',
216
- message: 'Network fetch + eval() in same file (staged payload execution).',
217
- file: path.relative(basePath, filePath)
218
- });
219
- }
220
-
221
- // Separate exfiltration sinks from file tampering sinks
222
- const exfilSinks = sinks.filter(s => s.type !== 'file_tamper');
223
- const fileTamperSinks = sinks.filter(s => s.type === 'file_tamper');
224
-
225
- if (sources.length > 0 && exfilSinks.length > 0) {
226
- // Determine severity by scope proximity: if source and sink are < 50 lines apart -> CRITICAL, else HIGH
227
- let severity = 'HIGH';
228
- for (const src of sources) {
229
- for (const sink of exfilSinks) {
230
- if (src.line && sink.line && Math.abs(src.line - sink.line) < 50) {
231
- severity = 'CRITICAL';
232
- break;
233
- }
234
- }
235
- if (severity === 'CRITICAL') break;
236
- }
237
-
238
- threats.push({
239
- type: 'suspicious_dataflow',
240
- severity: severity,
241
- message: `Suspicious flow: credentials read (${sources.map(s => s.name).join(', ')}) + network send (${exfilSinks.map(s => s.name).join(', ')})`,
242
- file: path.relative(basePath, filePath)
243
- });
244
- }
245
-
246
- // Detect cache poisoning: credential source + write to sensitive path
247
- if (sources.length > 0 && fileTamperSinks.length > 0) {
248
- threats.push({
249
- type: 'credential_tampering',
250
- severity: 'CRITICAL',
251
- message: `Cache poisoning: sensitive data access (${sources.map(s => s.name).join(', ')}) + write to sensitive path (${fileTamperSinks.map(s => s.name).join(', ')})`,
252
- file: path.relative(basePath, filePath)
253
- });
254
- }
255
-
256
- return threats;
257
- }
258
-
259
- const SENSITIVE_PATH_PATTERNS = [
260
- '.npmrc', '.ssh', '.aws', '.gitconfig', '.env',
261
- '/etc/passwd', '/etc/shadow', '/etc/hosts',
262
- '.ethereum', '.electrum', '.config/solana', '.exodus',
263
- '.atomic', '.metamask', '.ledger-live', '.trezor',
264
- '.bitcoin', '.monero', '.gnupg',
265
- '_cacache', '.cache/yarn', '.cache/pip',
266
- 'discord', 'leveldb'
267
- ];
268
-
269
- function isSensitivePath(val) {
270
- const lower = val.toLowerCase();
271
- return SENSITIVE_PATH_PATTERNS.some(p => lower.includes(p));
272
- }
273
-
274
- /**
275
- * Checks if an expression tree contains any sensitive path literal.
276
- * Used to determine if a variable assignment should be tracked.
277
- */
278
- function containsSensitiveLiteral(node) {
279
- if (!node || typeof node !== 'object') return false;
280
- if (node.type === 'Literal' && typeof node.value === 'string') {
281
- return isSensitivePath(node.value);
282
- }
283
- if (node.type === 'TemplateLiteral') {
284
- const quasiText = (node.quasis || []).map(q => q.value.raw).join('');
285
- return isSensitivePath(quasiText);
286
- }
287
- if (node.type === 'BinaryExpression' && node.operator === '+') {
288
- return containsSensitiveLiteral(node.left) || containsSensitiveLiteral(node.right);
289
- }
290
- if (node.type === 'CallExpression' && node.arguments) {
291
- return node.arguments.some(a => containsSensitiveLiteral(a));
292
- }
293
- if (node.type === 'ObjectExpression' && node.properties) {
294
- return node.properties.some(p => p.value && containsSensitiveLiteral(p.value));
295
- }
296
- return false;
297
- }
298
-
299
- function isCredentialPath(arg, sensitivePathVars) {
300
- if (arg.type === 'Literal' && typeof arg.value === 'string') {
301
- return isSensitivePath(arg.value);
302
- }
303
- if (arg.type === 'TemplateLiteral') {
304
- const quasiText = (arg.quasis || []).map(q => q.value.raw).join('');
305
- return isSensitivePath(quasiText);
306
- }
307
- // Handle string concatenation: homedir() + '/.npmrc'
308
- if (arg.type === 'BinaryExpression' && arg.operator === '+') {
309
- return isCredentialPath(arg.left, sensitivePathVars) || isCredentialPath(arg.right, sensitivePathVars);
310
- }
311
- // Handle variable references: fs.readFileSync(npmrcPath) where npmrcPath was assigned a sensitive path
312
- if (arg.type === 'Identifier' && sensitivePathVars && sensitivePathVars.has(arg.name)) {
313
- return true;
314
- }
315
- // Handle property access on tracked objects: _0x.a where _0x is tracked as sensitive
316
- if (arg.type === 'MemberExpression' && arg.object?.type === 'Identifier' &&
317
- sensitivePathVars && sensitivePathVars.has(arg.object.name)) {
318
- return true;
319
- }
320
- // Handle path.join(dir, '.npmrc') or path.join(sshDir, 'id_rsa') where sshDir is tracked
321
- if (arg.type === 'CallExpression' && arg.callee.type === 'MemberExpression') {
322
- const obj = arg.callee.object;
323
- const prop = arg.callee.property;
324
- if (obj.type === 'Identifier' && obj.name === 'path' &&
325
- prop.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
326
- return arg.arguments.some(a => isCredentialPath(a, sensitivePathVars));
327
- }
328
- }
329
- return false;
330
- }
331
-
332
- function isSensitiveEnv(name) {
333
- const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
334
- return sensitive.some(s => name.toUpperCase().includes(s));
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 };