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