muaddib-scanner 2.3.3 → 2.4.0
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/LICENSE +20 -20
- package/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -1
- package/src/canary-tokens.js +184 -184
- package/src/index.js +10 -1
- package/src/ioc/bootstrap.js +181 -181
- package/src/ioc/yaml-loader.js +223 -223
- package/src/maintainer-change.js +224 -224
- package/src/output-formatter.js +192 -192
- package/src/publish-anomaly.js +206 -206
- package/src/report.js +230 -230
- package/src/response/playbooks.js +3 -0
- package/src/rules/index.js +11 -0
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +2 -2
- package/src/scanner/ast.js +3 -4
- package/src/scanner/dataflow.js +12 -7
- package/src/scanner/deobfuscate.js +579 -591
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/hash.js +118 -118
- package/src/scanner/module-graph.js +2 -6
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/package.js +36 -0
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +33 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/shared/constants.js +113 -93
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-ast-diff.js +317 -319
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
package/src/scoring.js
CHANGED
|
@@ -137,7 +137,38 @@ const FRAMEWORK_PROTO_RE = new RegExp(
|
|
|
137
137
|
'^(' + FRAMEWORK_PROTOTYPES.join('|') + ')\\.prototype\\.'
|
|
138
138
|
);
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
// ============================================
|
|
141
|
+
// BENIGN PACKAGE WHITELIST (v2.3.5)
|
|
142
|
+
// ============================================
|
|
143
|
+
// Well-known npm packages whose legitimate code patterns trigger false positives.
|
|
144
|
+
// For whitelisted packages, non-IOC threats are downgraded to LOW.
|
|
145
|
+
// IOC matches, lifecycle_shell_pipe, and cross_file_dataflow are NEVER downgraded
|
|
146
|
+
// — a compromised version of these packages would still be detected.
|
|
147
|
+
const BENIGN_PACKAGE_WHITELIST = new Set([
|
|
148
|
+
'meteor', // powershell PATH setup in install.js (dangerous_exec FP)
|
|
149
|
+
'blessed', // module._compile for terminal capabilities (module_compile FP)
|
|
150
|
+
'sharp', // native bindings with dynamic require + postinstall (lifecycle FP)
|
|
151
|
+
'forever', // process manager: detached spawn + HOME config access (dataflow FP)
|
|
152
|
+
'start-server-and-test' // curl/wget in test scripts, not install hooks (lifecycle FP)
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Threat types never affected by benign package whitelist (real compromise indicators)
|
|
156
|
+
const WHITELIST_EXEMPT_TYPES = new Set([
|
|
157
|
+
'ioc_match', 'known_malicious_package', 'pypi_malicious_package', 'shai_hulud_marker',
|
|
158
|
+
'lifecycle_shell_pipe',
|
|
159
|
+
'cross_file_dataflow'
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
function applyFPReductions(threats, reachableFiles, packageName) {
|
|
163
|
+
// Benign package whitelist: downgrade all non-IOC threats to LOW
|
|
164
|
+
if (packageName && BENIGN_PACKAGE_WHITELIST.has(packageName)) {
|
|
165
|
+
for (const t of threats) {
|
|
166
|
+
if (!WHITELIST_EXEMPT_TYPES.has(t.type) && t.severity !== 'LOW') {
|
|
167
|
+
t.severity = 'LOW';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
141
172
|
// Count occurrences of each threat type (package-level, across all files)
|
|
142
173
|
const typeCounts = {};
|
|
143
174
|
for (const t of threats) {
|
|
@@ -267,5 +298,6 @@ function calculateRiskScore(deduped) {
|
|
|
267
298
|
|
|
268
299
|
module.exports = {
|
|
269
300
|
SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE,
|
|
301
|
+
BENIGN_PACKAGE_WHITELIST, WHITELIST_EXEMPT_TYPES,
|
|
270
302
|
isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore
|
|
271
303
|
};
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const { isDevFile, findJsFiles, forEachSafeFile } = require('../utils.js');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Shared scanner wrapper: iterates JS files, runs analyzeFileFn on original + deobfuscated code,
|
|
6
|
-
* deduplicates findings by type::message key.
|
|
7
|
-
* @param {string} targetPath - Root directory to scan
|
|
8
|
-
* @param {Function} analyzeFileFn - (content, filePath, basePath) => threats[]
|
|
9
|
-
* @param {object} [options]
|
|
10
|
-
* @param {Function} [options.deobfuscate] - Deobfuscation function
|
|
11
|
-
* @param {string[]} [options.excludedFiles] - Relative paths to skip
|
|
12
|
-
* @param {boolean} [options.skipDevFiles=true] - Whether to skip dev/test files
|
|
13
|
-
* @returns {Array} Combined threats
|
|
14
|
-
*/
|
|
15
|
-
function analyzeWithDeobfuscation(targetPath, analyzeFileFn, options = {}) {
|
|
16
|
-
const threats = [];
|
|
17
|
-
const files = findJsFiles(targetPath);
|
|
18
|
-
|
|
19
|
-
forEachSafeFile(files, (file, content) => {
|
|
20
|
-
const relativePath = path.relative(targetPath, file).replace(/\\/g, '/');
|
|
21
|
-
|
|
22
|
-
if (options.excludedFiles && options.excludedFiles.includes(relativePath)) return;
|
|
23
|
-
if (options.skipDevFiles !== false && isDevFile(relativePath)) return;
|
|
24
|
-
|
|
25
|
-
// Analyze original code first (preserves obfuscation-detection rules)
|
|
26
|
-
const fileThreats = analyzeFileFn(content, file, targetPath);
|
|
27
|
-
threats.push(...fileThreats);
|
|
28
|
-
|
|
29
|
-
// Also analyze deobfuscated code for additional findings hidden by obfuscation
|
|
30
|
-
if (typeof options.deobfuscate === 'function') {
|
|
31
|
-
try {
|
|
32
|
-
const result = options.deobfuscate(content);
|
|
33
|
-
if (result.transforms.length > 0) {
|
|
34
|
-
const deobThreats = analyzeFileFn(result.code, file, targetPath);
|
|
35
|
-
const existingKeys = new Set(fileThreats.map(t => `${t.type}::${t.message}`));
|
|
36
|
-
for (const dt of deobThreats) {
|
|
37
|
-
if (!existingKeys.has(`${dt.type}::${dt.message}`)) {
|
|
38
|
-
threats.push(dt);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
} catch { /* deobfuscation failed — skip */ }
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return threats;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
module.exports = { analyzeWithDeobfuscation };
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { isDevFile, findJsFiles, forEachSafeFile } = require('../utils.js');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared scanner wrapper: iterates JS files, runs analyzeFileFn on original + deobfuscated code,
|
|
6
|
+
* deduplicates findings by type::message key.
|
|
7
|
+
* @param {string} targetPath - Root directory to scan
|
|
8
|
+
* @param {Function} analyzeFileFn - (content, filePath, basePath) => threats[]
|
|
9
|
+
* @param {object} [options]
|
|
10
|
+
* @param {Function} [options.deobfuscate] - Deobfuscation function
|
|
11
|
+
* @param {string[]} [options.excludedFiles] - Relative paths to skip
|
|
12
|
+
* @param {boolean} [options.skipDevFiles=true] - Whether to skip dev/test files
|
|
13
|
+
* @returns {Array} Combined threats
|
|
14
|
+
*/
|
|
15
|
+
function analyzeWithDeobfuscation(targetPath, analyzeFileFn, options = {}) {
|
|
16
|
+
const threats = [];
|
|
17
|
+
const files = findJsFiles(targetPath);
|
|
18
|
+
|
|
19
|
+
forEachSafeFile(files, (file, content) => {
|
|
20
|
+
const relativePath = path.relative(targetPath, file).replace(/\\/g, '/');
|
|
21
|
+
|
|
22
|
+
if (options.excludedFiles && options.excludedFiles.includes(relativePath)) return;
|
|
23
|
+
if (options.skipDevFiles !== false && isDevFile(relativePath)) return;
|
|
24
|
+
|
|
25
|
+
// Analyze original code first (preserves obfuscation-detection rules)
|
|
26
|
+
const fileThreats = analyzeFileFn(content, file, targetPath);
|
|
27
|
+
threats.push(...fileThreats);
|
|
28
|
+
|
|
29
|
+
// Also analyze deobfuscated code for additional findings hidden by obfuscation
|
|
30
|
+
if (typeof options.deobfuscate === 'function') {
|
|
31
|
+
try {
|
|
32
|
+
const result = options.deobfuscate(content);
|
|
33
|
+
if (result.transforms.length > 0) {
|
|
34
|
+
const deobThreats = analyzeFileFn(result.code, file, targetPath);
|
|
35
|
+
const existingKeys = new Set(fileThreats.map(t => `${t.type}::${t.message}`));
|
|
36
|
+
for (const dt of deobThreats) {
|
|
37
|
+
if (!existingKeys.has(`${dt.type}::${dt.message}`)) {
|
|
38
|
+
threats.push(dt);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch { /* deobfuscation failed — skip */ }
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return threats;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { analyzeWithDeobfuscation };
|
package/src/shared/constants.js
CHANGED
|
@@ -1,93 +1,113 @@
|
|
|
1
|
-
// Shared REHABILITATED_PACKAGES — single source of truth
|
|
2
|
-
// Packages that were temporarily compromised but are now safe
|
|
3
|
-
// These packages will NOT be blocked (except specific compromised versions)
|
|
4
|
-
const REHABILITATED_PACKAGES = {
|
|
5
|
-
// September 2025 - Massive compromise via phishing, fixed within hours
|
|
6
|
-
'chalk': {
|
|
7
|
-
compromised: [],
|
|
8
|
-
safe: true,
|
|
9
|
-
note: 'Compromised Sept 2025, malicious versions removed from npm'
|
|
10
|
-
},
|
|
11
|
-
'debug': {
|
|
12
|
-
compromised: [],
|
|
13
|
-
safe: true,
|
|
14
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
15
|
-
},
|
|
16
|
-
'ansi-styles': {
|
|
17
|
-
compromised: [],
|
|
18
|
-
safe: true,
|
|
19
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
20
|
-
},
|
|
21
|
-
'strip-ansi': {
|
|
22
|
-
compromised: [],
|
|
23
|
-
safe: true,
|
|
24
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
25
|
-
},
|
|
26
|
-
'wrap-ansi': {
|
|
27
|
-
compromised: [],
|
|
28
|
-
safe: true,
|
|
29
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
30
|
-
},
|
|
31
|
-
'is-arrayish': {
|
|
32
|
-
compromised: [],
|
|
33
|
-
safe: true,
|
|
34
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
35
|
-
},
|
|
36
|
-
'simple-swizzle': {
|
|
37
|
-
compromised: [],
|
|
38
|
-
safe: true,
|
|
39
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
40
|
-
},
|
|
41
|
-
'color-convert': {
|
|
42
|
-
compromised: [],
|
|
43
|
-
safe: true,
|
|
44
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
45
|
-
},
|
|
46
|
-
'supports-color': {
|
|
47
|
-
compromised: [],
|
|
48
|
-
safe: true,
|
|
49
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
50
|
-
},
|
|
51
|
-
'has-flag': {
|
|
52
|
-
compromised: [],
|
|
53
|
-
safe: true,
|
|
54
|
-
note: 'Compromised Sept 2025, quickly fixed'
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
// Packages with specific compromised versions (not all)
|
|
58
|
-
'ua-parser-js': {
|
|
59
|
-
compromised: ['0.7.29', '0.8.0', '1.0.0'],
|
|
60
|
-
safe: false,
|
|
61
|
-
note: 'Specific versions compromised Oct 2021'
|
|
62
|
-
},
|
|
63
|
-
'coa': {
|
|
64
|
-
compromised: ['2.0.3', '2.0.4', '2.1.1', '2.1.3', '3.0.1', '3.1.3'],
|
|
65
|
-
safe: false,
|
|
66
|
-
note: 'Specific versions compromised Nov 2021'
|
|
67
|
-
},
|
|
68
|
-
'rc': {
|
|
69
|
-
compromised: ['1.2.9', '1.3.9', '2.3.9'],
|
|
70
|
-
safe: false,
|
|
71
|
-
note: 'Specific versions compromised Nov 2021'
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
// MUAD'DIB self-allowlisting (only the tool itself, not deps — deps must pass IOC checks)
|
|
75
|
-
'muaddib-scanner': {
|
|
76
|
-
compromised: [],
|
|
77
|
-
safe: true,
|
|
78
|
-
note: 'Our package — self-allowlisted to avoid self-flagging during scan'
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Regex to validate npm package names (prevents command injection)
|
|
83
|
-
const NPM_PACKAGE_REGEX = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
84
|
-
|
|
85
|
-
// Download/extraction limits
|
|
86
|
-
const MAX_TARBALL_SIZE = 50 * 1024 * 1024; // 50MB
|
|
87
|
-
const DOWNLOAD_TIMEOUT = 30_000; // 30 seconds
|
|
88
|
-
|
|
89
|
-
// Shared scanner constants
|
|
90
|
-
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB — skip files larger than this to avoid memory issues
|
|
91
|
-
const ACORN_OPTIONS = { ecmaVersion: 2024, sourceType: 'module', allowHashBang: true };
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
// Shared REHABILITATED_PACKAGES — single source of truth
|
|
2
|
+
// Packages that were temporarily compromised but are now safe
|
|
3
|
+
// These packages will NOT be blocked (except specific compromised versions)
|
|
4
|
+
const REHABILITATED_PACKAGES = {
|
|
5
|
+
// September 2025 - Massive compromise via phishing, fixed within hours
|
|
6
|
+
'chalk': {
|
|
7
|
+
compromised: [],
|
|
8
|
+
safe: true,
|
|
9
|
+
note: 'Compromised Sept 2025, malicious versions removed from npm'
|
|
10
|
+
},
|
|
11
|
+
'debug': {
|
|
12
|
+
compromised: [],
|
|
13
|
+
safe: true,
|
|
14
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
15
|
+
},
|
|
16
|
+
'ansi-styles': {
|
|
17
|
+
compromised: [],
|
|
18
|
+
safe: true,
|
|
19
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
20
|
+
},
|
|
21
|
+
'strip-ansi': {
|
|
22
|
+
compromised: [],
|
|
23
|
+
safe: true,
|
|
24
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
25
|
+
},
|
|
26
|
+
'wrap-ansi': {
|
|
27
|
+
compromised: [],
|
|
28
|
+
safe: true,
|
|
29
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
30
|
+
},
|
|
31
|
+
'is-arrayish': {
|
|
32
|
+
compromised: [],
|
|
33
|
+
safe: true,
|
|
34
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
35
|
+
},
|
|
36
|
+
'simple-swizzle': {
|
|
37
|
+
compromised: [],
|
|
38
|
+
safe: true,
|
|
39
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
40
|
+
},
|
|
41
|
+
'color-convert': {
|
|
42
|
+
compromised: [],
|
|
43
|
+
safe: true,
|
|
44
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
45
|
+
},
|
|
46
|
+
'supports-color': {
|
|
47
|
+
compromised: [],
|
|
48
|
+
safe: true,
|
|
49
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
50
|
+
},
|
|
51
|
+
'has-flag': {
|
|
52
|
+
compromised: [],
|
|
53
|
+
safe: true,
|
|
54
|
+
note: 'Compromised Sept 2025, quickly fixed'
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Packages with specific compromised versions (not all)
|
|
58
|
+
'ua-parser-js': {
|
|
59
|
+
compromised: ['0.7.29', '0.8.0', '1.0.0'],
|
|
60
|
+
safe: false,
|
|
61
|
+
note: 'Specific versions compromised Oct 2021'
|
|
62
|
+
},
|
|
63
|
+
'coa': {
|
|
64
|
+
compromised: ['2.0.3', '2.0.4', '2.1.1', '2.1.3', '3.0.1', '3.1.3'],
|
|
65
|
+
safe: false,
|
|
66
|
+
note: 'Specific versions compromised Nov 2021'
|
|
67
|
+
},
|
|
68
|
+
'rc': {
|
|
69
|
+
compromised: ['1.2.9', '1.3.9', '2.3.9'],
|
|
70
|
+
safe: false,
|
|
71
|
+
note: 'Specific versions compromised Nov 2021'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// MUAD'DIB self-allowlisting (only the tool itself, not deps — deps must pass IOC checks)
|
|
75
|
+
'muaddib-scanner': {
|
|
76
|
+
compromised: [],
|
|
77
|
+
safe: true,
|
|
78
|
+
note: 'Our package — self-allowlisted to avoid self-flagging during scan'
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Regex to validate npm package names (prevents command injection)
|
|
83
|
+
const NPM_PACKAGE_REGEX = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
84
|
+
|
|
85
|
+
// Download/extraction limits
|
|
86
|
+
const MAX_TARBALL_SIZE = 50 * 1024 * 1024; // 50MB
|
|
87
|
+
const DOWNLOAD_TIMEOUT = 30_000; // 30 seconds
|
|
88
|
+
|
|
89
|
+
// Shared scanner constants
|
|
90
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB — skip files larger than this to avoid memory issues
|
|
91
|
+
const ACORN_OPTIONS = { ecmaVersion: 2024, sourceType: 'module', allowHashBang: true };
|
|
92
|
+
|
|
93
|
+
const acorn = require('acorn');
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse JS source with module-mode fallback to script-mode.
|
|
97
|
+
* `const package = ...` is valid in script mode but reserved in module mode.
|
|
98
|
+
* Returns AST or null if both modes fail.
|
|
99
|
+
*/
|
|
100
|
+
function safeParse(code, extraOptions = {}) {
|
|
101
|
+
const opts = { ...ACORN_OPTIONS, ...extraOptions };
|
|
102
|
+
try {
|
|
103
|
+
return acorn.parse(code, opts);
|
|
104
|
+
} catch {
|
|
105
|
+
try {
|
|
106
|
+
return acorn.parse(code, { ...opts, sourceType: 'script' });
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { REHABILITATED_PACKAGES, NPM_PACKAGE_REGEX, MAX_TARBALL_SIZE, DOWNLOAD_TIMEOUT, MAX_FILE_SIZE, ACORN_OPTIONS, safeParse };
|