muaddib-scanner 2.6.1 → 2.6.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.
- package/package.json +2 -2
- package/src/scoring.js +20 -10
- package/src/temporal-runner.js +26 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm & PyPI/Python",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@eslint/js": "10.0.1",
|
|
58
|
-
"eslint": "10.0.
|
|
58
|
+
"eslint": "10.0.3",
|
|
59
59
|
"eslint-plugin-security": "^4.0.0",
|
|
60
60
|
"globals": "17.4.0"
|
|
61
61
|
}
|
package/src/scoring.js
CHANGED
|
@@ -122,8 +122,14 @@ const FP_COUNT_THRESHOLDS = {
|
|
|
122
122
|
// B1 FP reduction: bundled code aliases eval/Function (sinon, storybook, vitest)
|
|
123
123
|
dangerous_call_eval: { maxCount: 3, from: 'MEDIUM', to: 'LOW' },
|
|
124
124
|
// P6: HTTP client libraries (undici, aws-sdk, nodemailer, jsdom) parse Authorization/Bearer headers
|
|
125
|
-
// with
|
|
126
|
-
credential_regex_harvest: { maxCount:
|
|
125
|
+
// with 3+ credential regexes. Real harvesters use 1-2 targeted regexes.
|
|
126
|
+
credential_regex_harvest: { maxCount: 2, from: 'HIGH', to: 'LOW' },
|
|
127
|
+
// P7: Config frameworks (pm2, nx, dotenv, aws-sdk) read 10+ env vars — not credential theft.
|
|
128
|
+
// Real stealers access 1-5 targeted env vars. Count >10 = config loader pattern.
|
|
129
|
+
env_access: { maxCount: 10, from: 'HIGH', to: 'LOW' },
|
|
130
|
+
// P7: Bundled files with 5+ high-entropy strings are data files, not malware payloads.
|
|
131
|
+
// Real payloads use 1-2 targeted encoded strings. Count >5 = bundled assets/data.
|
|
132
|
+
high_entropy_string: { maxCount: 5, to: 'LOW' }
|
|
127
133
|
};
|
|
128
134
|
|
|
129
135
|
// Types exempt from dist/ downgrade — IOC matches, lifecycle scripts, and
|
|
@@ -144,8 +150,9 @@ const DIST_EXEMPT_TYPES = new Set([
|
|
|
144
150
|
// fetch_decrypt_exec (fetch+decrypt+eval triple) remains exempt — never coincidental.
|
|
145
151
|
]);
|
|
146
152
|
|
|
147
|
-
// Regex matching dist/build/minified/bundled file paths
|
|
148
|
-
|
|
153
|
+
// Regex matching dist/build/out/output/minified/bundled file paths
|
|
154
|
+
// P7: added out/ and output/ — common build output directories (esbuild, custom build scripts)
|
|
155
|
+
const DIST_FILE_RE = /(?:^|[/\\])(?:dist|build|out|output)[/\\]|\.min\.js$|\.bundle\.js$/i;
|
|
149
156
|
|
|
150
157
|
// Bundler artifact types: get two-notch downgrade in dist/ files (CRITICAL→MEDIUM, HIGH→LOW).
|
|
151
158
|
// These are individual pattern signals that bundlers routinely produce (eval for globalThis,
|
|
@@ -155,7 +162,9 @@ const DIST_BUNDLER_ARTIFACT_TYPES = new Set([
|
|
|
155
162
|
'dynamic_require', 'dynamic_import',
|
|
156
163
|
'obfuscation_detected', 'high_entropy_string', 'possible_obfuscation',
|
|
157
164
|
'js_obfuscation_pattern', 'vm_code_execution',
|
|
158
|
-
'module_compile', 'module_compile_dynamic'
|
|
165
|
+
'module_compile', 'module_compile_dynamic',
|
|
166
|
+
// P7: env_access in dist/ is bundled SDK config reading, not credential theft
|
|
167
|
+
'env_access'
|
|
159
168
|
]);
|
|
160
169
|
|
|
161
170
|
// Types exempt from reachability downgrade — IOC matches, lifecycle, and package-level types.
|
|
@@ -223,15 +232,16 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
223
232
|
const rule = FP_COUNT_THRESHOLDS[t.type];
|
|
224
233
|
if (rule && typeCounts[t.type] > rule.maxCount && (!rule.from || t.severity === rule.from)) {
|
|
225
234
|
const typeRatio = typeCounts[t.type] / totalThreats;
|
|
226
|
-
// suspicious_dataflow:
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
235
|
+
// suspicious_dataflow: full bypass of percentage guard. Packages with >3 suspicious_dataflow
|
|
236
|
+
// findings are always legitimate SDKs (SMTP, monitoring, analytics). Real malware has 1-2
|
|
237
|
+
// targeted source→sink pairs. The count >3 threshold is sufficient protection.
|
|
238
|
+
// P7: removed 80% ratio cap — it caused ~30k FP hits in production on SDK packages
|
|
239
|
+
// where dataflow was the dominant finding type (e.g. @darajs/core, addio-admin-sdk).
|
|
230
240
|
// vm_code_execution: full bypass — packages with only vm.Script calls (cassandra-driver,
|
|
231
241
|
// webpack, jest) are legitimate. Real malware using vm always has other signals
|
|
232
242
|
// (network, fs, obfuscation). The >3 count threshold is sufficient protection.
|
|
233
243
|
if (typeRatio < 0.4 ||
|
|
234
|
-
|
|
244
|
+
t.type === 'suspicious_dataflow' ||
|
|
235
245
|
t.type === 'vm_code_execution') {
|
|
236
246
|
t.severity = rule.to;
|
|
237
247
|
}
|
package/src/temporal-runner.js
CHANGED
|
@@ -84,6 +84,7 @@ async function runTemporalAnalyses(targetPath, options, pkgNames) {
|
|
|
84
84
|
}
|
|
85
85
|
{
|
|
86
86
|
const PUBLISH_CONCURRENCY = 5;
|
|
87
|
+
const publishThreats = [];
|
|
87
88
|
for (let i = 0; i < pkgNames.length; i += PUBLISH_CONCURRENCY) {
|
|
88
89
|
const batch = pkgNames.slice(i, i + PUBLISH_CONCURRENCY);
|
|
89
90
|
const results = await Promise.allSettled(
|
|
@@ -93,15 +94,38 @@ async function runTemporalAnalyses(targetPath, options, pkgNames) {
|
|
|
93
94
|
if (r.status !== 'fulfilled' || !r.value.suspicious) continue;
|
|
94
95
|
const det = r.value;
|
|
95
96
|
for (const a of det.anomalies) {
|
|
96
|
-
|
|
97
|
+
publishThreats.push({
|
|
97
98
|
type: a.type,
|
|
98
99
|
severity: a.severity,
|
|
99
100
|
message: a.description,
|
|
100
|
-
file: `node_modules/${det.packageName}/package.json
|
|
101
|
+
file: `node_modules/${det.packageName}/package.json`,
|
|
102
|
+
_scope: det.packageName.startsWith('@') ? det.packageName.split('/')[0] : null
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
|
|
108
|
+
// P7: Scope-aware deduplication for monorepo releases.
|
|
109
|
+
// When 3+ packages from the same @scope trigger publish_burst or rapid_succession,
|
|
110
|
+
// it's a coordinated monorepo release (lerna, nx, turbo), not an attack.
|
|
111
|
+
// Downgrade all findings for that scope to LOW severity.
|
|
112
|
+
const MONOREPO_SCOPE_THRESHOLD = 3;
|
|
113
|
+
const scopeTypeCounts = new Map(); // key: `${scope}:${type}` → count
|
|
114
|
+
for (const t of publishThreats) {
|
|
115
|
+
if (!t._scope) continue;
|
|
116
|
+
const key = `${t._scope}:${t.type}`;
|
|
117
|
+
scopeTypeCounts.set(key, (scopeTypeCounts.get(key) || 0) + 1);
|
|
118
|
+
}
|
|
119
|
+
for (const t of publishThreats) {
|
|
120
|
+
if (t._scope) {
|
|
121
|
+
const key = `${t._scope}:${t.type}`;
|
|
122
|
+
if ((scopeTypeCounts.get(key) || 0) >= MONOREPO_SCOPE_THRESHOLD) {
|
|
123
|
+
t.severity = 'LOW';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
delete t._scope; // clean up internal field
|
|
127
|
+
threats.push(t);
|
|
128
|
+
}
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
|