pompelmi 0.15.0-dev.27 → 0.15.0-dev.29

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,221 +1,223 @@
1
1
  'use strict';
2
2
 
3
- async function createRemoteEngine(opts) {
4
- const { endpoint, headers = {}, rulesField = 'rules', fileField = 'file', mode = 'multipart', rulesAsBase64 = false, } = opts;
5
- const engine = {
6
- async compile(rulesSource) {
7
- return {
8
- async scan(data) {
9
- const fetchFn = globalThis.fetch;
10
- if (!fetchFn)
11
- throw new Error('[remote-yara] fetch non disponibile in questo ambiente');
12
- let res;
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
- // Helpers
55
- function base64FromBytes(bytes) {
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 base64FromString(s) {
64
- const btoaFn = globalThis.btoa;
65
- return btoaFn ? btoaFn(s) : Buffer.from(s, 'utf8').toString('base64');
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
- // Factory: sceglie l'engine a runtime (Node o Browser)
69
- // (Per ora i moduli chiamati lanceranno "non implementato")
70
- async function createYaraEngine() {
71
- const isNode = typeof process !== 'undefined' && !!process.versions?.node;
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
- function sniffMagicBytes(bytes) {
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 hasSuspiciousJpegTrailer(bytes, maxTrailer = 1000000) {
108
- for (let i = bytes.length - 2; i >= 2; i--) {
109
- if (bytes[i] === 0xFF && bytes[i + 1] === 0xD9) {
110
- const trailer = bytes.length - (i + 2);
111
- return trailer > maxTrailer;
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
- function prefilterBrowser(bytes, filename, policy) {
117
- const reasons = [];
118
- const ext = (filename.split('.').pop() || '').toLowerCase();
119
- if (policy.maxFileSizeBytes && bytes.byteLength > policy.maxFileSizeBytes) {
120
- reasons.push(`size_exceeded:${bytes.byteLength}`);
121
- }
122
- if (policy.includeExtensions && policy.includeExtensions.length && !policy.includeExtensions.includes(ext)) {
123
- reasons.push(`ext_denied:${ext}`);
124
- }
125
- const s = sniffMagicBytes(bytes);
126
- if (!s)
127
- reasons.push('mime_unknown');
128
- if (s?.mime && policy.allowedMimeTypes && policy.allowedMimeTypes.length && !policy.allowedMimeTypes.includes(s.mime)) {
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
- const severity = reasons.length ? 'suspicious' : 'clean';
143
- return { severity, reasons, mime: s?.mime };
126
+ return out;
127
+ };
144
128
  }
145
- /**
146
- * Reads an array of File objects via FileReader and returns their text.
147
- */
148
- async function scanFiles(files) {
149
- const readText = (file) => new Promise((resolve, reject) => {
150
- const reader = new FileReader();
151
- reader.onload = () => resolve(reader.result);
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
- async function scanFilesWithYara(files, rulesSource) {
163
- // Prova a creare l'engine YARA (browser). Se non disponibile, prosegui silenziosamente.
164
- let compiled;
165
- try {
166
- const engine = await createYaraEngine(); // in browser userà l'engine WASM (prossimo step)
167
- compiled = await engine.compile(rulesSource); // compila UNA sola volta
168
- }
169
- catch (e) {
170
- console.warn('[yara] non disponibile o regole non compilate:', e);
171
- }
172
- const results = [];
173
- for (const file of files) {
174
- // 1) contenuto testuale (come nella tua scanFiles)
175
- const content = await file.text();
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
- * Scan files with fast browser heuristics + optional YARA.
193
- * Returns content, prefilter verdict, and YARA matches.
194
- */
195
- async function scanFilesWithHeuristicsAndYara(files, rulesSource, policy) {
196
- let compiled;
197
- try {
198
- const engine = await createYaraEngine();
199
- compiled = await engine.compile(rulesSource);
200
- }
201
- catch (e) {
202
- console.warn('[yara] non disponibile o regole non compilate:', e);
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 file of files) {
206
- const [content, bytes] = await Promise.all([file.text(), file.arrayBuffer().then(b => new Uint8Array(b))]);
207
- const prefilter = prefilterBrowser(bytes, file.name, policy);
208
- let matches = [];
209
- if (compiled) {
210
- try {
211
- // Optional short-circuit: only run YARA if needed. For now, we always run it if available.
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$1(buf) {
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$1(buf))
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.prefilterBrowser = prefilterBrowser;
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