np-audit 1.2.1 → 1.3.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/package.json +1 -1
- package/src/detector.js +81 -16
package/package.json
CHANGED
package/src/detector.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const MAX_CODE_SIZE = 500000; // 500KB - chunk larger files
|
|
6
|
+
|
|
3
7
|
// ─── Individual detection checks ─────────────────────────────────────────────
|
|
4
8
|
|
|
5
9
|
/**
|
|
@@ -22,31 +26,57 @@ function checkEval(code) {
|
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Detect obfuscator.io signature: _0x variable naming.
|
|
29
|
+
* Score scales with density of obfuscation.
|
|
25
30
|
* @param {string} code
|
|
26
31
|
* @returns {Finding|null}
|
|
27
32
|
*/
|
|
28
33
|
function checkObfuscatorIo(code) {
|
|
29
34
|
const matches = code.match(/_0x[0-9a-fA-F]+/g) || [];
|
|
30
35
|
if (matches.length < 3) return null;
|
|
31
|
-
|
|
36
|
+
// Scale score: 3-10 = 9, 11-50 = 15, 51-200 = 30, 201-1000 = 50, 1000+ = 80
|
|
37
|
+
let score = 9;
|
|
38
|
+
if (matches.length > 1000) score = 80;
|
|
39
|
+
else if (matches.length > 200) score = 50;
|
|
40
|
+
else if (matches.length > 50) score = 30;
|
|
41
|
+
else if (matches.length > 10) score = 15;
|
|
42
|
+
return { name: 'obfuscator.io', score, detail: `${matches.length} _0x identifiers found` };
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
/**
|
|
35
46
|
* Detect high-entropy strings (likely encoded/encrypted payloads).
|
|
47
|
+
* Uses indexOf-based extraction to avoid regex stack overflow on large files.
|
|
36
48
|
* @param {string} code
|
|
37
49
|
* @returns {Finding|null}
|
|
38
50
|
*/
|
|
39
51
|
function checkHighEntropy(code) {
|
|
40
|
-
// Extract string literals (single, double, template)
|
|
41
|
-
const stringRe = /(?:"([^"\\]|\\.){50,}"|'([^'\\]|\\.){50,}'|`([^`\\]|\\.){50,}`)/g;
|
|
42
|
-
let match;
|
|
43
52
|
let maxEntropy = 0;
|
|
44
53
|
let worst = '';
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
const minLen = 50;
|
|
55
|
+
|
|
56
|
+
// Simple string extraction without complex regex
|
|
57
|
+
for (const quote of ['"', "'", '`']) {
|
|
58
|
+
let pos = 0;
|
|
59
|
+
while (pos < code.length) {
|
|
60
|
+
const start = code.indexOf(quote, pos);
|
|
61
|
+
if (start === -1) break;
|
|
62
|
+
|
|
63
|
+
// Find end quote (skip escaped quotes)
|
|
64
|
+
let end = start + 1;
|
|
65
|
+
while (end < code.length) {
|
|
66
|
+
if (code[end] === '\\') { end += 2; continue; }
|
|
67
|
+
if (code[end] === quote) break;
|
|
68
|
+
end++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (end < code.length && end - start - 1 >= minLen) {
|
|
72
|
+
const s = code.slice(start + 1, end);
|
|
73
|
+
const e = shannonEntropy(s);
|
|
74
|
+
if (e > maxEntropy) { maxEntropy = e; worst = s.slice(0, 40); }
|
|
75
|
+
}
|
|
76
|
+
pos = end + 1;
|
|
77
|
+
}
|
|
49
78
|
}
|
|
79
|
+
|
|
50
80
|
if (maxEntropy < 4.5) return null;
|
|
51
81
|
return {
|
|
52
82
|
name: 'high-entropy-string',
|
|
@@ -57,13 +87,19 @@ function checkHighEntropy(code) {
|
|
|
57
87
|
|
|
58
88
|
/**
|
|
59
89
|
* Detect dense hex escape sequences (\x41).
|
|
90
|
+
* Score scales with volume.
|
|
60
91
|
* @param {string} code
|
|
61
92
|
* @returns {Finding|null}
|
|
62
93
|
*/
|
|
63
94
|
function checkHexEscapes(code) {
|
|
64
95
|
const hexMatches = (code.match(/\\x[0-9a-fA-F]{2}/g) || []).length;
|
|
65
96
|
if (hexMatches < 10) return null;
|
|
66
|
-
|
|
97
|
+
// Scale: 10-50 = 5, 51-200 = 15, 201-1000 = 30, 1000+ = 50
|
|
98
|
+
let score = 5;
|
|
99
|
+
if (hexMatches > 1000) score = 50;
|
|
100
|
+
else if (hexMatches > 200) score = 30;
|
|
101
|
+
else if (hexMatches > 50) score = 15;
|
|
102
|
+
return { name: 'hex-escape-density', score, detail: `${hexMatches} \\xNN hex escapes found` };
|
|
67
103
|
}
|
|
68
104
|
|
|
69
105
|
/**
|
|
@@ -119,6 +155,7 @@ function checkChildProcess(code) {
|
|
|
119
155
|
|
|
120
156
|
/**
|
|
121
157
|
* Detect large hex literal arrays (common in minified obfuscated code).
|
|
158
|
+
* Score scales with volume.
|
|
122
159
|
* @param {string} code
|
|
123
160
|
* @returns {Finding|null}
|
|
124
161
|
*/
|
|
@@ -126,7 +163,12 @@ function checkHexArray(code) {
|
|
|
126
163
|
// Count 0x1234-style literals
|
|
127
164
|
const hexLiterals = (code.match(/\b0x[0-9a-fA-F]+\b/g) || []).length;
|
|
128
165
|
if (hexLiterals < 20) return null;
|
|
129
|
-
|
|
166
|
+
// Scale: 20-100 = 7, 101-500 = 20, 501-2000 = 40, 2000+ = 60
|
|
167
|
+
let score = 7;
|
|
168
|
+
if (hexLiterals > 2000) score = 60;
|
|
169
|
+
else if (hexLiterals > 500) score = 40;
|
|
170
|
+
else if (hexLiterals > 100) score = 20;
|
|
171
|
+
return { name: 'hex-array', score, detail: `${hexLiterals} hex literal values found` };
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
/**
|
|
@@ -190,22 +232,45 @@ const CHECKS = [
|
|
|
190
232
|
|
|
191
233
|
/**
|
|
192
234
|
* Run all checks against a code string.
|
|
235
|
+
* For large files, analyzes multiple chunks and aggregates results.
|
|
193
236
|
* @param {string} code
|
|
194
237
|
* @param {object} config { blockScore, warnScore }
|
|
195
238
|
* @returns {{ score: number, findings: Finding[], verdict: 'BLOCK'|'WARN'|'OK' }}
|
|
196
239
|
*/
|
|
197
|
-
function detectObfuscation(code, config = { blockScore:
|
|
240
|
+
function detectObfuscation(code, config = { blockScore: 50, warnScore: 20 }) {
|
|
198
241
|
if (!code || typeof code !== 'string') {
|
|
199
242
|
return { score: 0, findings: [], verdict: 'OK' };
|
|
200
243
|
}
|
|
201
244
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
245
|
+
// For large files, analyze chunks and take worst results
|
|
246
|
+
const chunks = [];
|
|
247
|
+
if (code.length > MAX_CODE_SIZE) {
|
|
248
|
+
// Analyze start, middle, and end chunks
|
|
249
|
+
chunks.push(code.slice(0, MAX_CODE_SIZE));
|
|
250
|
+
const mid = Math.floor(code.length / 2) - Math.floor(MAX_CODE_SIZE / 2);
|
|
251
|
+
chunks.push(code.slice(mid, mid + MAX_CODE_SIZE));
|
|
252
|
+
chunks.push(code.slice(-MAX_CODE_SIZE));
|
|
253
|
+
} else {
|
|
254
|
+
chunks.push(code);
|
|
206
255
|
}
|
|
207
256
|
|
|
208
|
-
|
|
257
|
+
const allFindings = new Map(); // Dedupe by name, keep highest score
|
|
258
|
+
|
|
259
|
+
for (const chunk of chunks) {
|
|
260
|
+
for (const check of CHECKS) {
|
|
261
|
+
const result = check(chunk);
|
|
262
|
+
if (result) {
|
|
263
|
+
const existing = allFindings.get(result.name);
|
|
264
|
+
if (!existing || result.score > existing.score) {
|
|
265
|
+
allFindings.set(result.name, result);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const findings = Array.from(allFindings.values());
|
|
272
|
+
|
|
273
|
+
// Score = highest individual finding score
|
|
209
274
|
const score = findings.length > 0
|
|
210
275
|
? Math.max(...findings.map(f => f.score))
|
|
211
276
|
: 0;
|