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/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
- function applyFPReductions(threats, reachableFiles) {
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 };
@@ -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
- module.exports = { REHABILITATED_PACKAGES, NPM_PACKAGE_REGEX, MAX_TARBALL_SIZE, DOWNLOAD_TIMEOUT, MAX_FILE_SIZE, ACORN_OPTIONS };
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 };