pompelmi 0.15.0-dev.27 → 0.15.0-dev.28
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/dist/pompelmi.cjs.js +277 -319
- package/dist/pompelmi.cjs.js.map +1 -1
- package/dist/pompelmi.esm.js +274 -317
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/types/browser-index.d.ts +1 -1
- package/dist/types/index.d.ts +2 -3
- package/dist/types/scan.d.ts +11 -40
- package/dist/types/useFileScanner.d.ts +2 -1
- package/package.json +1 -1
package/dist/pompelmi.cjs.js
CHANGED
|
@@ -1,221 +1,223 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (mode === 'multipart') {
|
|
14
|
-
const FormDataCtor = globalThis.FormData;
|
|
15
|
-
const BlobCtor = globalThis.Blob;
|
|
16
|
-
if (!FormDataCtor || !BlobCtor) {
|
|
17
|
-
throw new Error('[remote-yara] FormData/Blob non disponibili (usa json-base64 oppure esegui in browser)');
|
|
18
|
-
}
|
|
19
|
-
const form = new FormDataCtor();
|
|
20
|
-
form.set(rulesField, new BlobCtor([rulesSource], { type: 'text/plain' }), 'rules.yar');
|
|
21
|
-
form.set(fileField, new BlobCtor([data], { type: 'application/octet-stream' }), 'sample.bin');
|
|
22
|
-
res = await fetchFn(endpoint, { method: 'POST', body: form, headers });
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
const b64 = base64FromBytes(data);
|
|
26
|
-
const payload = { [fileField]: b64 };
|
|
27
|
-
if (rulesAsBase64) {
|
|
28
|
-
payload['rulesB64'] = base64FromString(rulesSource);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
payload[rulesField] = rulesSource;
|
|
32
|
-
}
|
|
33
|
-
res = await fetchFn(endpoint, {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
36
|
-
body: JSON.stringify(payload),
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
if (!res.ok) {
|
|
40
|
-
throw new Error(`[remote-yara] HTTP ${res.status} ${res.statusText}`);
|
|
41
|
-
}
|
|
42
|
-
const json = await res.json().catch(() => null);
|
|
43
|
-
const arr = Array.isArray(json) ? json : (json?.matches ?? []);
|
|
44
|
-
return (arr ?? []).map((m) => ({
|
|
45
|
-
rule: m.rule ?? m.ruleIdentifier ?? 'unknown',
|
|
46
|
-
tags: m.tags ?? [],
|
|
47
|
-
}));
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
return engine;
|
|
3
|
+
const SIG_CEN = 0x02014b50;
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
maxEntries: 1000,
|
|
6
|
+
maxTotalUncompressedBytes: 500 * 1024 * 1024,
|
|
7
|
+
maxEntryNameLength: 255,
|
|
8
|
+
maxCompressionRatio: 1000,
|
|
9
|
+
eocdSearchWindow: 70000,
|
|
10
|
+
};
|
|
11
|
+
function r16(buf, off) {
|
|
12
|
+
return buf.readUInt16LE(off);
|
|
53
13
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// usa btoa se disponibile (browser); altrimenti fallback manuale
|
|
57
|
-
const btoaFn = globalThis.btoa;
|
|
58
|
-
let bin = '';
|
|
59
|
-
for (let i = 0; i < bytes.byteLength; i++)
|
|
60
|
-
bin += String.fromCharCode(bytes[i]);
|
|
61
|
-
return btoaFn ? btoaFn(bin) : Buffer.from(bin, 'binary').toString('base64');
|
|
14
|
+
function r32(buf, off) {
|
|
15
|
+
return buf.readUInt32LE(off);
|
|
62
16
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
return
|
|
17
|
+
function isZipLike$1(buf) {
|
|
18
|
+
// local file header at start is common
|
|
19
|
+
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
|
|
66
20
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const target = isNode ? 'node' : 'browser';
|
|
73
|
-
const mod = await import(`./${target}`);
|
|
74
|
-
return isNode
|
|
75
|
-
? mod.createNodeEngine()
|
|
76
|
-
: mod.createBrowserEngine();
|
|
21
|
+
function lastIndexOfEOCD(buf, window) {
|
|
22
|
+
const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
|
23
|
+
const start = Math.max(0, buf.length - window);
|
|
24
|
+
const idx = buf.lastIndexOf(sig, Math.min(buf.length - sig.length, buf.length - 1));
|
|
25
|
+
return idx >= start ? idx : -1;
|
|
77
26
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const s = (sig) => {
|
|
81
|
-
const arr = typeof sig === 'string' ? new TextEncoder().encode(sig) : new Uint8Array(sig);
|
|
82
|
-
if (bytes.length < arr.length)
|
|
83
|
-
return false;
|
|
84
|
-
for (let i = 0; i < arr.length; i++)
|
|
85
|
-
if (bytes[i] !== arr[i])
|
|
86
|
-
return false;
|
|
87
|
-
return true;
|
|
88
|
-
};
|
|
89
|
-
if (s([0x50, 0x4B, 0x03, 0x04]) || s([0x50, 0x4B, 0x05, 0x06]) || s([0x50, 0x4B, 0x07, 0x08]))
|
|
90
|
-
return { mime: 'application/zip', extHint: 'zip' };
|
|
91
|
-
if (s([0x25, 0x50, 0x44, 0x46, 0x2D]))
|
|
92
|
-
return { mime: 'application/pdf', extHint: 'pdf' };
|
|
93
|
-
if (s([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))
|
|
94
|
-
return { mime: 'image/png', extHint: 'png' };
|
|
95
|
-
if (s([0xFF, 0xD8, 0xFF]))
|
|
96
|
-
return { mime: 'image/jpeg', extHint: 'jpg' };
|
|
97
|
-
if (s('GIF87a') || s('GIF89a'))
|
|
98
|
-
return { mime: 'image/gif', extHint: 'gif' };
|
|
99
|
-
if (s('<?xml') || s('<svg'))
|
|
100
|
-
return { mime: 'image/svg+xml', extHint: 'svg' };
|
|
101
|
-
if (s([0x4D, 0x5A]))
|
|
102
|
-
return { mime: 'application/vnd.microsoft.portable-executable', extHint: 'exe' };
|
|
103
|
-
if (s([0x7F, 0x45, 0x4C, 0x46]))
|
|
104
|
-
return { mime: 'application/x-elf', extHint: 'elf' };
|
|
105
|
-
return null;
|
|
27
|
+
function hasTraversal(name) {
|
|
28
|
+
return name.includes('../') || name.includes('..\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name);
|
|
106
29
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
30
|
+
function createZipBombGuard(opts = {}) {
|
|
31
|
+
const cfg = { ...DEFAULTS, ...opts };
|
|
32
|
+
return {
|
|
33
|
+
async scan(input) {
|
|
34
|
+
const buf = Buffer.from(input);
|
|
35
|
+
const matches = [];
|
|
36
|
+
if (!isZipLike$1(buf))
|
|
37
|
+
return matches;
|
|
38
|
+
// Find EOCD near the end
|
|
39
|
+
const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
|
|
40
|
+
if (eocdPos < 0 || eocdPos + 22 > buf.length) {
|
|
41
|
+
// ZIP but no EOCD — malformed or polyglot → suspicious
|
|
42
|
+
matches.push({ rule: 'zip_eocd_not_found', severity: 'medium' });
|
|
43
|
+
return matches;
|
|
44
|
+
}
|
|
45
|
+
const totalEntries = r16(buf, eocdPos + 10);
|
|
46
|
+
const cdSize = r32(buf, eocdPos + 12);
|
|
47
|
+
const cdOffset = r32(buf, eocdPos + 16);
|
|
48
|
+
// Bounds check
|
|
49
|
+
if (cdOffset + cdSize > buf.length) {
|
|
50
|
+
matches.push({ rule: 'zip_cd_out_of_bounds', severity: 'medium' });
|
|
51
|
+
return matches;
|
|
52
|
+
}
|
|
53
|
+
// Iterate central directory entries
|
|
54
|
+
let ptr = cdOffset;
|
|
55
|
+
let seen = 0;
|
|
56
|
+
let sumComp = 0;
|
|
57
|
+
let sumUnc = 0;
|
|
58
|
+
while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
|
|
59
|
+
const sig = r32(buf, ptr);
|
|
60
|
+
if (sig !== SIG_CEN)
|
|
61
|
+
break; // stop if structure breaks
|
|
62
|
+
const compSize = r32(buf, ptr + 20);
|
|
63
|
+
const uncSize = r32(buf, ptr + 24);
|
|
64
|
+
const fnLen = r16(buf, ptr + 28);
|
|
65
|
+
const exLen = r16(buf, ptr + 30);
|
|
66
|
+
const cmLen = r16(buf, ptr + 32);
|
|
67
|
+
const nameStart = ptr + 46;
|
|
68
|
+
const nameEnd = nameStart + fnLen;
|
|
69
|
+
if (nameEnd > buf.length)
|
|
70
|
+
break;
|
|
71
|
+
const name = buf.toString('utf8', nameStart, nameEnd);
|
|
72
|
+
sumComp += compSize;
|
|
73
|
+
sumUnc += uncSize;
|
|
74
|
+
seen++;
|
|
75
|
+
if (name.length > cfg.maxEntryNameLength) {
|
|
76
|
+
matches.push({ rule: 'zip_entry_name_too_long', severity: 'medium', meta: { name, length: name.length } });
|
|
77
|
+
}
|
|
78
|
+
if (hasTraversal(name)) {
|
|
79
|
+
matches.push({ rule: 'zip_path_traversal_entry', severity: 'medium', meta: { name } });
|
|
80
|
+
}
|
|
81
|
+
// move to next entry
|
|
82
|
+
ptr = nameEnd + exLen + cmLen;
|
|
83
|
+
}
|
|
84
|
+
if (seen !== totalEntries) {
|
|
85
|
+
// central dir truncated/odd, still report what we found
|
|
86
|
+
matches.push({ rule: 'zip_cd_truncated', severity: 'medium', meta: { seen, totalEntries } });
|
|
87
|
+
}
|
|
88
|
+
// Heuristics thresholds
|
|
89
|
+
if (seen > cfg.maxEntries) {
|
|
90
|
+
matches.push({ rule: 'zip_too_many_entries', severity: 'medium', meta: { seen, limit: cfg.maxEntries } });
|
|
91
|
+
}
|
|
92
|
+
if (sumUnc > cfg.maxTotalUncompressedBytes) {
|
|
93
|
+
matches.push({
|
|
94
|
+
rule: 'zip_total_uncompressed_too_large',
|
|
95
|
+
severity: 'medium',
|
|
96
|
+
meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (sumComp === 0 && sumUnc > 0) {
|
|
100
|
+
matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio: Infinity } });
|
|
101
|
+
}
|
|
102
|
+
else if (sumComp > 0) {
|
|
103
|
+
const ratio = sumUnc / Math.max(1, sumComp);
|
|
104
|
+
if (ratio >= cfg.maxCompressionRatio) {
|
|
105
|
+
matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio, limit: cfg.maxCompressionRatio } });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return matches;
|
|
112
109
|
}
|
|
113
|
-
}
|
|
114
|
-
return false;
|
|
110
|
+
};
|
|
115
111
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
reasons.push(`mime_denied:${s.mime}`);
|
|
130
|
-
}
|
|
131
|
-
if (s?.extHint && ext && s.extHint !== ext) {
|
|
132
|
-
reasons.push(`ext_mismatch:${ext}->${s.extHint}`);
|
|
133
|
-
}
|
|
134
|
-
if (s?.mime === 'image/jpeg' && hasSuspiciousJpegTrailer(bytes))
|
|
135
|
-
reasons.push('jpeg_trailer_payload');
|
|
136
|
-
if (s?.mime === 'image/svg+xml' && policy.denyScriptableSvg !== false) {
|
|
137
|
-
const text = new TextDecoder().decode(bytes).toLowerCase();
|
|
138
|
-
if (text.includes('<script') || text.includes('onload=') || text.includes('href="javascript:')) {
|
|
139
|
-
reasons.push('svg_script');
|
|
112
|
+
|
|
113
|
+
/** Risolve uno Scanner (fn o oggetto con .scan) in una funzione */
|
|
114
|
+
function asScanFn(s) {
|
|
115
|
+
return typeof s === 'function' ? s : s.scan;
|
|
116
|
+
}
|
|
117
|
+
/** Composizione sequenziale: concatena tutti i match degli scanner */
|
|
118
|
+
function composeScanners(scanners) {
|
|
119
|
+
return async (input, ctx) => {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const s of scanners) {
|
|
122
|
+
const res = await Promise.resolve(asScanFn(s)(input, ctx));
|
|
123
|
+
if (Array.isArray(res) && res.length)
|
|
124
|
+
out.push(...res);
|
|
140
125
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return { severity, reasons, mime: s?.mime };
|
|
126
|
+
return out;
|
|
127
|
+
};
|
|
144
128
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
reader.onerror = () => reject(reader.error);
|
|
153
|
-
reader.readAsText(file);
|
|
154
|
-
});
|
|
155
|
-
const results = [];
|
|
156
|
-
for (const file of files) {
|
|
157
|
-
const content = await readText(file);
|
|
158
|
-
results.push({ file, content });
|
|
159
|
-
}
|
|
160
|
-
return results;
|
|
129
|
+
/** Ritorna uno ScanFn pronto all'uso, oggi con zip-bomb guard */
|
|
130
|
+
function createPresetScanner(_name = 'zip-basic', _opts = {}) {
|
|
131
|
+
// Al momento un solo preset "zip-basic"
|
|
132
|
+
const scanners = [
|
|
133
|
+
createZipBombGuard(), // usa i default interni
|
|
134
|
+
];
|
|
135
|
+
return composeScanners(scanners);
|
|
161
136
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// 2) bytes per YARA (meglio dei soli caratteri)
|
|
177
|
-
let matches = [];
|
|
178
|
-
if (compiled) {
|
|
179
|
-
try {
|
|
180
|
-
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
181
|
-
matches = await compiled.scan(bytes);
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
console.warn(`[yara] errore scansione ${file.name}:`, e);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
results.push({ file, content, yara: { matches } });
|
|
137
|
+
|
|
138
|
+
/** Mappa veloce estensione -> mime (basic) */
|
|
139
|
+
function guessMimeByExt(name) {
|
|
140
|
+
if (!name)
|
|
141
|
+
return;
|
|
142
|
+
const ext = name.toLowerCase().split('.').pop();
|
|
143
|
+
switch (ext) {
|
|
144
|
+
case 'zip': return 'application/zip';
|
|
145
|
+
case 'png': return 'image/png';
|
|
146
|
+
case 'jpg':
|
|
147
|
+
case 'jpeg': return 'image/jpeg';
|
|
148
|
+
case 'pdf': return 'application/pdf';
|
|
149
|
+
case 'txt': return 'text/plain';
|
|
150
|
+
default: return;
|
|
188
151
|
}
|
|
189
|
-
return results;
|
|
190
152
|
}
|
|
191
|
-
/**
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
153
|
+
/** Heuristica semplice per verdetto */
|
|
154
|
+
function computeVerdict(matches) {
|
|
155
|
+
if (!matches.length)
|
|
156
|
+
return 'clean';
|
|
157
|
+
// se la regola contiene 'zip_' lo marchiamo "suspicious"
|
|
158
|
+
const anyHigh = matches.some(m => (m.tags ?? []).includes('critical') || (m.tags ?? []).includes('high'));
|
|
159
|
+
return anyHigh ? 'malicious' : 'suspicious';
|
|
160
|
+
}
|
|
161
|
+
/** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
|
|
162
|
+
function toYaraMatches(ms) {
|
|
163
|
+
return ms.map(m => ({
|
|
164
|
+
rule: m.rule,
|
|
165
|
+
namespace: 'heuristics',
|
|
166
|
+
tags: ['heuristics'].concat(m.severity ? [m.severity] : []),
|
|
167
|
+
meta: m.meta,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/** Scan di bytes (browser/node) usando preset (default: zip-basic) */
|
|
171
|
+
async function scanBytes(input, opts = {}) {
|
|
172
|
+
const t0 = Date.now();
|
|
173
|
+
const preset = opts.preset ?? 'zip-basic';
|
|
174
|
+
const ctx = {
|
|
175
|
+
...opts.ctx,
|
|
176
|
+
mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
|
|
177
|
+
size: opts.ctx?.size ?? input.byteLength,
|
|
178
|
+
};
|
|
179
|
+
const scanFn = createPresetScanner(preset);
|
|
180
|
+
const matchesH = await scanFn(input, ctx);
|
|
181
|
+
const matches = toYaraMatches(matchesH);
|
|
182
|
+
const verdict = computeVerdict(matches);
|
|
183
|
+
const durationMs = Date.now() - t0;
|
|
184
|
+
return {
|
|
185
|
+
ok: verdict === 'clean',
|
|
186
|
+
verdict,
|
|
187
|
+
matches,
|
|
188
|
+
reasons: matches.map(m => m.rule),
|
|
189
|
+
file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
|
|
190
|
+
durationMs,
|
|
191
|
+
engine: 'heuristics',
|
|
192
|
+
truncated: false,
|
|
193
|
+
timedOut: false,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
|
|
197
|
+
async function scanFile(filePath, opts = {}) {
|
|
198
|
+
const [{ readFile, stat }, path] = await Promise.all([
|
|
199
|
+
import('fs/promises'),
|
|
200
|
+
import('path'),
|
|
201
|
+
]);
|
|
202
|
+
const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
|
|
203
|
+
const ctx = {
|
|
204
|
+
filename: path.basename(filePath),
|
|
205
|
+
mimeType: guessMimeByExt(filePath),
|
|
206
|
+
size: st.size,
|
|
207
|
+
};
|
|
208
|
+
return scanBytes(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), { ...opts, ctx });
|
|
209
|
+
}
|
|
210
|
+
/** Scan multipli File (browser) usando scanBytes + preset di default */
|
|
211
|
+
async function scanFiles(files, opts = {}) {
|
|
212
|
+
const list = Array.from(files);
|
|
204
213
|
const out = [];
|
|
205
|
-
for (const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
matches = await compiled.scan(bytes);
|
|
213
|
-
}
|
|
214
|
-
catch (e) {
|
|
215
|
-
console.warn(`[yara] errore scansione ${file.name}:`, e);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
out.push({ file, content, prefilter, yara: { matches } });
|
|
214
|
+
for (const f of list) {
|
|
215
|
+
const buf = new Uint8Array(await f.arrayBuffer());
|
|
216
|
+
const rep = await scanBytes(buf, {
|
|
217
|
+
...opts,
|
|
218
|
+
ctx: { filename: f.name, mimeType: f.type || guessMimeByExt(f.name), size: f.size },
|
|
219
|
+
});
|
|
220
|
+
out.push(rep);
|
|
219
221
|
}
|
|
220
222
|
return out;
|
|
221
223
|
}
|
|
@@ -3060,7 +3062,7 @@ function useFileScanner() {
|
|
|
3060
3062
|
setErrors(bad);
|
|
3061
3063
|
if (good.length) {
|
|
3062
3064
|
const scanned = await scanFiles(good);
|
|
3063
|
-
setResults(scanned);
|
|
3065
|
+
setResults(scanned.map((r, i) => ({ file: good[i], report: r })));
|
|
3064
3066
|
}
|
|
3065
3067
|
else {
|
|
3066
3068
|
setResults([]);
|
|
@@ -3069,6 +3071,71 @@ function useFileScanner() {
|
|
|
3069
3071
|
return { results, errors, onChange };
|
|
3070
3072
|
}
|
|
3071
3073
|
|
|
3074
|
+
async function createRemoteEngine(opts) {
|
|
3075
|
+
const { endpoint, headers = {}, rulesField = 'rules', fileField = 'file', mode = 'multipart', rulesAsBase64 = false, } = opts;
|
|
3076
|
+
const engine = {
|
|
3077
|
+
async compile(rulesSource) {
|
|
3078
|
+
return {
|
|
3079
|
+
async scan(data) {
|
|
3080
|
+
const fetchFn = globalThis.fetch;
|
|
3081
|
+
if (!fetchFn)
|
|
3082
|
+
throw new Error('[remote-yara] fetch non disponibile in questo ambiente');
|
|
3083
|
+
let res;
|
|
3084
|
+
if (mode === 'multipart') {
|
|
3085
|
+
const FormDataCtor = globalThis.FormData;
|
|
3086
|
+
const BlobCtor = globalThis.Blob;
|
|
3087
|
+
if (!FormDataCtor || !BlobCtor) {
|
|
3088
|
+
throw new Error('[remote-yara] FormData/Blob non disponibili (usa json-base64 oppure esegui in browser)');
|
|
3089
|
+
}
|
|
3090
|
+
const form = new FormDataCtor();
|
|
3091
|
+
form.set(rulesField, new BlobCtor([rulesSource], { type: 'text/plain' }), 'rules.yar');
|
|
3092
|
+
form.set(fileField, new BlobCtor([data], { type: 'application/octet-stream' }), 'sample.bin');
|
|
3093
|
+
res = await fetchFn(endpoint, { method: 'POST', body: form, headers });
|
|
3094
|
+
}
|
|
3095
|
+
else {
|
|
3096
|
+
const b64 = base64FromBytes(data);
|
|
3097
|
+
const payload = { [fileField]: b64 };
|
|
3098
|
+
if (rulesAsBase64) {
|
|
3099
|
+
payload['rulesB64'] = base64FromString(rulesSource);
|
|
3100
|
+
}
|
|
3101
|
+
else {
|
|
3102
|
+
payload[rulesField] = rulesSource;
|
|
3103
|
+
}
|
|
3104
|
+
res = await fetchFn(endpoint, {
|
|
3105
|
+
method: 'POST',
|
|
3106
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
3107
|
+
body: JSON.stringify(payload),
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
if (!res.ok) {
|
|
3111
|
+
throw new Error(`[remote-yara] HTTP ${res.status} ${res.statusText}`);
|
|
3112
|
+
}
|
|
3113
|
+
const json = await res.json().catch(() => null);
|
|
3114
|
+
const arr = Array.isArray(json) ? json : (json?.matches ?? []);
|
|
3115
|
+
return (arr ?? []).map((m) => ({
|
|
3116
|
+
rule: m.rule ?? m.ruleIdentifier ?? 'unknown',
|
|
3117
|
+
tags: m.tags ?? [],
|
|
3118
|
+
}));
|
|
3119
|
+
},
|
|
3120
|
+
};
|
|
3121
|
+
},
|
|
3122
|
+
};
|
|
3123
|
+
return engine;
|
|
3124
|
+
}
|
|
3125
|
+
// Helpers
|
|
3126
|
+
function base64FromBytes(bytes) {
|
|
3127
|
+
// usa btoa se disponibile (browser); altrimenti fallback manuale
|
|
3128
|
+
const btoaFn = globalThis.btoa;
|
|
3129
|
+
let bin = '';
|
|
3130
|
+
for (let i = 0; i < bytes.byteLength; i++)
|
|
3131
|
+
bin += String.fromCharCode(bytes[i]);
|
|
3132
|
+
return btoaFn ? btoaFn(bin) : Buffer.from(bin, 'binary').toString('base64');
|
|
3133
|
+
}
|
|
3134
|
+
function base64FromString(s) {
|
|
3135
|
+
const btoaFn = globalThis.btoa;
|
|
3136
|
+
return btoaFn ? btoaFn(s) : Buffer.from(s, 'utf8').toString('base64');
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3072
3139
|
// src/scan/remote.ts
|
|
3073
3140
|
/**
|
|
3074
3141
|
* Scansiona una lista di File nel browser usando il motore remoto via HTTP.
|
|
@@ -3123,7 +3190,7 @@ function isOleCfb(buf) {
|
|
|
3123
3190
|
const sig = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
|
|
3124
3191
|
return startsWith(buf, sig);
|
|
3125
3192
|
}
|
|
3126
|
-
function isZipLike
|
|
3193
|
+
function isZipLike(buf) {
|
|
3127
3194
|
// PK\x03\x04
|
|
3128
3195
|
return startsWith(buf, [0x50, 0x4b, 0x03, 0x04]);
|
|
3129
3196
|
}
|
|
@@ -3133,7 +3200,7 @@ function isPeExecutable(buf) {
|
|
|
3133
3200
|
}
|
|
3134
3201
|
/** OOXML macro hint via filename token in ZIP container */
|
|
3135
3202
|
function hasOoxmlMacros(buf) {
|
|
3136
|
-
if (!isZipLike
|
|
3203
|
+
if (!isZipLike(buf))
|
|
3137
3204
|
return false;
|
|
3138
3205
|
return hasAsciiToken(buf, 'vbaProject.bin');
|
|
3139
3206
|
}
|
|
@@ -3172,116 +3239,6 @@ const CommonHeuristicsScanner = {
|
|
|
3172
3239
|
}
|
|
3173
3240
|
};
|
|
3174
3241
|
|
|
3175
|
-
const SIG_CEN = 0x02014b50;
|
|
3176
|
-
const DEFAULTS = {
|
|
3177
|
-
maxEntries: 1000,
|
|
3178
|
-
maxTotalUncompressedBytes: 500 * 1024 * 1024,
|
|
3179
|
-
maxEntryNameLength: 255,
|
|
3180
|
-
maxCompressionRatio: 1000,
|
|
3181
|
-
eocdSearchWindow: 70000,
|
|
3182
|
-
};
|
|
3183
|
-
function r16(buf, off) {
|
|
3184
|
-
return buf.readUInt16LE(off);
|
|
3185
|
-
}
|
|
3186
|
-
function r32(buf, off) {
|
|
3187
|
-
return buf.readUInt32LE(off);
|
|
3188
|
-
}
|
|
3189
|
-
function isZipLike(buf) {
|
|
3190
|
-
// local file header at start is common
|
|
3191
|
-
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
|
|
3192
|
-
}
|
|
3193
|
-
function lastIndexOfEOCD(buf, window) {
|
|
3194
|
-
const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
|
3195
|
-
const start = Math.max(0, buf.length - window);
|
|
3196
|
-
const idx = buf.lastIndexOf(sig, Math.min(buf.length - sig.length, buf.length - 1));
|
|
3197
|
-
return idx >= start ? idx : -1;
|
|
3198
|
-
}
|
|
3199
|
-
function hasTraversal(name) {
|
|
3200
|
-
return name.includes('../') || name.includes('..\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name);
|
|
3201
|
-
}
|
|
3202
|
-
function createZipBombGuard(opts = {}) {
|
|
3203
|
-
const cfg = { ...DEFAULTS, ...opts };
|
|
3204
|
-
return {
|
|
3205
|
-
async scan(input) {
|
|
3206
|
-
const buf = Buffer.from(input);
|
|
3207
|
-
const matches = [];
|
|
3208
|
-
if (!isZipLike(buf))
|
|
3209
|
-
return matches;
|
|
3210
|
-
// Find EOCD near the end
|
|
3211
|
-
const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
|
|
3212
|
-
if (eocdPos < 0 || eocdPos + 22 > buf.length) {
|
|
3213
|
-
// ZIP but no EOCD — malformed or polyglot → suspicious
|
|
3214
|
-
matches.push({ rule: 'zip_eocd_not_found', severity: 'medium' });
|
|
3215
|
-
return matches;
|
|
3216
|
-
}
|
|
3217
|
-
const totalEntries = r16(buf, eocdPos + 10);
|
|
3218
|
-
const cdSize = r32(buf, eocdPos + 12);
|
|
3219
|
-
const cdOffset = r32(buf, eocdPos + 16);
|
|
3220
|
-
// Bounds check
|
|
3221
|
-
if (cdOffset + cdSize > buf.length) {
|
|
3222
|
-
matches.push({ rule: 'zip_cd_out_of_bounds', severity: 'medium' });
|
|
3223
|
-
return matches;
|
|
3224
|
-
}
|
|
3225
|
-
// Iterate central directory entries
|
|
3226
|
-
let ptr = cdOffset;
|
|
3227
|
-
let seen = 0;
|
|
3228
|
-
let sumComp = 0;
|
|
3229
|
-
let sumUnc = 0;
|
|
3230
|
-
while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
|
|
3231
|
-
const sig = r32(buf, ptr);
|
|
3232
|
-
if (sig !== SIG_CEN)
|
|
3233
|
-
break; // stop if structure breaks
|
|
3234
|
-
const compSize = r32(buf, ptr + 20);
|
|
3235
|
-
const uncSize = r32(buf, ptr + 24);
|
|
3236
|
-
const fnLen = r16(buf, ptr + 28);
|
|
3237
|
-
const exLen = r16(buf, ptr + 30);
|
|
3238
|
-
const cmLen = r16(buf, ptr + 32);
|
|
3239
|
-
const nameStart = ptr + 46;
|
|
3240
|
-
const nameEnd = nameStart + fnLen;
|
|
3241
|
-
if (nameEnd > buf.length)
|
|
3242
|
-
break;
|
|
3243
|
-
const name = buf.toString('utf8', nameStart, nameEnd);
|
|
3244
|
-
sumComp += compSize;
|
|
3245
|
-
sumUnc += uncSize;
|
|
3246
|
-
seen++;
|
|
3247
|
-
if (name.length > cfg.maxEntryNameLength) {
|
|
3248
|
-
matches.push({ rule: 'zip_entry_name_too_long', severity: 'medium', meta: { name, length: name.length } });
|
|
3249
|
-
}
|
|
3250
|
-
if (hasTraversal(name)) {
|
|
3251
|
-
matches.push({ rule: 'zip_path_traversal_entry', severity: 'medium', meta: { name } });
|
|
3252
|
-
}
|
|
3253
|
-
// move to next entry
|
|
3254
|
-
ptr = nameEnd + exLen + cmLen;
|
|
3255
|
-
}
|
|
3256
|
-
if (seen !== totalEntries) {
|
|
3257
|
-
// central dir truncated/odd, still report what we found
|
|
3258
|
-
matches.push({ rule: 'zip_cd_truncated', severity: 'medium', meta: { seen, totalEntries } });
|
|
3259
|
-
}
|
|
3260
|
-
// Heuristics thresholds
|
|
3261
|
-
if (seen > cfg.maxEntries) {
|
|
3262
|
-
matches.push({ rule: 'zip_too_many_entries', severity: 'medium', meta: { seen, limit: cfg.maxEntries } });
|
|
3263
|
-
}
|
|
3264
|
-
if (sumUnc > cfg.maxTotalUncompressedBytes) {
|
|
3265
|
-
matches.push({
|
|
3266
|
-
rule: 'zip_total_uncompressed_too_large',
|
|
3267
|
-
severity: 'medium',
|
|
3268
|
-
meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
|
|
3269
|
-
});
|
|
3270
|
-
}
|
|
3271
|
-
if (sumComp === 0 && sumUnc > 0) {
|
|
3272
|
-
matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio: Infinity } });
|
|
3273
|
-
}
|
|
3274
|
-
else if (sumComp > 0) {
|
|
3275
|
-
const ratio = sumUnc / Math.max(1, sumComp);
|
|
3276
|
-
if (ratio >= cfg.maxCompressionRatio) {
|
|
3277
|
-
matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio, limit: cfg.maxCompressionRatio } });
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
return matches;
|
|
3281
|
-
}
|
|
3282
|
-
};
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
3242
|
const MB = 1024 * 1024;
|
|
3286
3243
|
const DEFAULT_POLICY = {
|
|
3287
3244
|
includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf'],
|
|
@@ -3308,14 +3265,15 @@ function definePolicy(input = {}) {
|
|
|
3308
3265
|
|
|
3309
3266
|
exports.CommonHeuristicsScanner = CommonHeuristicsScanner;
|
|
3310
3267
|
exports.DEFAULT_POLICY = DEFAULT_POLICY;
|
|
3268
|
+
exports.composeScanners = composeScanners;
|
|
3269
|
+
exports.createPresetScanner = createPresetScanner;
|
|
3311
3270
|
exports.createZipBombGuard = createZipBombGuard;
|
|
3312
3271
|
exports.definePolicy = definePolicy;
|
|
3313
3272
|
exports.mapMatchesToVerdict = mapMatchesToVerdict;
|
|
3314
|
-
exports.
|
|
3273
|
+
exports.scanBytes = scanBytes;
|
|
3274
|
+
exports.scanFile = scanFile;
|
|
3315
3275
|
exports.scanFiles = scanFiles;
|
|
3316
|
-
exports.scanFilesWithHeuristicsAndYara = scanFilesWithHeuristicsAndYara;
|
|
3317
3276
|
exports.scanFilesWithRemoteYara = scanFilesWithRemoteYara;
|
|
3318
|
-
exports.scanFilesWithYara = scanFilesWithYara;
|
|
3319
3277
|
exports.useFileScanner = useFileScanner;
|
|
3320
3278
|
exports.validateFile = validateFile;
|
|
3321
3279
|
//# sourceMappingURL=pompelmi.cjs.js.map
|