np-audit 1.5.1 → 2.1.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.
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { shannonEntropy } = require('../utils/entropy');
4
+
3
5
  // ─── Constants ───────────────────────────────────────────────────────────────
4
6
 
5
7
  const MAX_CODE_SIZE = 500000; // 500KB - chunk larger files
@@ -338,36 +340,10 @@ function checkFilesystemManipulation(code) {
338
340
  };
339
341
  }
340
342
 
341
- // ─── Entropy helper ──────────────────────────────────────────────────────────
342
-
343
- function shannonEntropy(str) {
344
- if (!str || str.length === 0) return 0;
345
- const freq = {};
346
- for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
347
- let entropy = 0;
348
- for (const count of Object.values(freq)) {
349
- const p = count / str.length;
350
- entropy -= p * Math.log2(p);
351
- }
352
- return entropy;
353
- }
343
+ // ─── Entropy helper (re-exported from utils/entropy.js) ─────────────────────
354
344
 
355
345
  // ─── Main detection function ─────────────────────────────────────────────────
356
346
 
357
- const CHECKS = [
358
- checkEval,
359
- checkObfuscatorIo,
360
- checkHighEntropy,
361
- checkHexEscapes,
362
- checkFromCharCode,
363
- checkBase64Exec,
364
- checkChildProcess,
365
- checkHexArray,
366
- checkProcessEnv,
367
- checkNetworkCalls,
368
- checkFilesystemManipulation,
369
- ];
370
-
371
347
  /**
372
348
  * Run all checks against a code string.
373
349
  * For large files, uses a sliding window (50% overlap) so payloads cannot
@@ -399,8 +375,12 @@ function detectObfuscation(code, config = { blockScore: 50, warnScore: 20 }) {
399
375
 
400
376
  const allFindings = new Map(); // Dedupe by name, keep highest score
401
377
 
378
+ // Combine inline checks with marshaller registry
379
+ const { getStaticMarshallers } = require('../marshallers');
380
+ const allChecks = getStaticMarshallers().map(m => m.check.bind(m));
381
+
402
382
  for (const chunk of chunks) {
403
- for (const check of CHECKS) {
383
+ for (const check of allChecks) {
404
384
  const result = check(chunk);
405
385
  if (result) {
406
386
  const existing = allFindings.get(result.name);
@@ -8,6 +8,7 @@ const { parseTarGz, extractFile, getPackageJson } = require('../utils/tar
8
8
  const { detectObfuscation } = require('./detector');
9
9
  const { walkRequires, MAX_FILES_PER_PACKAGE, MAX_TOTAL_BYTES } = require('./requireWalker');
10
10
  const { parseCommand } = require('../utils/command');
11
+ const { getPackageMarshallers } = require('../marshallers');
11
12
  const output = require('../utils/output');
12
13
 
13
14
  // Lifecycle scripts that npm executes during install. The original tool only
@@ -73,10 +74,7 @@ async function scan(opts) {
73
74
  }
74
75
  }
75
76
 
76
- // Track packages without install scripts (for skipped count)
77
- let skippedCount = 0;
78
-
79
- // Apply skip filters
77
+ // Apply user skip filters (scopes, packages, dev)
80
78
  packages = packages.filter(pkg => {
81
79
  if (noDev && pkg.dev) return false;
82
80
  if (pkg.inBundle || pkg.link) return false;
@@ -86,15 +84,15 @@ async function scan(opts) {
86
84
  if (pkg.name.startsWith(scope + '/') || pkg.name === scope) return false;
87
85
  }
88
86
  }
89
- // For explicit packages, always include them but track if they have no scripts
90
- if (explicitPackageNames.has(pkg.name)) {
91
- if (!pkg.hasInstallScript) {
92
- skippedCount++;
93
- return false;
94
- }
95
- return true;
96
- }
97
- // v2/v3 lockfiles reliably report hasInstallScript — skip definitive negatives
87
+ return true;
88
+ });
89
+
90
+ // All non-skipped packages are eligible for package-level marshallers (CVE)
91
+ const allPackages = packages;
92
+
93
+ // Filter to only packages with lifecycle scripts for code analysis
94
+ packages = packages.filter(pkg => {
95
+ if (explicitPackageNames.has(pkg.name)) return true;
98
96
  if (lockfileVersion >= 2 && pkg.hasInstallScript === false) return false;
99
97
  return true;
100
98
  });
@@ -106,23 +104,52 @@ async function scan(opts) {
106
104
  return scanPackage(pkg, cwd, config, verbose);
107
105
  });
108
106
 
107
+ // Run package-level marshallers (CVE checks) on ALL packages, not just those with scripts
108
+ if (config.checkVulnerabilities) {
109
+ const packageMarshallers = getPackageMarshallers();
110
+ const cveResults = await mapWithConcurrency(allPackages, config.parallelFetches, async (pkg) => {
111
+ for (const marshaller of packageMarshallers) {
112
+ const finding = await marshaller.checkPackage(pkg, config);
113
+ if (finding) return { pkg, finding };
114
+ }
115
+ return null;
116
+ });
117
+
118
+ for (const cveResult of cveResults) {
119
+ if (!cveResult) continue;
120
+ const { pkg, finding } = cveResult;
121
+ const existing = results.find(r => r && r.pkg && r.pkg.name === pkg.name);
122
+ if (existing) {
123
+ existing.findings.push(finding);
124
+ if (finding.score > existing.score) {
125
+ existing.score = finding.score;
126
+ existing.verdict = verdictFromScore(finding.score, config);
127
+ }
128
+ } else {
129
+ results.push({
130
+ pkg,
131
+ scripts: [],
132
+ score: finding.score,
133
+ findings: [finding],
134
+ verdict: verdictFromScore(finding.score, config),
135
+ });
136
+ }
137
+ }
138
+ }
139
+
109
140
  const scanned = results.filter(Boolean);
110
- // Add packages that returned null from scanPackage (no scripts found during scan)
111
- skippedCount += results.filter(r => r === null).length;
112
-
113
- // Optionally scan the *current project's own* lifecycle scripts. This is
114
- // off by default to avoid surprising users — `npa` is a drop-in replacement
115
- // for `npm install` and most projects' own postinstall scripts are
116
- // intentionally local. Set `scanSelf: true` in .npmauditor.json (or pass
117
- // --scan-self) to opt in. Useful for CI on third-party PRs.
141
+
142
+ // Optionally scan the *current project's own* lifecycle scripts.
118
143
  if (config.scanSelf) {
119
144
  const selfResult = scanCwdProject(cwd, config);
120
145
  if (selfResult) scanned.unshift(selfResult);
121
- else skippedCount++;
122
146
  }
123
147
 
124
- // Attach metadata to results array
125
- scanned.skippedCount = skippedCount;
148
+ // Scan IDE/tool config files that can auto-execute code
149
+ const ideResults = scanIdeConfigs(cwd, config);
150
+ scanned.push(...ideResults);
151
+
152
+ scanned.totalPackages = allPackages.length + ideResults.length;
126
153
  return scanned;
127
154
  }
128
155
 
@@ -155,6 +182,78 @@ function scanCwdProject(cwd, config) {
155
182
  return analyzeScriptsLocalFromDir(pkg, pkgJson, cwd, config);
156
183
  }
157
184
 
185
+ const IDE_CONFIG_FILES = [
186
+ '.vscode/tasks.json',
187
+ '.vscode/settings.json',
188
+ '.vscode/launch.json',
189
+ '.claude/settings.json',
190
+ ];
191
+
192
+ function scanIdeConfigs(cwd, config) {
193
+ const results = [];
194
+
195
+ for (const relPath of IDE_CONFIG_FILES) {
196
+ const fullPath = path.join(cwd, relPath);
197
+ if (!fs.existsSync(fullPath)) continue;
198
+
199
+ let code;
200
+ try { code = fs.readFileSync(fullPath, 'utf8'); } catch { continue; }
201
+
202
+ const result = detectObfuscation(code, config);
203
+ if (result.score === 0) continue;
204
+
205
+ results.push({
206
+ pkg: { name: relPath, version: '', self: true },
207
+ scripts: [{ lifecycle: 'ide-config', file: relPath, code, ...result }],
208
+ score: result.score,
209
+ findings: result.findings,
210
+ verdict: result.verdict,
211
+ });
212
+
213
+ // Also scan any files referenced by commands in tasks.json
214
+ if (relPath.endsWith('tasks.json')) {
215
+ const referenced = extractReferencedScripts(code, cwd);
216
+ for (const { file, scriptCode } of referenced) {
217
+ const scriptResult = detectObfuscation(scriptCode, config);
218
+ if (scriptResult.score > 0) {
219
+ const existing = results.find(r => r.pkg.name === relPath);
220
+ if (existing) {
221
+ existing.scripts.push({ lifecycle: 'task-script', file, code: scriptCode, ...scriptResult });
222
+ existing.findings.push(...scriptResult.findings);
223
+ if (scriptResult.score > existing.score) {
224
+ existing.score = scriptResult.score;
225
+ existing.verdict = verdictFromScore(scriptResult.score, config);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ return results;
234
+ }
235
+
236
+ function extractReferencedScripts(tasksJson, cwd) {
237
+ const scripts = [];
238
+ try {
239
+ const tasks = JSON.parse(tasksJson);
240
+ for (const task of tasks.tasks || []) {
241
+ if (!task.command) continue;
242
+ // Extract file paths from commands like "node .claude/setup.mjs"
243
+ const match = task.command.match(/(?:node|bun|deno|sh|bash|python)\s+([^\s]+)/);
244
+ if (match) {
245
+ const scriptPath = path.join(cwd, match[1]);
246
+ if (fs.existsSync(scriptPath)) {
247
+ try {
248
+ scripts.push({ file: match[1], scriptCode: fs.readFileSync(scriptPath, 'utf8') });
249
+ } catch {}
250
+ }
251
+ }
252
+ }
253
+ } catch {}
254
+ return scripts;
255
+ }
256
+
158
257
  /**
159
258
  * Analyze a package's lifecycle scripts using a directory root as the
160
259
  * filesystem base. Used for both node_modules packages and the CWD itself.
@@ -647,24 +746,55 @@ async function resolveSinglePackage(packageSpec, config) {
647
746
  const packages = [];
648
747
  const seen = new Set();
649
748
 
650
- function collectDeps(deps) {
651
- for (const [depName, range] of Object.entries(deps || {})) {
652
- if (seen.has(depName)) continue;
653
- seen.add(depName);
654
- // Extract the first clean semver from the range (e.g. "4.22.1 || ^5" → "4.22.1", "^5.1.0" → "5.1.0")
749
+ async function collectDeps(deps, recurse) {
750
+ const queue = Object.entries(deps || {}).filter(([depName]) => !seen.has(depName));
751
+ // Mark all as seen first to avoid duplicate fetches
752
+ for (const [depName] of queue) seen.add(depName);
753
+
754
+ const resolutions = await mapWithConcurrency(queue, config.parallelFetches, async ([depName, range]) => {
655
755
  const exactVersion = extractSemver(range);
656
- if (!exactVersion) continue; // skip unresolvable ranges — lockfile scan will cover them
657
- packages.push({
658
- name: depName,
659
- version: exactVersion,
660
- resolved: buildTarballUrl(depName, exactVersion, config.registry),
661
- integrity: '',
662
- hasInstallScript: false,
663
- dev: false,
664
- optional: false,
665
- inBundle: false,
666
- link: false,
667
- });
756
+ if (!exactVersion) return null;
757
+
758
+ let depScripts = false;
759
+ let depDeps = null;
760
+ let depTarball = buildTarballUrl(depName, exactVersion, config.registry);
761
+ let depIntegrity = '';
762
+
763
+ try {
764
+ const encodedDep = depName.startsWith('@') ? `@${encodeURIComponent(depName.slice(1))}` : encodeURIComponent(depName);
765
+ const depMeta = await fetchJSON(`${config.registry}/${encodedDep}`, { timeout: config.timeout });
766
+ const depData = depMeta.versions && depMeta.versions[exactVersion];
767
+ if (depData) {
768
+ depScripts = !!(depData.scripts &&
769
+ (depData.scripts.preinstall || depData.scripts.postinstall || depData.scripts.install));
770
+ depTarball = depData.dist && depData.dist.tarball || depTarball;
771
+ depIntegrity = depData.dist && depData.dist.integrity || '';
772
+ depDeps = depData.dependencies;
773
+ }
774
+ } catch {
775
+ // Failed to fetch dep metadata — add with what we have
776
+ }
777
+
778
+ return {
779
+ pkg: {
780
+ name: depName,
781
+ version: exactVersion,
782
+ resolved: depTarball,
783
+ integrity: depIntegrity,
784
+ hasInstallScript: depScripts,
785
+ dev: false,
786
+ optional: false,
787
+ inBundle: false,
788
+ link: false,
789
+ },
790
+ depDeps,
791
+ };
792
+ });
793
+
794
+ for (const r of resolutions) {
795
+ if (!r) continue;
796
+ packages.push(r.pkg);
797
+ if (recurse && r.depDeps) await collectDeps(r.depDeps, true);
668
798
  }
669
799
  }
670
800
 
@@ -682,7 +812,8 @@ async function resolveSinglePackage(packageSpec, config) {
682
812
  link: false,
683
813
  });
684
814
 
685
- collectDeps(versionData.dependencies);
815
+ seen.add(name);
816
+ await collectDeps(versionData.dependencies, !!config.deepResolve);
686
817
 
687
818
  return packages;
688
819
  }
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ class Marshaller {
4
+ constructor(name, title) {
5
+ this.name = name;
6
+ this.title = title;
7
+ }
8
+
9
+ check(code) {
10
+ return null;
11
+ }
12
+
13
+ async checkPackage(pkg, config) {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ module.exports = { Marshaller };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { Marshaller } = require('./base');
4
+
5
+ class Base64ExecMarshaller extends Marshaller {
6
+ constructor() {
7
+ super('encoded-decode', 'Encoded decode + execution detection');
8
+ }
9
+
10
+ check(code) {
11
+ const hasBase64 = /atob\s*\(|Buffer\.from\s*\([^)]*,\s*['"]base64['"]\)/.test(code);
12
+ const hasHexDecode = /Buffer\.from\s*\([^)]*,\s*['"]hex['"]\)/.test(code);
13
+ const hasExec = /\beval\s*\(|new\s+Function\s*\(|\.exec\s*\(|\(\s*0\s*,\s*eval\s*\)\s*\(/.test(code);
14
+ if (!hasBase64 && !hasHexDecode) return null;
15
+ const kind = hasBase64 ? 'Base64' : 'Hex';
16
+ if (!hasExec) {
17
+ return { name: 'encoded-decode', score: 3, detail: `${kind} decode found — verify usage` };
18
+ }
19
+ return { name: 'encoded-decode+exec', score: 8, detail: `${kind} decode with code execution found` };
20
+ }
21
+ }
22
+
23
+ module.exports = new Base64ExecMarshaller();
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { Marshaller } = require('./base');
4
+
5
+ class ChildProcessMarshaller extends Marshaller {
6
+ constructor() {
7
+ super('child-process', 'Shell / process execution detection');
8
+ }
9
+
10
+ check(code) {
11
+ const patterns = [
12
+ /require\s*\(\s*['"]child_process['"]\s*\)/,
13
+ /require\s*\(\s*['"]node:child_process['"]\s*\)/,
14
+ /require\s*\(\s*['"`][^'"`]*['"`](?:\s*\+\s*['"`][^'"`]*['"`])+\s*\)/,
15
+ /require\s*\(\s*[a-zA-Z_$][\w$]*\s*\[/,
16
+ /\bexec\s*\(/,
17
+ /\bspawn\s*\(/,
18
+ /\bexecSync\s*\(/,
19
+ /\bspawnSync\s*\(/,
20
+ /\bexecFile\s*\(/,
21
+ /\bexecFileSync\s*\(/,
22
+ /\bfork\s*\(/,
23
+ /require\s*\(\s*['"]worker_threads['"]\s*\)/,
24
+ /new\s+Worker\s*\(/,
25
+ ];
26
+ const matched = patterns.filter(p => p.test(code));
27
+ if (matched.length === 0) return null;
28
+ return { name: this.name, score: 5, detail: `Shell/process execution found (${matched.length} pattern(s))` };
29
+ }
30
+ }
31
+
32
+ module.exports = new ChildProcessMarshaller();
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { Marshaller } = require('./base');
7
+
8
+ const SNYK_CONFIG_FILE = '.config/configstore/snyk.json';
9
+
10
+ class CveMarshaller extends Marshaller {
11
+ constructor() {
12
+ super('known-vulnerability', 'Known vulnerability check (Snyk / OSV.dev)');
13
+ }
14
+
15
+ async checkPackage(pkg, config) {
16
+ if (!pkg.name || !pkg.version) return null;
17
+
18
+ try {
19
+ const token = this.getSnykToken();
20
+ const timeout = Math.min(config.timeout || 10000, 10000);
21
+ const result = token
22
+ ? await this.querySnyk(pkg.name, pkg.version, token, timeout)
23
+ : await this.queryOsv(pkg.name, pkg.version, timeout);
24
+
25
+ if (result.issuesCount === 0) return null;
26
+
27
+ if (result.isMalicious) {
28
+ return {
29
+ name: this.name,
30
+ score: 80,
31
+ detail: `Malicious package detected — ${result.issuesCount} advisory(ies) found`,
32
+ };
33
+ }
34
+
35
+ // Non-malicious CVEs are informational (WARN, never BLOCK)
36
+ let score = 4;
37
+ if (result.issuesCount >= 10) score = 6;
38
+ else if (result.issuesCount >= 5) score = 5;
39
+
40
+ const source = token ? 'Snyk' : 'OSV.dev';
41
+ return {
42
+ name: this.name,
43
+ score,
44
+ detail: `${result.issuesCount} known vulnerability(ies) via ${source}`,
45
+ };
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ getSnykToken() {
52
+ const token = process.env.SNYK_API_TOKEN || process.env.SNYK_TOKEN;
53
+ if (token) return token;
54
+
55
+ try {
56
+ const configPath = path.join(os.homedir(), SNYK_CONFIG_FILE);
57
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
58
+ if (cfg && cfg.api) return cfg.api;
59
+ } catch {
60
+ // No Snyk config
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ async querySnyk(packageName, packageVersion, token, timeout) {
67
+ const { fetchJSON } = require('../utils/fetcher');
68
+ const apiUrl = process.env.SNYK_API_URL || process.env.SNYK_API || 'https://snyk.io/api/v1/vuln/npm';
69
+ const url = `${apiUrl}/${encodeURIComponent(packageName + '@' + packageVersion)}`;
70
+
71
+ const data = await fetchJSON(url, {
72
+ timeout,
73
+ headers: { Authorization: `token ${token}` },
74
+ });
75
+
76
+ if (data && data.vulnerabilities) {
77
+ const isMalicious = data.vulnerabilities.some(v => v.title === 'Malicious Package');
78
+ return { issuesCount: data.vulnerabilities.length, isMalicious };
79
+ }
80
+ return { issuesCount: 0, isMalicious: false };
81
+ }
82
+
83
+ async queryOsv(packageName, packageVersion, timeout) {
84
+ const { fetchJSON } = require('../utils/fetcher');
85
+ const url = 'https://api.osv.dev/v1/query';
86
+
87
+ const body = JSON.stringify({
88
+ version: packageVersion,
89
+ package: { name: packageName, ecosystem: 'npm' },
90
+ });
91
+
92
+ const data = await fetchJSON(url, {
93
+ timeout,
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body,
97
+ });
98
+
99
+ if (data && data.vulns && data.vulns.length > 0) {
100
+ const isMalicious = data.vulns.some(vuln => {
101
+ const ds = vuln.database_specific;
102
+ if (ds && ds['malicious-packages-origins'] && Array.isArray(ds['malicious-packages-origins'])) {
103
+ return true;
104
+ }
105
+ if (typeof vuln.summary === 'string' && vuln.summary.toLowerCase().startsWith('malicious')) {
106
+ return true;
107
+ }
108
+ return false;
109
+ });
110
+ return { issuesCount: data.vulns.length, isMalicious };
111
+ }
112
+ return { issuesCount: 0, isMalicious: false };
113
+ }
114
+ }
115
+
116
+ module.exports = new CveMarshaller();
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const { Marshaller } = require('./base');
4
+
5
+ class EvalMarshaller extends Marshaller {
6
+ constructor() {
7
+ super('eval/dynamic-exec', 'Eval / dynamic code execution');
8
+ }
9
+
10
+ check(code) {
11
+ const patterns = [
12
+ /\beval\s*\(/,
13
+ /new\s+Function\s*\(/,
14
+ /vm\.runInThisContext\s*\(/,
15
+ /vm\.runInNewContext\s*\(/,
16
+ /vm\.Script\s*\(/,
17
+ /\(\s*0\s*,\s*eval\s*\)\s*\(/,
18
+ /(?:global|globalThis|window|self|this)\s*\[\s*['"`](?:eval|Function)['"`]\s*\]\s*\(/,
19
+ /(?:global|globalThis|window|self|this)\s*\[\s*['"`][^'"`]*['"`](?:\s*\+\s*['"`][^'"`]*['"`]){1,}\s*\]\s*\(/,
20
+ /\.constructor\s*\.\s*constructor\s*\(/,
21
+ /\b(?:setTimeout|setInterval)\s*\(\s*['"`]/,
22
+ /require\s*\(\s*['"]vm['"]\s*\)/,
23
+ ];
24
+ const matched = patterns.filter(p => p.test(code));
25
+ if (matched.length === 0) return null;
26
+ return { name: this.name, score: 8, detail: `eval-like call found (${matched.length} pattern(s))` };
27
+ }
28
+ }
29
+
30
+ module.exports = new EvalMarshaller();
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { Marshaller } = require('./base');
4
+
5
+ class FilesystemManipulationMarshaller extends Marshaller {
6
+ constructor() {
7
+ super('filesystem-manipulation', 'Filesystem manipulation detection');
8
+ }
9
+
10
+ check(code) {
11
+ const writePatterns = [
12
+ /fs\.write(?:File)?(?:Sync)?\s*\(/,
13
+ /fs\.append(?:File)?(?:Sync)?\s*\(/,
14
+ /fs\.create(?:WriteStream)?\s*\(/,
15
+ /\.pipe\s*\(/,
16
+ ];
17
+ const permissionPatterns = [
18
+ /fs\.chmod(?:Sync)?\s*\(/,
19
+ /fs\.chown(?:Sync)?\s*\(/,
20
+ /fs\.access(?:Sync)?\s*\(/,
21
+ ];
22
+ const linkPatterns = [
23
+ /fs\.symlink(?:Sync)?\s*\(/,
24
+ /fs\.link(?:Sync)?\s*\(/,
25
+ ];
26
+
27
+ const writeMatches = writePatterns.filter(p => p.test(code)).length;
28
+ const permMatches = permissionPatterns.filter(p => p.test(code)).length;
29
+ const linkMatches = linkPatterns.filter(p => p.test(code)).length;
30
+
31
+ if (writeMatches === 0 && permMatches === 0 && linkMatches === 0) return null;
32
+
33
+ const details = [];
34
+ if (writeMatches > 0) details.push(`${writeMatches} write operation(s)`);
35
+ if (permMatches > 0) details.push(`${permMatches} permission change(s)`);
36
+ if (linkMatches > 0) details.push(`${linkMatches} symlink operation(s)`);
37
+
38
+ let score = 3;
39
+ if ((writeMatches > 0 ? 1 : 0) + (permMatches > 0 ? 1 : 0) + (linkMatches > 0 ? 1 : 0) >= 2) {
40
+ score = 4;
41
+ }
42
+
43
+ return { name: this.name, score, detail: details.join(', ') };
44
+ }
45
+ }
46
+
47
+ module.exports = new FilesystemManipulationMarshaller();
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { Marshaller } = require('./base');
4
+
5
+ class FromCharCodeMarshaller extends Marshaller {
6
+ constructor() {
7
+ super('fromCharCode', 'String.fromCharCode obfuscation detection');
8
+ }
9
+
10
+ check(code) {
11
+ let maxArgs = 0;
12
+ const direct = /(?:String|[\w$]+)\.fromCharCode\s*\(([^)]+)\)/g;
13
+ let match;
14
+ while ((match = direct.exec(code)) !== null) {
15
+ const args = match[1].split(',').filter(a => /^\s*\d+\s*$/.test(a));
16
+ if (args.length > maxArgs) maxArgs = args.length;
17
+ }
18
+
19
+ const arrRe = /\[\s*((?:\d{1,3}\s*,\s*){7,}\d{1,3})\s*\]/g;
20
+ let arrMatch;
21
+ let maxArr = 0;
22
+ while ((arrMatch = arrRe.exec(code)) !== null) {
23
+ const nums = arrMatch[1].split(',')
24
+ .map(s => parseInt(s.trim(), 10))
25
+ .filter(n => !Number.isNaN(n));
26
+ const printable = nums.filter(n => n >= 32 && n <= 126).length;
27
+ if (printable / nums.length >= 0.9 && nums.length > maxArr) maxArr = nums.length;
28
+ }
29
+
30
+ if (maxArgs >= 5) {
31
+ return { name: this.name, score: 7, detail: `fromCharCode with ${maxArgs} numeric args` };
32
+ }
33
+ if (maxArr >= 16) {
34
+ return { name: this.name, score: 7, detail: `decimal char-code array of length ${maxArr}` };
35
+ }
36
+ return null;
37
+ }
38
+ }
39
+
40
+ module.exports = new FromCharCodeMarshaller();
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const { Marshaller } = require('./base');
4
+
5
+ class HexArrayMarshaller extends Marshaller {
6
+ constructor() {
7
+ super('hex-array', 'Hex literal array detection');
8
+ }
9
+
10
+ check(code) {
11
+ const hexLiterals = (code.match(/\b0x[0-9a-fA-F]+\b/g) || []).length;
12
+ if (hexLiterals < 20) return null;
13
+ let score = 7;
14
+ if (hexLiterals > 2000) score = 60;
15
+ else if (hexLiterals > 500) score = 40;
16
+ else if (hexLiterals > 100) score = 20;
17
+ return { name: this.name, score, detail: `${hexLiterals} hex literal values found` };
18
+ }
19
+ }
20
+
21
+ module.exports = new HexArrayMarshaller();