muaddib-scanner 2.8.2 → 2.8.4
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/ioc/scraper.js +15 -6
- package/src/scanner/ast-detectors.js +34 -12
- package/src/scanner/ast.js +7 -1
- package/src/scanner/dataflow.js +20 -0
- package/src/scanner/package.js +5 -1
- package/src/scoring.js +15 -0
package/package.json
CHANGED
package/src/ioc/scraper.js
CHANGED
|
@@ -462,7 +462,11 @@ function extractVersions(affected) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
|
|
465
|
+
if (versions.size === 0) {
|
|
466
|
+
console.log('[SCRAPER] WARN: No version info found, skipping wildcard fallback');
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
return [...versions];
|
|
466
470
|
}
|
|
467
471
|
|
|
468
472
|
/**
|
|
@@ -519,7 +523,8 @@ async function scrapeShaiHuludDetector() {
|
|
|
519
523
|
// Extract packages — one IOC per version for correct matching
|
|
520
524
|
const pkgList = data.packages || [];
|
|
521
525
|
for (const pkg of pkgList) {
|
|
522
|
-
const versions = pkg.affectedVersions || [
|
|
526
|
+
const versions = pkg.affectedVersions || [];
|
|
527
|
+
if (versions.length === 0) continue; // Skip packages with no version info — avoids false wildcard
|
|
523
528
|
for (const ver of versions) {
|
|
524
529
|
packages.push({
|
|
525
530
|
id: `SHAI-HULUD-${pkg.name}-${ver}`,
|
|
@@ -588,10 +593,11 @@ async function scrapeDatadogIOCs() {
|
|
|
588
593
|
? versionsStr.split(',').map(v => v.trim()).filter(Boolean)
|
|
589
594
|
: [versionsStr];
|
|
590
595
|
for (const ver of versionList) {
|
|
596
|
+
if (!ver || ver === '*') continue; // Skip wildcard fallbacks — avoids false positive cascade
|
|
591
597
|
packages.push({
|
|
592
598
|
id: `DATADOG-${name}`,
|
|
593
599
|
name: name,
|
|
594
|
-
version: ver
|
|
600
|
+
version: ver,
|
|
595
601
|
severity: 'critical',
|
|
596
602
|
confidence: 'high',
|
|
597
603
|
source: 'datadog-consolidated',
|
|
@@ -967,10 +973,11 @@ async function scrapeStaticIOCs() {
|
|
|
967
973
|
|
|
968
974
|
// Socket.dev reports
|
|
969
975
|
for (const pkg of staticIOCs.socket || []) {
|
|
976
|
+
if (!pkg.version) continue; // Skip entries without version — avoids wildcard cascade
|
|
970
977
|
packages.push({
|
|
971
978
|
id: `SOCKET-${pkg.name}`,
|
|
972
979
|
name: pkg.name,
|
|
973
|
-
version: pkg.version
|
|
980
|
+
version: pkg.version,
|
|
974
981
|
severity: pkg.severity || 'critical',
|
|
975
982
|
confidence: 'high',
|
|
976
983
|
source: 'socket-dev',
|
|
@@ -983,10 +990,11 @@ async function scrapeStaticIOCs() {
|
|
|
983
990
|
|
|
984
991
|
// Phylum Research
|
|
985
992
|
for (const pkg of staticIOCs.phylum || []) {
|
|
993
|
+
if (!pkg.version) continue; // Skip entries without version — avoids wildcard cascade
|
|
986
994
|
packages.push({
|
|
987
995
|
id: `PHYLUM-${pkg.name}`,
|
|
988
996
|
name: pkg.name,
|
|
989
|
-
version: pkg.version
|
|
997
|
+
version: pkg.version,
|
|
990
998
|
severity: pkg.severity || 'critical',
|
|
991
999
|
confidence: 'high',
|
|
992
1000
|
source: 'phylum',
|
|
@@ -999,10 +1007,11 @@ async function scrapeStaticIOCs() {
|
|
|
999
1007
|
|
|
1000
1008
|
// npm removed packages
|
|
1001
1009
|
for (const pkg of staticIOCs.npmRemoved || []) {
|
|
1010
|
+
if (!pkg.version) continue; // Skip entries without version — avoids wildcard cascade
|
|
1002
1011
|
packages.push({
|
|
1003
1012
|
id: `NPM-REMOVED-${pkg.name}`,
|
|
1004
1013
|
name: pkg.name,
|
|
1005
|
-
version: pkg.version
|
|
1014
|
+
version: pkg.version,
|
|
1006
1015
|
severity: 'critical',
|
|
1007
1016
|
confidence: 'high',
|
|
1008
1017
|
source: 'npm-removed',
|
|
@@ -2062,11 +2062,14 @@ function handlePostWalk(ctx) {
|
|
|
2062
2062
|
// Wave 4: Download-execute-cleanup — https download + chmod executable + execSync + unlink
|
|
2063
2063
|
// Exclude when all URLs in the file point to safe registries (npm, GitHub, nodejs.org)
|
|
2064
2064
|
// B4: removed fetchOnlySafeDomains guard — compound requires fetch+chmod+exec, which is never legitimate
|
|
2065
|
+
// C10: If file also contains hash/checksum verification, downgrade to HIGH — real droppers
|
|
2066
|
+
// don't verify payload integrity; legitimate installers (esbuild, sharp) do.
|
|
2065
2067
|
if (ctx.hasRemoteFetch && ctx.hasChmodExecutable && ctx.hasExecSyncCall) {
|
|
2066
2068
|
ctx.threats.push({
|
|
2067
2069
|
type: 'download_exec_binary',
|
|
2068
|
-
severity: 'CRITICAL',
|
|
2069
|
-
message: 'Download-execute pattern: remote fetch + chmod executable + execSync in same file.
|
|
2070
|
+
severity: ctx.hasHashVerification ? 'HIGH' : 'CRITICAL',
|
|
2071
|
+
message: 'Download-execute pattern: remote fetch + chmod executable + execSync in same file.' +
|
|
2072
|
+
(ctx.hasHashVerification ? ' Hash verification detected — likely legitimate binary installer.' : ' Binary dropper camouflaged as native addon build.'),
|
|
2070
2073
|
file: ctx.relFile
|
|
2071
2074
|
});
|
|
2072
2075
|
}
|
|
@@ -2082,22 +2085,41 @@ function handlePostWalk(ctx) {
|
|
|
2082
2085
|
});
|
|
2083
2086
|
}
|
|
2084
2087
|
|
|
2085
|
-
// WASM payload detection: WebAssembly.compile/instantiate +
|
|
2086
|
-
//
|
|
2087
|
-
//
|
|
2088
|
+
// WASM payload detection: WebAssembly.compile/instantiate + network in same file
|
|
2089
|
+
// C5+C6: Only emit CRITICAL wasm_host_sink if corroborating exfil signals exist
|
|
2090
|
+
// (env_access, sensitive_string, credential reads). WASM + fetch alone is likely
|
|
2091
|
+
// just WASM module loading via fetch() (standard pattern: fetch('mod.wasm').then(WebAssembly.instantiateStreaming))
|
|
2088
2092
|
if (ctx.hasWasmLoad && ctx.hasNetworkCallInFile) {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
file
|
|
2094
|
-
|
|
2093
|
+
// C5/C6: Distinguish fetch-for-WASM-loading from independent network channels
|
|
2094
|
+
// https.request, http.get, dns.resolve are NEVER used for WASM loading — they indicate
|
|
2095
|
+
// an independent network channel (e.g., WASM host callbacks for C2 exfiltration)
|
|
2096
|
+
const hasExfilSignal = ctx.threats.some(t =>
|
|
2097
|
+
t.file === ctx.relFile && (
|
|
2098
|
+
t.type === 'env_access' || t.type === 'sensitive_string' ||
|
|
2099
|
+
t.type === 'suspicious_dataflow' || t.type === 'credential_regex_harvest'
|
|
2100
|
+
)
|
|
2101
|
+
);
|
|
2102
|
+
if (ctx.hasNonFetchNetworkCall || hasExfilSignal) {
|
|
2103
|
+
ctx.threats.push({
|
|
2104
|
+
type: 'wasm_host_sink',
|
|
2105
|
+
severity: 'CRITICAL',
|
|
2106
|
+
message: 'WebAssembly module with network-capable host imports and credential/env access. WASM can invoke host callbacks to exfiltrate data while hiding control flow.',
|
|
2107
|
+
file: ctx.relFile
|
|
2108
|
+
});
|
|
2109
|
+
} else {
|
|
2110
|
+
// WASM + network but no credential/env signals → standalone MEDIUM (likely fetch for WASM loading)
|
|
2111
|
+
ctx.threats.push({
|
|
2112
|
+
type: 'wasm_standalone',
|
|
2113
|
+
severity: 'MEDIUM',
|
|
2114
|
+
message: 'WebAssembly module with network calls but no credential/env access signals. Likely WASM loading via fetch(). Verify .wasm file purpose.',
|
|
2115
|
+
file: ctx.relFile
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2095
2118
|
}
|
|
2096
2119
|
|
|
2097
2120
|
// WASM standalone: WebAssembly.compile/instantiate WITHOUT network sinks.
|
|
2098
2121
|
// Legitimate: crypto, image processing, codecs. Still warrants investigation
|
|
2099
2122
|
// because WASM hides control flow from static analysis.
|
|
2100
|
-
// Compound WASM + network → wasm_host_sink (CRITICAL) takes priority (mutually exclusive).
|
|
2101
2123
|
if (ctx.hasWasmLoad && !ctx.hasNetworkCallInFile) {
|
|
2102
2124
|
ctx.threats.push({
|
|
2103
2125
|
type: 'wasm_standalone',
|
package/src/scanner/ast.js
CHANGED
|
@@ -111,6 +111,8 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
111
111
|
hasEnvEnumeration: false, // Object.entries/keys/values(process.env)
|
|
112
112
|
hasEnvHarvestPattern: /\b(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|NPM|AWS|SSH|WEBHOOK)\b/.test(content),
|
|
113
113
|
hasNetworkCallInFile: /\b(fetch|https?\.request|https?\.get|dns\.resolve)\b/.test(content),
|
|
114
|
+
// C5: Non-fetch network calls indicate independent network channel (NOT WASM loading)
|
|
115
|
+
hasNonFetchNetworkCall: /\bhttps?\.request\b|\bhttps?\.get\b|\bdns\.resolve\b/.test(content),
|
|
114
116
|
// Credential regex harvesting: regex literals or new RegExp() whose PATTERN contains credential keywords
|
|
115
117
|
// Must check that the keyword is inside the regex, not just anywhere in the file
|
|
116
118
|
hasCredentialRegex: hasCredentialInsideRegex(content),
|
|
@@ -154,7 +156,11 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
154
156
|
// WASM payload detection: WebAssembly.compile/instantiate with host import sinks
|
|
155
157
|
hasWasmLoad: /\bWebAssembly\s*\.\s*(compile|instantiate|compileStreaming|instantiateStreaming)\b/.test(content),
|
|
156
158
|
hasWasmHostSink: false, // set in handleCallExpression when WASM import object contains network/fs sinks
|
|
157
|
-
hasProxyTrap: false // set in handleNewExpression when Proxy has set/get/apply trap
|
|
159
|
+
hasProxyTrap: false, // set in handleNewExpression when Proxy has set/get/apply trap
|
|
160
|
+
// C10: Hash verification — legitimate binary installers verify checksums
|
|
161
|
+
// Requires BOTH createHash() call AND .digest() call — false positives from
|
|
162
|
+
// standalone mentions of 'sha256' or 'integrity' in comments/descriptions
|
|
163
|
+
hasHashVerification: /\bcreateHash\s*\(/.test(content) && /\.digest\s*\(/.test(content)
|
|
158
164
|
};
|
|
159
165
|
|
|
160
166
|
// Compute fetchOnlySafeDomains: check if ALL URLs in file point to known registries
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -809,6 +809,26 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
809
809
|
const allTelemetryOnly = sources.every(s => s.type === 'telemetry_read');
|
|
810
810
|
if (allTelemetryOnly && severity === 'CRITICAL') severity = 'HIGH';
|
|
811
811
|
|
|
812
|
+
// C7: SDK pattern downgrade — if ALL env_read sources match SDK env→domain mappings,
|
|
813
|
+
// this is legitimate SDK usage (e.g., STRIPE_API_KEY → api.stripe.com). Cap at HIGH.
|
|
814
|
+
if (severity === 'CRITICAL') {
|
|
815
|
+
const envSources = sources.filter(s => s.type === 'env_read');
|
|
816
|
+
if (envSources.length > 0 && sources.every(s => s.type === 'env_read' || s.type === 'telemetry_read')) {
|
|
817
|
+
try {
|
|
818
|
+
const { isSDKPattern } = require('../intent-graph.js');
|
|
819
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
820
|
+
const allSDK = envSources.every(s => {
|
|
821
|
+
// Extract env var name from source name (e.g., "STRIPE_API_KEY" from "process.env.STRIPE_API_KEY")
|
|
822
|
+
const envVar = s.name.replace(/^process\.env\./, '').replace(/^process\.env\[['"]/, '').replace(/['"]\]$/, '');
|
|
823
|
+
return isSDKPattern(envVar, fileContent);
|
|
824
|
+
});
|
|
825
|
+
if (allSDK) severity = 'HIGH';
|
|
826
|
+
} catch {
|
|
827
|
+
// Intent graph not available — keep CRITICAL
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
812
832
|
const sourceDesc = hasCommandOutput ? 'command output' : 'credentials read';
|
|
813
833
|
threats.push({
|
|
814
834
|
type: 'suspicious_dataflow',
|
package/src/scanner/package.js
CHANGED
|
@@ -188,11 +188,15 @@ async function scanPackageJson(targetPath) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
if (malicious) {
|
|
191
|
+
// C1: Include triggering dependency metadata for diagnostic
|
|
191
192
|
threats.push({
|
|
192
193
|
type: 'known_malicious_package',
|
|
193
194
|
severity: 'CRITICAL',
|
|
194
195
|
message: `Malicious dependency declared: ${depName}@${depVersion} (source: ${malicious.source || 'IOC'})`,
|
|
195
|
-
file: 'package.json'
|
|
196
|
+
file: 'package.json',
|
|
197
|
+
matchedDep: depName,
|
|
198
|
+
matchedVersion: malicious.version,
|
|
199
|
+
iocSource: malicious.source || 'IOC'
|
|
196
200
|
});
|
|
197
201
|
}
|
|
198
202
|
}
|
package/src/scoring.js
CHANGED
|
@@ -340,6 +340,21 @@ function applyFPReductions(threats, reachableFiles, packageName, packageDeps) {
|
|
|
340
340
|
t.severity = 'MEDIUM';
|
|
341
341
|
t.mcpSdkDowngrade = true;
|
|
342
342
|
}
|
|
343
|
+
|
|
344
|
+
// C12: AI SDK awareness — env_access on AI API keys is expected in SDK packages.
|
|
345
|
+
// Downgrade env_access HIGH → MEDIUM when @modelcontextprotocol/sdk, @anthropic/sdk,
|
|
346
|
+
// or openai is in dependencies AND the env var is an AI provider key.
|
|
347
|
+
// Does NOT affect compound detections (intent_credential_exfil stays CRITICAL).
|
|
348
|
+
if (t.type === 'env_access' && t.severity === 'HIGH' &&
|
|
349
|
+
packageDeps && typeof packageDeps === 'object') {
|
|
350
|
+
const hasAiSdk = packageDeps['@modelcontextprotocol/sdk'] ||
|
|
351
|
+
packageDeps['@anthropic/sdk'] ||
|
|
352
|
+
packageDeps['openai'];
|
|
353
|
+
if (hasAiSdk && /\b(ANTHROPIC_API_KEY|OPENAI_API_KEY|CLAUDE_API_KEY)\b/.test(t.message)) {
|
|
354
|
+
t.reductions.push({ rule: 'ai_sdk_env', from: 'HIGH', to: 'MEDIUM' });
|
|
355
|
+
t.severity = 'MEDIUM';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
343
358
|
}
|
|
344
359
|
}
|
|
345
360
|
|