muaddib-scanner 2.2.0 → 2.2.1
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/datasets/holdout-v2/conditional-os-payload/index.js +36 -0
- package/datasets/holdout-v2/conditional-os-payload/package.json +6 -0
- package/datasets/holdout-v2/env-var-reconstruction/index.js +21 -0
- package/datasets/holdout-v2/env-var-reconstruction/package.json +6 -0
- package/datasets/holdout-v2/github-workflow-inject/index.js +36 -0
- package/datasets/holdout-v2/github-workflow-inject/package.json +6 -0
- package/datasets/holdout-v2/homedir-ssh-key-steal/index.js +29 -0
- package/datasets/holdout-v2/homedir-ssh-key-steal/package.json +6 -0
- package/datasets/holdout-v2/npm-cache-poison/index.js +38 -0
- package/datasets/holdout-v2/npm-cache-poison/package.json +6 -0
- package/datasets/holdout-v2/npm-lifecycle-preinstall-curl/package.json +8 -0
- package/datasets/holdout-v2/process-env-proxy-getter/index.js +35 -0
- package/datasets/holdout-v2/process-env-proxy-getter/package.json +6 -0
- package/datasets/holdout-v2/readable-stream-hijack/index.js +44 -0
- package/datasets/holdout-v2/readable-stream-hijack/package.json +6 -0
- package/datasets/holdout-v2/setTimeout-chain/index.js +50 -0
- package/datasets/holdout-v2/setTimeout-chain/package.json +6 -0
- package/datasets/holdout-v2/wasm-loader/index.js +46 -0
- package/datasets/holdout-v2/wasm-loader/package.json +6 -0
- package/metrics/v2.1.5.json +752 -752
- package/metrics/v2.2.0.json +752 -752
- package/package.json +3 -3
- package/src/response/playbooks.js +15 -0
- package/src/rules/index.js +39 -1
- package/src/scanner/ast.js +93 -3
- package/src/scanner/dataflow.js +54 -4
- package/src/scanner/package.js +13 -0
- package/iocs.json.gz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm & PyPI/Python",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"node": ">=18.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@inquirer/prompts": "8.2.
|
|
46
|
+
"@inquirer/prompts": "8.2.1",
|
|
47
47
|
"acorn": "8.15.0",
|
|
48
48
|
"acorn-walk": "8.3.4",
|
|
49
49
|
"adm-zip": "^0.5.16",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"yargs": "18.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@eslint/js": "
|
|
55
|
+
"@eslint/js": "10.0.1",
|
|
56
56
|
"eslint": "10.0.0",
|
|
57
57
|
"eslint-plugin-security": "^3.0.1",
|
|
58
58
|
"globals": "17.3.0"
|
|
@@ -293,6 +293,21 @@ const PLAYBOOKS = {
|
|
|
293
293
|
'CRITIQUE: Le package a tente de voler des credentials (honey tokens). Comportement malveillant confirme. ' +
|
|
294
294
|
'NE PAS installer. Signaler immediatement sur npm/PyPI. ' +
|
|
295
295
|
'Si deja installe: considerer la machine compromise, regenerer TOUS les secrets.',
|
|
296
|
+
|
|
297
|
+
env_charcode_reconstruction:
|
|
298
|
+
'Obfuscation detectee: le nom de la variable d\'environnement est reconstruit dynamiquement via fromCharCode ' +
|
|
299
|
+
'pour eviter la detection statique. Technique de vol de GITHUB_TOKEN, NPM_TOKEN, etc. ' +
|
|
300
|
+
'Verifier quelles variables sont accedees et si elles sont exfiltrees.',
|
|
301
|
+
|
|
302
|
+
lifecycle_shell_pipe:
|
|
303
|
+
'CRITIQUE: Le script lifecycle (preinstall/postinstall) pipe du code distant vers un shell (curl | sh). ' +
|
|
304
|
+
'NE PAS installer. Ceci execute du code arbitraire a l\'installation. ' +
|
|
305
|
+
'Si deja installe: considerer la machine compromise. Auditer les modifications systeme.',
|
|
306
|
+
|
|
307
|
+
credential_tampering:
|
|
308
|
+
'CRITIQUE: Ecriture detectee dans un cache sensible (npm _cacache, yarn, pip). ' +
|
|
309
|
+
'Possible cache poisoning: injection de code malveillant dans des packages caches. ' +
|
|
310
|
+
'Nettoyer le cache: npm cache clean --force. Reinstaller les dependances depuis zero.',
|
|
296
311
|
};
|
|
297
312
|
|
|
298
313
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -500,7 +500,7 @@ const RULES = {
|
|
|
500
500
|
workflow_write: {
|
|
501
501
|
id: 'MUADDIB-AST-015',
|
|
502
502
|
name: 'GitHub Actions Workflow Write',
|
|
503
|
-
severity: '
|
|
503
|
+
severity: 'CRITICAL',
|
|
504
504
|
confidence: 'high',
|
|
505
505
|
description: 'fs.writeFileSync cree un fichier dans .github/workflows — injection de workflow GitHub Actions pour persistence. Technique Shai-Hulud 2.0.',
|
|
506
506
|
references: [
|
|
@@ -562,6 +562,44 @@ const RULES = {
|
|
|
562
562
|
mitre: 'T1059'
|
|
563
563
|
},
|
|
564
564
|
|
|
565
|
+
env_charcode_reconstruction: {
|
|
566
|
+
id: 'MUADDIB-AST-018',
|
|
567
|
+
name: 'Environment Variable Key Reconstruction',
|
|
568
|
+
severity: 'HIGH',
|
|
569
|
+
confidence: 'high',
|
|
570
|
+
description: 'process.env accede avec une cle reconstruite dynamiquement via String.fromCharCode. Technique d\'obfuscation pour eviter la detection statique des noms de variables sensibles (GITHUB_TOKEN, etc.).',
|
|
571
|
+
references: [
|
|
572
|
+
'https://attack.mitre.org/techniques/T1027/',
|
|
573
|
+
'https://attack.mitre.org/techniques/T1552/001/'
|
|
574
|
+
],
|
|
575
|
+
mitre: 'T1027'
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
lifecycle_shell_pipe: {
|
|
579
|
+
id: 'MUADDIB-PKG-010',
|
|
580
|
+
name: 'Lifecycle Script Pipes to Shell',
|
|
581
|
+
severity: 'CRITICAL',
|
|
582
|
+
confidence: 'high',
|
|
583
|
+
description: 'Script lifecycle (preinstall/install/postinstall) execute curl | sh ou wget | bash — telecharge et execute du code distant au moment de npm install.',
|
|
584
|
+
references: [
|
|
585
|
+
'https://blog.phylum.io/shai-hulud-npm-worm',
|
|
586
|
+
'https://socket.dev/blog/2025-supply-chain-report'
|
|
587
|
+
],
|
|
588
|
+
mitre: 'T1195.002'
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
credential_tampering: {
|
|
592
|
+
id: 'MUADDIB-FLOW-003',
|
|
593
|
+
name: 'Credential/Cache Tampering',
|
|
594
|
+
severity: 'CRITICAL',
|
|
595
|
+
confidence: 'high',
|
|
596
|
+
description: 'Ecriture dans un chemin sensible (cache npm _cacache, cache yarn, credentials). Possible cache poisoning: injection de code malveillant dans des packages caches.',
|
|
597
|
+
references: [
|
|
598
|
+
'https://attack.mitre.org/techniques/T1565/001/'
|
|
599
|
+
],
|
|
600
|
+
mitre: 'T1565.001'
|
|
601
|
+
},
|
|
602
|
+
|
|
565
603
|
ai_agent_abuse: {
|
|
566
604
|
id: 'MUADDIB-AST-013',
|
|
567
605
|
name: 'AI Agent Weaponization',
|
package/src/scanner/ast.js
CHANGED
|
@@ -78,6 +78,13 @@ const HOOKABLE_NATIVES = [
|
|
|
78
78
|
'WebSocket', 'EventSource'
|
|
79
79
|
];
|
|
80
80
|
|
|
81
|
+
// Node.js core module classes targeted for prototype hooking
|
|
82
|
+
const NODE_HOOKABLE_MODULES = ['http', 'https', 'net', 'tls', 'stream'];
|
|
83
|
+
const NODE_HOOKABLE_CLASSES = [
|
|
84
|
+
'IncomingMessage', 'ServerResponse', 'ClientRequest',
|
|
85
|
+
'OutgoingMessage', 'Socket', 'Server', 'Agent'
|
|
86
|
+
];
|
|
87
|
+
|
|
81
88
|
// Paths indicating sandbox/container environment detection (anti-analysis)
|
|
82
89
|
const SANDBOX_INDICATORS = [
|
|
83
90
|
'/.dockerenv',
|
|
@@ -162,6 +169,20 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
162
169
|
allowHashBang: true
|
|
163
170
|
});
|
|
164
171
|
} catch {
|
|
172
|
+
// AST parse failed — apply regex fallback for known dangerous patterns
|
|
173
|
+
|
|
174
|
+
// Workflow manipulation: reads + writes to .github/workflows
|
|
175
|
+
if (/\.github/.test(content) && /workflows/.test(content) &&
|
|
176
|
+
/writeFileSync|writeFile/.test(content) &&
|
|
177
|
+
/readdirSync|readFileSync/.test(content)) {
|
|
178
|
+
threats.push({
|
|
179
|
+
type: 'workflow_write',
|
|
180
|
+
severity: 'CRITICAL',
|
|
181
|
+
message: 'File reads and modifies .github/workflows — GitHub Actions injection (regex fallback).',
|
|
182
|
+
file: path.relative(basePath, filePath)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
165
186
|
if (content.length > 1000 && content.split('\n').length < 10) {
|
|
166
187
|
threats.push({
|
|
167
188
|
type: 'possible_obfuscation',
|
|
@@ -198,6 +219,9 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
198
219
|
return null;
|
|
199
220
|
}
|
|
200
221
|
|
|
222
|
+
// Pre-scan for fromCharCode pattern (env var name obfuscation)
|
|
223
|
+
const hasFromCharCode = content.includes('fromCharCode');
|
|
224
|
+
|
|
201
225
|
walk.simple(ast, {
|
|
202
226
|
VariableDeclarator(node) {
|
|
203
227
|
if (node.id?.type === 'Identifier') {
|
|
@@ -232,6 +256,10 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
232
256
|
if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
|
|
233
257
|
workflowPathVars.add(node.id.name);
|
|
234
258
|
}
|
|
259
|
+
// Propagate: path.join(workflowPathVar, ...) inherits tracking
|
|
260
|
+
else if (node.init.arguments.some(a => a.type === 'Identifier' && workflowPathVars.has(a.name))) {
|
|
261
|
+
workflowPathVars.add(node.id.name);
|
|
262
|
+
}
|
|
235
263
|
}
|
|
236
264
|
}
|
|
237
265
|
}
|
|
@@ -416,8 +444,8 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
416
444
|
if (hasWorkflowVar || /\.github[\\/]workflows/i.test(checkPath) || /\.github[\\/]actions/i.test(checkPath)) {
|
|
417
445
|
threats.push({
|
|
418
446
|
type: 'workflow_write',
|
|
419
|
-
severity: '
|
|
420
|
-
message: `${writeMethod}()
|
|
447
|
+
severity: 'CRITICAL',
|
|
448
|
+
message: `${writeMethod}() writes to .github/workflows — GitHub Actions injection/persistence technique.`,
|
|
421
449
|
file: path.relative(basePath, filePath)
|
|
422
450
|
});
|
|
423
451
|
}
|
|
@@ -433,7 +461,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
433
461
|
if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
|
|
434
462
|
threats.push({
|
|
435
463
|
type: 'workflow_write',
|
|
436
|
-
severity: '
|
|
464
|
+
severity: 'CRITICAL',
|
|
437
465
|
message: `${mkdirMethod}() creates .github/workflows directory — GitHub Actions persistence technique.`,
|
|
438
466
|
file: path.relative(basePath, filePath)
|
|
439
467
|
});
|
|
@@ -441,6 +469,22 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
441
469
|
}
|
|
442
470
|
}
|
|
443
471
|
|
|
472
|
+
// Detect fs.readdirSync on .github/workflows — workflow enumeration for injection
|
|
473
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
474
|
+
const readDirMethod = node.callee.property.name;
|
|
475
|
+
if ((readDirMethod === 'readdirSync' || readDirMethod === 'readdir') && node.arguments.length > 0) {
|
|
476
|
+
const pathArg = node.arguments[0];
|
|
477
|
+
if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
|
|
478
|
+
threats.push({
|
|
479
|
+
type: 'workflow_write',
|
|
480
|
+
severity: 'CRITICAL',
|
|
481
|
+
message: `${readDirMethod}() enumerates .github/workflows — workflow modification/injection technique.`,
|
|
482
|
+
file: path.relative(basePath, filePath)
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
444
488
|
// Detect fs.chmodSync with executable permissions — binary dropper pattern
|
|
445
489
|
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
446
490
|
const chmodMethod = node.callee.property.name;
|
|
@@ -505,6 +549,24 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
505
549
|
}
|
|
506
550
|
}
|
|
507
551
|
|
|
552
|
+
// Detect Object.defineProperty(process.env, ...) — env interception via property descriptors
|
|
553
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
554
|
+
node.callee.object?.type === 'Identifier' && node.callee.object.name === 'Object' &&
|
|
555
|
+
node.callee.property?.type === 'Identifier' && node.callee.property.name === 'defineProperty' &&
|
|
556
|
+
node.arguments.length >= 2) {
|
|
557
|
+
const target = node.arguments[0];
|
|
558
|
+
if (target.type === 'MemberExpression' &&
|
|
559
|
+
target.object?.name === 'process' &&
|
|
560
|
+
target.property?.name === 'env') {
|
|
561
|
+
threats.push({
|
|
562
|
+
type: 'env_proxy_intercept',
|
|
563
|
+
severity: 'CRITICAL',
|
|
564
|
+
message: 'Object.defineProperty(process.env) detected — intercepts environment variable access for credential theft.',
|
|
565
|
+
file: path.relative(basePath, filePath)
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
508
570
|
if (callName === 'eval') {
|
|
509
571
|
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
510
572
|
threats.push({
|
|
@@ -665,6 +727,25 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
665
727
|
file: path.relative(basePath, filePath)
|
|
666
728
|
});
|
|
667
729
|
}
|
|
730
|
+
|
|
731
|
+
// <module>.<Class>.prototype.<method> = ... (Node.js core module prototype hooking)
|
|
732
|
+
// e.g. http.IncomingMessage.prototype.emit = function(...)
|
|
733
|
+
if (left.object?.type === 'MemberExpression' &&
|
|
734
|
+
left.object.property?.type === 'Identifier' && left.object.property.name === 'prototype' &&
|
|
735
|
+
left.object.object?.type === 'MemberExpression' &&
|
|
736
|
+
left.object.object.object?.type === 'Identifier' &&
|
|
737
|
+
left.object.object.property?.type === 'Identifier') {
|
|
738
|
+
const moduleName = left.object.object.object.name;
|
|
739
|
+
const className = left.object.object.property.name;
|
|
740
|
+
if (NODE_HOOKABLE_MODULES.includes(moduleName) && NODE_HOOKABLE_CLASSES.includes(className)) {
|
|
741
|
+
threats.push({
|
|
742
|
+
type: 'prototype_hook',
|
|
743
|
+
severity: 'CRITICAL',
|
|
744
|
+
message: `${moduleName}.${className}.prototype.${left.property?.name || '?'} overridden — Node.js core module prototype hooking for traffic interception.`,
|
|
745
|
+
file: path.relative(basePath, filePath)
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
668
749
|
}
|
|
669
750
|
},
|
|
670
751
|
|
|
@@ -675,6 +756,15 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
675
756
|
) {
|
|
676
757
|
// Dynamic access: process.env[variable] — always flag as MEDIUM
|
|
677
758
|
if (node.computed) {
|
|
759
|
+
// Escalate if env key was built via String.fromCharCode (obfuscation)
|
|
760
|
+
if (hasFromCharCode) {
|
|
761
|
+
threats.push({
|
|
762
|
+
type: 'env_charcode_reconstruction',
|
|
763
|
+
severity: 'HIGH',
|
|
764
|
+
message: 'process.env accessed with dynamically reconstructed key (String.fromCharCode obfuscation).',
|
|
765
|
+
file: path.relative(basePath, filePath)
|
|
766
|
+
});
|
|
767
|
+
}
|
|
678
768
|
threats.push({
|
|
679
769
|
type: 'env_access',
|
|
680
770
|
severity: 'MEDIUM',
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -69,6 +69,17 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
69
69
|
if (containsSensitiveLiteral(node.init)) {
|
|
70
70
|
sensitivePathVars.add(node.id.name);
|
|
71
71
|
}
|
|
72
|
+
// Propagate sensitive vars through path.join/resolve
|
|
73
|
+
if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
|
|
74
|
+
const obj = node.init.callee.object;
|
|
75
|
+
const prop = node.init.callee.property;
|
|
76
|
+
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
77
|
+
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
78
|
+
if (node.init.arguments.some(a => a.type === 'Identifier' && sensitivePathVars.has(a.name))) {
|
|
79
|
+
sensitivePathVars.add(node.id.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
72
83
|
}
|
|
73
84
|
},
|
|
74
85
|
|
|
@@ -158,6 +169,21 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
171
|
|
|
172
|
+
// Detect writeFileSync/writeFile on sensitive paths → cache poisoning / credential tampering
|
|
173
|
+
if (node.callee.type === 'MemberExpression') {
|
|
174
|
+
const prop = node.callee.property;
|
|
175
|
+
if (prop?.type === 'Identifier' && (prop.name === 'writeFileSync' || prop.name === 'writeFile')) {
|
|
176
|
+
const arg = node.arguments[0];
|
|
177
|
+
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
178
|
+
sinks.push({
|
|
179
|
+
type: 'file_tamper',
|
|
180
|
+
name: prop.name,
|
|
181
|
+
line: node.loc?.start?.line
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
161
187
|
// Track eval calls for staged payload detection
|
|
162
188
|
if (callName === 'eval') {
|
|
163
189
|
sinks.push({
|
|
@@ -173,6 +199,15 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
173
199
|
node.object?.object?.name === 'process' &&
|
|
174
200
|
node.object?.property?.name === 'env'
|
|
175
201
|
) {
|
|
202
|
+
// Dynamic bracket access: process.env[variable]
|
|
203
|
+
if (node.computed) {
|
|
204
|
+
sources.push({
|
|
205
|
+
type: 'env_read',
|
|
206
|
+
name: 'process.env[dynamic]',
|
|
207
|
+
line: node.loc?.start?.line
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
176
211
|
const envVar = node.property?.name || '';
|
|
177
212
|
if (isSensitiveEnv(envVar)) {
|
|
178
213
|
sources.push({
|
|
@@ -197,11 +232,15 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
197
232
|
});
|
|
198
233
|
}
|
|
199
234
|
|
|
200
|
-
|
|
235
|
+
// Separate exfiltration sinks from file tampering sinks
|
|
236
|
+
const exfilSinks = sinks.filter(s => s.type !== 'file_tamper');
|
|
237
|
+
const fileTamperSinks = sinks.filter(s => s.type === 'file_tamper');
|
|
238
|
+
|
|
239
|
+
if (sources.length > 0 && exfilSinks.length > 0) {
|
|
201
240
|
// Determine severity by scope proximity: if source and sink are < 50 lines apart -> CRITICAL, else HIGH
|
|
202
241
|
let severity = 'HIGH';
|
|
203
242
|
for (const src of sources) {
|
|
204
|
-
for (const sink of
|
|
243
|
+
for (const sink of exfilSinks) {
|
|
205
244
|
if (src.line && sink.line && Math.abs(src.line - sink.line) < 50) {
|
|
206
245
|
severity = 'CRITICAL';
|
|
207
246
|
break;
|
|
@@ -213,7 +252,17 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
213
252
|
threats.push({
|
|
214
253
|
type: 'suspicious_dataflow',
|
|
215
254
|
severity: severity,
|
|
216
|
-
message: `Suspicious flow: credentials read (${sources.map(s => s.name).join(', ')}) + network send (${
|
|
255
|
+
message: `Suspicious flow: credentials read (${sources.map(s => s.name).join(', ')}) + network send (${exfilSinks.map(s => s.name).join(', ')})`,
|
|
256
|
+
file: path.relative(basePath, filePath)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Detect cache poisoning: credential source + write to sensitive path
|
|
261
|
+
if (sources.length > 0 && fileTamperSinks.length > 0) {
|
|
262
|
+
threats.push({
|
|
263
|
+
type: 'credential_tampering',
|
|
264
|
+
severity: 'CRITICAL',
|
|
265
|
+
message: `Cache poisoning: sensitive data access (${sources.map(s => s.name).join(', ')}) + write to sensitive path (${fileTamperSinks.map(s => s.name).join(', ')})`,
|
|
217
266
|
file: path.relative(basePath, filePath)
|
|
218
267
|
});
|
|
219
268
|
}
|
|
@@ -226,7 +275,8 @@ const SENSITIVE_PATH_PATTERNS = [
|
|
|
226
275
|
'/etc/passwd', '/etc/shadow', '/etc/hosts',
|
|
227
276
|
'.ethereum', '.electrum', '.config/solana', '.exodus',
|
|
228
277
|
'.atomic', '.metamask', '.ledger-live', '.trezor',
|
|
229
|
-
'.bitcoin', '.monero', '.gnupg'
|
|
278
|
+
'.bitcoin', '.monero', '.gnupg',
|
|
279
|
+
'_cacache', '.cache/yarn', '.cache/pip'
|
|
230
280
|
];
|
|
231
281
|
|
|
232
282
|
function isSensitivePath(val) {
|
package/src/scanner/package.js
CHANGED
|
@@ -80,6 +80,19 @@ async function scanPackageJson(targetPath) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
// Escalate: lifecycle script (preinstall/install/postinstall) + shell pipe → CRITICAL
|
|
85
|
+
if (['preinstall', 'install', 'postinstall'].includes(scriptName)) {
|
|
86
|
+
if (/curl\s.*\|\s*(sh|bash)\b/.test(scriptContent) ||
|
|
87
|
+
/wget\s.*\|\s*(sh|bash)\b/.test(scriptContent)) {
|
|
88
|
+
threats.push({
|
|
89
|
+
type: 'lifecycle_shell_pipe',
|
|
90
|
+
severity: 'CRITICAL',
|
|
91
|
+
message: `Critical: "${scriptName}" pipes remote code to shell — supply chain RCE.`,
|
|
92
|
+
file: 'package.json'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
98
|
|
package/iocs.json.gz
DELETED
|
Binary file
|