pompelmi 0.34.10 → 0.35.1

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.
Files changed (112) hide show
  1. package/README.md +26 -15
  2. package/dist/pompelmi.audit.cjs +13 -15
  3. package/dist/pompelmi.audit.cjs.map +1 -1
  4. package/dist/pompelmi.audit.esm.js +13 -15
  5. package/dist/pompelmi.audit.esm.js.map +1 -1
  6. package/dist/pompelmi.browser.cjs +595 -557
  7. package/dist/pompelmi.browser.cjs.map +1 -1
  8. package/dist/pompelmi.browser.esm.js +595 -557
  9. package/dist/pompelmi.browser.esm.js.map +1 -1
  10. package/dist/pompelmi.cjs +2056 -2015
  11. package/dist/pompelmi.cjs.map +1 -1
  12. package/dist/pompelmi.esm.js +2056 -2015
  13. package/dist/pompelmi.esm.js.map +1 -1
  14. package/dist/pompelmi.hooks.cjs +2 -2
  15. package/dist/pompelmi.hooks.cjs.map +1 -1
  16. package/dist/pompelmi.hooks.esm.js +2 -2
  17. package/dist/pompelmi.hooks.esm.js.map +1 -1
  18. package/dist/pompelmi.policy-packs.cjs +74 -73
  19. package/dist/pompelmi.policy-packs.cjs.map +1 -1
  20. package/dist/pompelmi.policy-packs.esm.js +74 -73
  21. package/dist/pompelmi.policy-packs.esm.js.map +1 -1
  22. package/dist/pompelmi.quarantine.cjs +135 -133
  23. package/dist/pompelmi.quarantine.cjs.map +1 -1
  24. package/dist/pompelmi.quarantine.esm.js +135 -133
  25. package/dist/pompelmi.quarantine.esm.js.map +1 -1
  26. package/dist/pompelmi.react.cjs +595 -557
  27. package/dist/pompelmi.react.cjs.map +1 -1
  28. package/dist/pompelmi.react.esm.js +595 -557
  29. package/dist/pompelmi.react.esm.js.map +1 -1
  30. package/dist/types/audit.d.ts +12 -12
  31. package/dist/types/browser-index.d.ts +12 -12
  32. package/dist/types/config.d.ts +4 -4
  33. package/dist/types/engines/dynamic-taint.d.ts +1 -1
  34. package/dist/types/engines/hybrid-orchestrator.d.ts +1 -1
  35. package/dist/types/engines/hybrid-taint-integration.d.ts +6 -6
  36. package/dist/types/engines/taint-policies.d.ts +4 -4
  37. package/dist/types/hipaa-compliance.d.ts +2 -2
  38. package/dist/types/hooks.d.ts +2 -2
  39. package/dist/types/index.d.ts +20 -20
  40. package/dist/types/node/scanDir.d.ts +5 -5
  41. package/dist/types/policy-packs.d.ts +2 -2
  42. package/dist/types/presets.d.ts +3 -3
  43. package/dist/types/quarantine/index.d.ts +3 -3
  44. package/dist/types/quarantine/storage.d.ts +1 -1
  45. package/dist/types/quarantine/types.d.ts +3 -3
  46. package/dist/types/quarantine/workflow.d.ts +4 -4
  47. package/dist/types/react-index.d.ts +2 -2
  48. package/dist/types/risk.d.ts +1 -1
  49. package/dist/types/scan/remote.d.ts +2 -2
  50. package/dist/types/scan.d.ts +5 -5
  51. package/dist/types/scanners/common-heuristics.d.ts +1 -1
  52. package/dist/types/scanners/zip-bomb-guard.d.ts +1 -1
  53. package/dist/types/src/audit.d.ts +84 -0
  54. package/dist/types/src/browser-index.d.ts +29 -0
  55. package/dist/types/src/config.d.ts +143 -0
  56. package/dist/types/src/engines/dynamic-taint.d.ts +102 -0
  57. package/dist/types/src/engines/hybrid-orchestrator.d.ts +65 -0
  58. package/dist/types/src/engines/hybrid-taint-integration.d.ts +129 -0
  59. package/dist/types/src/engines/taint-policies.d.ts +84 -0
  60. package/dist/types/src/hipaa-compliance.d.ts +110 -0
  61. package/dist/types/src/hooks.d.ts +89 -0
  62. package/dist/types/src/index.d.ts +29 -0
  63. package/dist/types/src/magic.d.ts +7 -0
  64. package/dist/types/src/node/scanDir.d.ts +30 -0
  65. package/dist/types/src/policy-packs.d.ts +98 -0
  66. package/dist/types/src/policy.d.ts +12 -0
  67. package/dist/types/src/presets.d.ts +72 -0
  68. package/dist/types/src/quarantine/index.d.ts +18 -0
  69. package/dist/types/src/quarantine/storage.d.ts +77 -0
  70. package/dist/types/src/quarantine/types.d.ts +78 -0
  71. package/dist/types/src/quarantine/workflow.d.ts +97 -0
  72. package/dist/types/src/react-index.d.ts +13 -0
  73. package/dist/types/src/risk.d.ts +18 -0
  74. package/dist/types/src/scan/remote.d.ts +12 -0
  75. package/dist/types/src/scan.d.ts +17 -0
  76. package/dist/types/src/scanners/common-heuristics.d.ts +14 -0
  77. package/dist/types/src/scanners/zip-bomb-guard.d.ts +9 -0
  78. package/dist/types/src/scanners/zipTraversalGuard.d.ts +19 -0
  79. package/dist/types/src/stream.d.ts +10 -0
  80. package/dist/types/src/types/decompilation.d.ts +96 -0
  81. package/dist/types/src/types/taint-tracking.d.ts +495 -0
  82. package/dist/types/src/types.d.ts +48 -0
  83. package/dist/types/src/useFileScanner.d.ts +15 -0
  84. package/dist/types/src/utils/advanced-detection.d.ts +21 -0
  85. package/dist/types/src/utils/batch-scanner.d.ts +62 -0
  86. package/dist/types/src/utils/cache-manager.d.ts +95 -0
  87. package/dist/types/src/utils/export.d.ts +51 -0
  88. package/dist/types/src/utils/performance-metrics.d.ts +68 -0
  89. package/dist/types/src/utils/threat-intelligence.d.ts +96 -0
  90. package/dist/types/src/validate.d.ts +7 -0
  91. package/dist/types/src/verdict.d.ts +2 -0
  92. package/dist/types/src/yara/browser.d.ts +7 -0
  93. package/dist/types/src/yara/index.d.ts +17 -0
  94. package/dist/types/src/yara/node.d.ts +2 -0
  95. package/dist/types/src/yara/remote.d.ts +10 -0
  96. package/dist/types/src/yara-bridge.d.ts +3 -0
  97. package/dist/types/src/zip.d.ts +13 -0
  98. package/dist/types/types/decompilation.d.ts +4 -4
  99. package/dist/types/types/taint-tracking.d.ts +19 -19
  100. package/dist/types/types.d.ts +3 -3
  101. package/dist/types/useFileScanner.d.ts +1 -1
  102. package/dist/types/utils/advanced-detection.d.ts +1 -1
  103. package/dist/types/utils/batch-scanner.d.ts +3 -3
  104. package/dist/types/utils/cache-manager.d.ts +1 -1
  105. package/dist/types/utils/export.d.ts +2 -2
  106. package/dist/types/utils/threat-intelligence.d.ts +4 -4
  107. package/dist/types/verdict.d.ts +1 -1
  108. package/dist/types/yara/browser.d.ts +1 -1
  109. package/dist/types/yara/index.d.ts +1 -1
  110. package/dist/types/yara/node.d.ts +1 -1
  111. package/dist/types/yara/remote.d.ts +2 -2
  112. package/package.json +7 -7
@@ -1,8 +1,238 @@
1
1
  import { createHash } from 'crypto';
2
2
 
3
+ const MB$1 = 1024 * 1024;
4
+ const DEFAULT_POLICY = {
5
+ includeExtensions: ["zip", "png", "jpg", "jpeg", "pdf"],
6
+ allowedMimeTypes: ["application/zip", "image/png", "image/jpeg", "application/pdf", "text/plain"],
7
+ maxFileSizeBytes: 20 * MB$1,
8
+ timeoutMs: 5000,
9
+ concurrency: 4,
10
+ failClosed: true,
11
+ };
12
+ function definePolicy(input = {}) {
13
+ const p = { ...DEFAULT_POLICY, ...input };
14
+ if (!Array.isArray(p.includeExtensions))
15
+ throw new TypeError("includeExtensions must be string[]");
16
+ if (!Array.isArray(p.allowedMimeTypes))
17
+ throw new TypeError("allowedMimeTypes must be string[]");
18
+ if (!(Number.isFinite(p.maxFileSizeBytes) && p.maxFileSizeBytes > 0))
19
+ throw new TypeError("maxFileSizeBytes must be > 0");
20
+ if (!(Number.isFinite(p.timeoutMs) && p.timeoutMs > 0))
21
+ throw new TypeError("timeoutMs must be > 0");
22
+ if (!(Number.isInteger(p.concurrency) && p.concurrency > 0))
23
+ throw new TypeError("concurrency must be > 0");
24
+ return p;
25
+ }
26
+
27
+ /**
28
+ * Policy packs for Pompelmi.
29
+ *
30
+ * Pre-configured, named policies for common upload scenarios. Each pack
31
+ * defines the file type allowlist, size limits, and timeout appropriate for
32
+ * its use case.
33
+ *
34
+ * All packs are built on `definePolicy` and are fully overridable:
35
+ *
36
+ * ```ts
37
+ * import { POLICY_PACKS } from 'pompelmi/policy-packs';
38
+ *
39
+ * // Use a pack as-is:
40
+ * const policy = POLICY_PACKS['images-only'];
41
+ *
42
+ * // Or override individual fields:
43
+ * import { definePolicy } from 'pompelmi';
44
+ * const custom = definePolicy({ ...POLICY_PACKS['documents-only'], maxFileSizeBytes: 5 * 1024 * 1024 });
45
+ * ```
46
+ *
47
+ * These packs are *deterministic* and *descriptor-based* — they do not
48
+ * depend on any external threat intelligence feed.
49
+ *
50
+ * @module policy-packs
51
+ */
52
+ const KB = 1024;
53
+ const MB = 1024 * KB;
54
+ // ── Policy packs ──────────────────────────────────────────────────────────────
55
+ /**
56
+ * Documents-only policy.
57
+ *
58
+ * Appropriate for: document management APIs, PDF/Office file upload endpoints,
59
+ * data import pipelines.
60
+ *
61
+ * Allowed: PDF, Word (.docx/.doc), Excel (.xlsx/.xls), PowerPoint (.pptx/.ppt),
62
+ * CSV, plain text, JSON, YAML, ODT/ODS/ODP (OpenDocument).
63
+ * Max size: 25 MB.
64
+ */
65
+ const DOCUMENTS_ONLY = definePolicy({
66
+ includeExtensions: [
67
+ "pdf",
68
+ "doc",
69
+ "docx",
70
+ "xls",
71
+ "xlsx",
72
+ "ppt",
73
+ "pptx",
74
+ "odt",
75
+ "ods",
76
+ "odp",
77
+ "csv",
78
+ "txt",
79
+ "json",
80
+ "yaml",
81
+ "yml",
82
+ "md",
83
+ ],
84
+ allowedMimeTypes: [
85
+ "application/pdf",
86
+ "application/msword",
87
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
88
+ "application/vnd.ms-excel",
89
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
90
+ "application/vnd.ms-powerpoint",
91
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
92
+ "application/vnd.oasis.opendocument.text",
93
+ "application/vnd.oasis.opendocument.spreadsheet",
94
+ "application/vnd.oasis.opendocument.presentation",
95
+ "text/csv",
96
+ "text/plain",
97
+ "application/json",
98
+ "text/yaml",
99
+ "text/markdown",
100
+ ],
101
+ maxFileSizeBytes: 25 * MB,
102
+ timeoutMs: 10000,
103
+ concurrency: 4,
104
+ failClosed: true,
105
+ });
106
+ /**
107
+ * Images-only policy.
108
+ *
109
+ * Appropriate for: avatar uploads, product image APIs, content platforms with
110
+ * user-generated imagery.
111
+ *
112
+ * Allowed: JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP, ICO.
113
+ * Max size: 10 MB.
114
+ * Note: SVG is intentionally excluded — inline SVGs can contain scripts.
115
+ */
116
+ const IMAGES_ONLY = definePolicy({
117
+ includeExtensions: ["jpg", "jpeg", "png", "gif", "webp", "avif", "tiff", "tif", "bmp", "ico"],
118
+ allowedMimeTypes: [
119
+ "image/jpeg",
120
+ "image/png",
121
+ "image/gif",
122
+ "image/webp",
123
+ "image/avif",
124
+ "image/tiff",
125
+ "image/bmp",
126
+ "image/x-icon",
127
+ "image/vnd.microsoft.icon",
128
+ ],
129
+ maxFileSizeBytes: 10 * MB,
130
+ timeoutMs: 5000,
131
+ concurrency: 8,
132
+ failClosed: true,
133
+ });
134
+ /**
135
+ * Strict public-upload policy.
136
+ *
137
+ * Appropriate for: anonymous or low-trust upload endpoints, public APIs,
138
+ * any surface exposed to untrusted users.
139
+ *
140
+ * Aggressive size limit (5 MB), short timeout, fail-closed, narrow MIME
141
+ * allowlist. Only allows plain images and PDF.
142
+ */
143
+ const STRICT_PUBLIC_UPLOAD = definePolicy({
144
+ includeExtensions: ["jpg", "jpeg", "png", "webp", "pdf"],
145
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
146
+ maxFileSizeBytes: 5 * MB,
147
+ timeoutMs: 4000,
148
+ concurrency: 2,
149
+ failClosed: true,
150
+ });
151
+ /**
152
+ * Conservative default policy.
153
+ *
154
+ * A hardened version of the built-in `DEFAULT_POLICY` suitable for
155
+ * production without further customisation. Stricter size limit and
156
+ * shorter timeout than the permissive default.
157
+ */
158
+ const CONSERVATIVE_DEFAULT = definePolicy({
159
+ includeExtensions: ["zip", "png", "jpg", "jpeg", "pdf", "txt", "csv", "docx", "xlsx"],
160
+ allowedMimeTypes: [
161
+ "application/zip",
162
+ "image/png",
163
+ "image/jpeg",
164
+ "application/pdf",
165
+ "text/plain",
166
+ "text/csv",
167
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
168
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
169
+ ],
170
+ maxFileSizeBytes: 10 * MB,
171
+ timeoutMs: 8000,
172
+ concurrency: 4,
173
+ failClosed: true,
174
+ });
175
+ /**
176
+ * Archives policy.
177
+ *
178
+ * Appropriate for: endpoints that accept ZIP, tar, or compressed archives.
179
+ * Combines a generous size allowance with a longer timeout for deep inspection.
180
+ *
181
+ * NOTE: Pair this policy with `createZipBombGuard()` to defend against
182
+ * decompression-bomb attacks:
183
+ *
184
+ * ```ts
185
+ * import { composeScanners, createZipBombGuard, CommonHeuristicsScanner } from 'pompelmi';
186
+ * const scanner = composeScanners(
187
+ * [['zipGuard', createZipBombGuard()], ['heuristics', CommonHeuristicsScanner]]
188
+ * );
189
+ * ```
190
+ */
191
+ const ARCHIVES = definePolicy({
192
+ includeExtensions: ["zip", "tar", "gz", "tgz", "bz2", "xz", "7z", "rar"],
193
+ allowedMimeTypes: [
194
+ "application/zip",
195
+ "application/x-tar",
196
+ "application/gzip",
197
+ "application/x-bzip2",
198
+ "application/x-xz",
199
+ "application/x-7z-compressed",
200
+ "application/x-rar-compressed",
201
+ ],
202
+ maxFileSizeBytes: 100 * MB,
203
+ timeoutMs: 30000,
204
+ concurrency: 2,
205
+ failClosed: true,
206
+ });
207
+ /**
208
+ * Named map of all built-in policy packs.
209
+ *
210
+ * ```ts
211
+ * import { POLICY_PACKS } from 'pompelmi/policy-packs';
212
+ * const policy = POLICY_PACKS['strict-public-upload'];
213
+ * ```
214
+ */
215
+ const POLICY_PACKS = {
216
+ "documents-only": DOCUMENTS_ONLY,
217
+ "images-only": IMAGES_ONLY,
218
+ "strict-public-upload": STRICT_PUBLIC_UPLOAD,
219
+ "conservative-default": CONSERVATIVE_DEFAULT,
220
+ archives: ARCHIVES,
221
+ };
222
+ /**
223
+ * Look up a policy pack by name.
224
+ * Throws if the name is not recognised.
225
+ */
226
+ function getPolicyPack(name) {
227
+ const policy = POLICY_PACKS[name];
228
+ if (!policy)
229
+ throw new Error(`Unknown policy pack: '${name}'. Valid names: ${Object.keys(POLICY_PACKS).join(", ")}`);
230
+ return policy;
231
+ }
232
+
3
233
  function hasAsciiToken(buf, token) {
4
234
  // Use latin1 so we can safely search binary
5
- return buf.indexOf(token, 0, 'latin1') !== -1;
235
+ return buf.indexOf(token, 0, "latin1") !== -1;
6
236
  }
7
237
  function startsWith(buf, bytes) {
8
238
  if (buf.length < bytes.length)
@@ -18,7 +248,7 @@ function isPDF(buf) {
18
248
  }
19
249
  function isOleCfb(buf) {
20
250
  // D0 CF 11 E0 A1 B1 1A E1
21
- const sig = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
251
+ const sig = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
22
252
  return startsWith(buf, sig);
23
253
  }
24
254
  function isZipLike$1(buf) {
@@ -33,12 +263,12 @@ function isPeExecutable(buf) {
33
263
  function hasOoxmlMacros(buf) {
34
264
  if (!isZipLike$1(buf))
35
265
  return false;
36
- return hasAsciiToken(buf, 'vbaProject.bin');
266
+ return hasAsciiToken(buf, "vbaProject.bin");
37
267
  }
38
268
  /** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
39
269
  function pdfRiskTokens(buf) {
40
- const tokens = ['/JavaScript', '/OpenAction', '/AA', '/Launch'];
41
- return tokens.filter(t => hasAsciiToken(buf, t));
270
+ const tokens = ["/JavaScript", "/OpenAction", "/AA", "/Launch"];
271
+ return tokens.filter((t) => hasAsciiToken(buf, t));
42
272
  }
43
273
  const CommonHeuristicsScanner = {
44
274
  async scan(input) {
@@ -46,33 +276,37 @@ const CommonHeuristicsScanner = {
46
276
  const matches = [];
47
277
  // Office macros (OLE / OOXML)
48
278
  if (isOleCfb(buf)) {
49
- matches.push({ rule: 'office_ole_container', severity: 'suspicious' });
279
+ matches.push({ rule: "office_ole_container", severity: "suspicious" });
50
280
  }
51
281
  if (hasOoxmlMacros(buf)) {
52
- matches.push({ rule: 'office_ooxml_macros', severity: 'suspicious' });
282
+ matches.push({ rule: "office_ooxml_macros", severity: "suspicious" });
53
283
  }
54
284
  // PDF risky tokens
55
285
  if (isPDF(buf)) {
56
286
  const toks = pdfRiskTokens(buf);
57
287
  if (toks.length) {
58
288
  matches.push({
59
- rule: 'pdf_risky_actions',
60
- severity: 'suspicious',
61
- meta: { tokens: toks }
289
+ rule: "pdf_risky_actions",
290
+ severity: "suspicious",
291
+ meta: { tokens: toks },
62
292
  });
63
293
  }
64
294
  }
65
295
  // Executable header
66
296
  if (isPeExecutable(buf)) {
67
- matches.push({ rule: 'pe_executable_signature', severity: 'suspicious' });
297
+ matches.push({ rule: "pe_executable_signature", severity: "suspicious" });
68
298
  }
69
299
  // EICAR test file
70
300
  const EICAR_NEEDLE = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!";
71
301
  if (hasAsciiToken(buf, EICAR_NEEDLE)) {
72
- matches.push({ rule: 'eicar_test_file', severity: 'high', meta: { note: 'EICAR standard antivirus test file detected' } });
302
+ matches.push({
303
+ rule: "eicar_test_file",
304
+ severity: "high",
305
+ meta: { note: "EICAR standard antivirus test file detected" },
306
+ });
73
307
  }
74
308
  return matches;
75
- }
309
+ },
76
310
  };
77
311
 
78
312
  function toScanFn(s) {
@@ -111,7 +345,13 @@ async function runWithTimeout(fn, timeoutMs) {
111
345
  return fn();
112
346
  return new Promise((resolve, reject) => {
113
347
  const timer = setTimeout(() => reject(new Error("scanner timeout")), timeoutMs);
114
- fn().then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
348
+ fn().then((v) => {
349
+ clearTimeout(timer);
350
+ resolve(v);
351
+ }, (e) => {
352
+ clearTimeout(timer);
353
+ reject(e);
354
+ });
115
355
  });
116
356
  }
117
357
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -122,7 +362,9 @@ function composeScanners(...args) {
122
362
  if (Array.isArray(first) &&
123
363
  (first.length === 0 || (Array.isArray(first[0]) && typeof first[0][0] === "string"))) {
124
364
  const entries = first;
125
- const opts = rest.length > 0 && !Array.isArray(rest[0]) && typeof rest[0] !== "function" &&
365
+ const opts = rest.length > 0 &&
366
+ !Array.isArray(rest[0]) &&
367
+ typeof rest[0] !== "function" &&
126
368
  !(typeof rest[0] === "object" && rest[0] !== null && "scan" in rest[0])
127
369
  ? rest[0]
128
370
  : {};
@@ -130,7 +372,7 @@ function composeScanners(...args) {
130
372
  const all = [];
131
373
  if (opts.parallel) {
132
374
  // Parallel execution — collect all results then return
133
- const results = await Promise.allSettled(entries.map(([name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
375
+ const results = await Promise.allSettled(entries.map(([_name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
134
376
  for (let i = 0; i < results.length; i++) {
135
377
  const result = results[i];
136
378
  if (result.status === "fulfilled" && Array.isArray(result.value)) {
@@ -184,152 +426,61 @@ function composeScanners(...args) {
184
426
  };
185
427
  }
186
428
  function createPresetScanner(preset, opts = {}) {
187
- const scanners = [];
188
- // Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
189
- scanners.push(CommonHeuristicsScanner);
429
+ const baseScanners = [CommonHeuristicsScanner];
430
+ const dynamicScannerPromises = [];
190
431
  // Add decompilation scanners based on preset
191
- if (preset === 'decompilation-basic' || preset === 'decompilation-deep' ||
192
- preset === 'malware-analysis' || opts.enableDecompilation) {
193
- const depth = preset === 'decompilation-deep' ? 'deep' :
194
- preset === 'decompilation-basic' ? 'basic' :
195
- opts.decompilationDepth || 'basic';
196
- if (!opts.decompilationEngine || opts.decompilationEngine === 'binaryninja-hlil' || opts.decompilationEngine === 'both') {
197
- try {
198
- // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
199
- const importModule = new Function('specifier', 'return import(specifier)');
200
- importModule('@pompelmi/engine-binaryninja').then((mod) => {
201
- const binjaScanner = mod.createBinaryNinjaScanner({
202
- timeout: opts.decompilationTimeout || opts.timeout || 30000,
203
- depth,
204
- pythonPath: opts.pythonPath,
205
- binaryNinjaPath: opts.binaryNinjaPath
206
- });
207
- scanners.push(binjaScanner);
208
- }).catch(() => {
209
- // Binary Ninja engine not available - silently skip
210
- });
211
- }
212
- catch {
213
- // Engine not installed
214
- }
215
- }
216
- if (!opts.decompilationEngine || opts.decompilationEngine === 'ghidra-pcode' || opts.decompilationEngine === 'both') {
217
- try {
218
- // Dynamic import for Ghidra engine (when implemented) - using Function to bypass TypeScript type checking
219
- const importModule = new Function('specifier', 'return import(specifier)');
220
- importModule('@pompelmi/engine-ghidra').then((mod) => {
221
- const ghidraScanner = mod.createGhidraScanner({
222
- timeout: opts.decompilationTimeout || opts.timeout || 30000,
223
- depth,
224
- ghidraPath: opts.ghidraPath,
225
- analyzeHeadless: opts.analyzeHeadless
226
- });
227
- scanners.push(ghidraScanner);
228
- }).catch(() => {
229
- // Ghidra engine not available - silently skip
230
- });
231
- }
232
- catch {
233
- // Engine not installed
234
- }
432
+ if (preset === "decompilation-basic" ||
433
+ preset === "decompilation-deep" ||
434
+ preset === "malware-analysis" ||
435
+ opts.enableDecompilation) {
436
+ const depth = preset === "decompilation-deep" || preset === "malware-analysis"
437
+ ? "deep"
438
+ : preset === "decompilation-basic"
439
+ ? "basic"
440
+ : opts.decompilationDepth || "basic";
441
+ let importModule;
442
+ try {
443
+ // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
444
+ importModule = new Function("specifier", "return import(specifier)");
235
445
  }
236
- }
237
- if (scanners.length === 0) {
238
- // Fallback scanner that returns no matches
239
- return async (_input, _ctx) => {
240
- return [];
241
- };
242
- }
243
- return composeScanners(...scanners);
244
- }
245
-
246
- /**
247
- * Performance monitoring utilities for pompelmi scans
248
- * @module utils/performance-metrics
249
- */
250
- /**
251
- * Track performance metrics for a scan operation
252
- */
253
- class PerformanceTracker {
254
- constructor() {
255
- this.checkpoints = new Map();
256
- this.startTime = Date.now();
257
- }
258
- /**
259
- * Mark a checkpoint in the scan process
260
- */
261
- checkpoint(name) {
262
- this.checkpoints.set(name, Date.now());
263
- }
264
- /**
265
- * Get duration since start or since a specific checkpoint
266
- */
267
- getDuration(since) {
268
- const now = Date.now();
269
- if (since && this.checkpoints.has(since)) {
270
- return now - (this.checkpoints.get(since) ?? now);
446
+ catch {
447
+ importModule = undefined;
271
448
  }
272
- return now - this.startTime;
273
- }
274
- /**
275
- * Generate final metrics report
276
- */
277
- getMetrics(bytesScanned) {
278
- const totalDuration = this.getDuration();
279
- const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
280
- return {
281
- totalDurationMs: totalDuration,
282
- heuristicsDurationMs: this.checkpoints.has('heuristics_end')
283
- ? (this.checkpoints.get('heuristics_end') ?? 0) - (this.checkpoints.get('heuristics_start') ?? 0)
284
- : undefined,
285
- yaraDurationMs: this.checkpoints.has('yara_end')
286
- ? (this.checkpoints.get('yara_end') ?? 0) - (this.checkpoints.get('yara_start') ?? 0)
287
- : undefined,
288
- prepDurationMs: this.checkpoints.has('prep_end')
289
- ? (this.checkpoints.get('prep_end') ?? 0) - this.startTime
290
- : undefined,
291
- throughputBps: throughput,
292
- bytesScanned,
293
- startedAt: this.startTime,
294
- completedAt: Date.now(),
295
- };
296
- }
297
- }
298
- /**
299
- * Aggregate statistics from multiple scan reports
300
- */
301
- function aggregateScanStats(reports) {
302
- let cleanCount = 0;
303
- let suspiciousCount = 0;
304
- let maliciousCount = 0;
305
- let totalDuration = 0;
306
- let totalBytes = 0;
307
- let validDurationCount = 0;
308
- for (const report of reports) {
309
- if (report.verdict === 'clean')
310
- cleanCount++;
311
- else if (report.verdict === 'suspicious')
312
- suspiciousCount++;
313
- else if (report.verdict === 'malicious')
314
- maliciousCount++;
315
- if (report.durationMs !== undefined) {
316
- totalDuration += report.durationMs;
317
- validDurationCount++;
449
+ if (importModule &&
450
+ (!opts.decompilationEngine ||
451
+ opts.decompilationEngine === "binaryninja-hlil" ||
452
+ opts.decompilationEngine === "both")) {
453
+ dynamicScannerPromises.push(importModule("@pompelmi/engine-binaryninja")
454
+ .then((mod) => mod.createBinaryNinjaScanner({
455
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
456
+ depth,
457
+ pythonPath: opts.pythonPath,
458
+ binaryNinjaPath: opts.binaryNinjaPath,
459
+ }))
460
+ .catch(() => null));
318
461
  }
319
- if (report.file?.size !== undefined) {
320
- totalBytes += report.file.size;
462
+ if (importModule &&
463
+ (!opts.decompilationEngine ||
464
+ opts.decompilationEngine === "ghidra-pcode" ||
465
+ opts.decompilationEngine === "both")) {
466
+ dynamicScannerPromises.push(importModule("@pompelmi/engine-ghidra")
467
+ .then((mod) => mod.createGhidraScanner({
468
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
469
+ depth,
470
+ ghidraPath: opts.ghidraPath,
471
+ analyzeHeadless: opts.analyzeHeadless,
472
+ }))
473
+ .catch(() => null));
321
474
  }
322
475
  }
323
- const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
324
- const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
325
- return {
326
- totalScans: reports.length,
327
- cleanCount,
328
- suspiciousCount,
329
- maliciousCount,
330
- avgDurationMs: avgDuration,
331
- avgThroughputBps: avgThroughput,
332
- totalBytesScanned: totalBytes,
476
+ let composedScannerPromise;
477
+ const getComposedScanner = async () => {
478
+ composedScannerPromise ?? (composedScannerPromise = Promise.all(dynamicScannerPromises).then((dynamicScanners) => composeScanners(...baseScanners, ...dynamicScanners.filter((scanner) => scanner !== null))));
479
+ return composedScannerPromise;
480
+ };
481
+ return async (input, ctx) => {
482
+ const scanner = await getComposedScanner();
483
+ return scanner(input, ctx);
333
484
  };
334
485
  }
335
486
 
@@ -346,25 +497,25 @@ function detectPolyglot(bytes) {
346
497
  // Check for PDF/ZIP polyglot
347
498
  if (isPDFZipPolyglot(bytes)) {
348
499
  matches.push({
349
- rule: 'polyglot_pdf_zip',
350
- severity: 'high',
351
- meta: { description: 'File can be interpreted as both PDF and ZIP' },
500
+ rule: "polyglot_pdf_zip",
501
+ severity: "high",
502
+ meta: { description: "File can be interpreted as both PDF and ZIP" },
352
503
  });
353
504
  }
354
505
  // Check for image/script polyglot
355
506
  if (isImageScriptPolyglot(bytes)) {
356
507
  matches.push({
357
- rule: 'polyglot_image_script',
358
- severity: 'high',
359
- meta: { description: 'Image file contains executable script content' },
508
+ rule: "polyglot_image_script",
509
+ severity: "high",
510
+ meta: { description: "Image file contains executable script content" },
360
511
  });
361
512
  }
362
513
  // Check for GIFAR (GIF/JAR polyglot)
363
514
  if (isGIFAR(bytes)) {
364
515
  matches.push({
365
- rule: 'polyglot_gifar',
366
- severity: 'critical',
367
- meta: { description: 'GIF file contains Java archive' },
516
+ rule: "polyglot_gifar",
517
+ severity: "critical",
518
+ meta: { description: "GIF file contains Java archive" },
368
519
  });
369
520
  }
370
521
  return matches;
@@ -374,7 +525,7 @@ function detectPolyglot(bytes) {
374
525
  */
375
526
  function detectObfuscatedScripts(bytes) {
376
527
  const matches = [];
377
- const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
528
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
378
529
  // Check for common obfuscation patterns
379
530
  const obfuscationPatterns = [
380
531
  /eval\s*\(\s*unescape\s*\(/gi,
@@ -386,10 +537,10 @@ function detectObfuscatedScripts(bytes) {
386
537
  for (const pattern of obfuscationPatterns) {
387
538
  if (pattern.test(text)) {
388
539
  matches.push({
389
- rule: 'obfuscated_script',
390
- severity: 'medium',
540
+ rule: "obfuscated_script",
541
+ severity: "medium",
391
542
  meta: {
392
- description: 'Detected obfuscated script content',
543
+ description: "Detected obfuscated script content",
393
544
  pattern: pattern.source,
394
545
  },
395
546
  });
@@ -429,7 +580,10 @@ function isPDFZipPolyglot(bytes) {
429
580
  // Check for ZIP signature anywhere in the file
430
581
  let hasZIP = false;
431
582
  for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
432
- if (bytes[i] === 0x50 && bytes[i + 1] === 0x4B && bytes[i + 2] === 0x03 && bytes[i + 3] === 0x04) {
583
+ if (bytes[i] === 0x50 &&
584
+ bytes[i + 1] === 0x4b &&
585
+ bytes[i + 2] === 0x03 &&
586
+ bytes[i + 3] === 0x04) {
433
587
  hasZIP = true;
434
588
  break;
435
589
  }
@@ -440,14 +594,13 @@ function isImageScriptPolyglot(bytes) {
440
594
  if (bytes.length < 100)
441
595
  return false;
442
596
  // Check for image signatures
443
- const isImage = ((bytes[0] === 0xFF && bytes[1] === 0xD8) || // JPEG
444
- (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) || // PNG
445
- (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) // GIF
446
- );
597
+ const isImage = (bytes[0] === 0xff && bytes[1] === 0xd8) || // JPEG
598
+ (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) || // PNG
599
+ (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46); // GIF
447
600
  if (!isImage)
448
601
  return false;
449
602
  // Check for script content
450
- const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
603
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
451
604
  return /<script|javascript:|eval\(|function\s*\(/i.test(text);
452
605
  }
453
606
  function isGIFAR(bytes) {
@@ -458,7 +611,10 @@ function isGIFAR(bytes) {
458
611
  // Check for ZIP/JAR signature
459
612
  let hasZIP = false;
460
613
  for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
461
- if (bytes[i] === 0x50 && bytes[i + 1] === 0x4B && bytes[i + 2] === 0x03 && bytes[i + 3] === 0x04) {
614
+ if (bytes[i] === 0x50 &&
615
+ bytes[i + 1] === 0x4b &&
616
+ bytes[i + 2] === 0x03 &&
617
+ bytes[i + 3] === 0x04) {
462
618
  hasZIP = true;
463
619
  break;
464
620
  }
@@ -470,13 +626,13 @@ function isArchive(bytes) {
470
626
  return false;
471
627
  return (
472
628
  // ZIP
473
- (bytes[0] === 0x50 && bytes[1] === 0x4B && bytes[2] === 0x03 && bytes[3] === 0x04) ||
629
+ (bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) ||
474
630
  // RAR
475
631
  (bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
476
632
  // 7z
477
- (bytes[0] === 0x37 && bytes[1] === 0x7A && bytes[2] === 0xBC && bytes[3] === 0xAF) ||
633
+ (bytes[0] === 0x37 && bytes[1] === 0x7a && bytes[2] === 0xbc && bytes[3] === 0xaf) ||
478
634
  // tar.gz
479
- (bytes[0] === 0x1F && bytes[1] === 0x8B));
635
+ (bytes[0] === 0x1f && bytes[1] === 0x8b));
480
636
  }
481
637
 
482
638
  /**
@@ -504,10 +660,10 @@ class ScanCacheManager {
504
660
  * Generate cache key from file content
505
661
  */
506
662
  generateKey(content, preset) {
507
- const hash = createHash('sha256')
663
+ const hash = createHash("sha256")
508
664
  .update(content)
509
- .update(preset || 'default')
510
- .digest('hex');
665
+ .update(preset || "default")
666
+ .digest("hex");
511
667
  return hash;
512
668
  }
513
669
  /**
@@ -651,35 +807,132 @@ function getDefaultCache(options) {
651
807
  return defaultCache;
652
808
  }
653
809
 
654
- /** Mappa veloce estensione -> mime (basic) */
810
+ /**
811
+ * Performance monitoring utilities for pompelmi scans
812
+ * @module utils/performance-metrics
813
+ */
814
+ /**
815
+ * Track performance metrics for a scan operation
816
+ */
817
+ class PerformanceTracker {
818
+ constructor() {
819
+ this.checkpoints = new Map();
820
+ this.startTime = Date.now();
821
+ }
822
+ /**
823
+ * Mark a checkpoint in the scan process
824
+ */
825
+ checkpoint(name) {
826
+ this.checkpoints.set(name, Date.now());
827
+ }
828
+ /**
829
+ * Get duration since start or since a specific checkpoint
830
+ */
831
+ getDuration(since) {
832
+ const now = Date.now();
833
+ if (since && this.checkpoints.has(since)) {
834
+ return now - (this.checkpoints.get(since) ?? now);
835
+ }
836
+ return now - this.startTime;
837
+ }
838
+ /**
839
+ * Generate final metrics report
840
+ */
841
+ getMetrics(bytesScanned) {
842
+ const totalDuration = this.getDuration();
843
+ const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
844
+ return {
845
+ totalDurationMs: totalDuration,
846
+ heuristicsDurationMs: this.checkpoints.has("heuristics_end")
847
+ ? (this.checkpoints.get("heuristics_end") ?? 0) -
848
+ (this.checkpoints.get("heuristics_start") ?? 0)
849
+ : undefined,
850
+ yaraDurationMs: this.checkpoints.has("yara_end")
851
+ ? (this.checkpoints.get("yara_end") ?? 0) - (this.checkpoints.get("yara_start") ?? 0)
852
+ : undefined,
853
+ prepDurationMs: this.checkpoints.has("prep_end")
854
+ ? (this.checkpoints.get("prep_end") ?? 0) - this.startTime
855
+ : undefined,
856
+ throughputBps: throughput,
857
+ bytesScanned,
858
+ startedAt: this.startTime,
859
+ completedAt: Date.now(),
860
+ };
861
+ }
862
+ }
863
+ /**
864
+ * Aggregate statistics from multiple scan reports
865
+ */
866
+ function aggregateScanStats(reports) {
867
+ let cleanCount = 0;
868
+ let suspiciousCount = 0;
869
+ let maliciousCount = 0;
870
+ let totalDuration = 0;
871
+ let totalBytes = 0;
872
+ let validDurationCount = 0;
873
+ for (const report of reports) {
874
+ if (report.verdict === "clean")
875
+ cleanCount++;
876
+ else if (report.verdict === "suspicious")
877
+ suspiciousCount++;
878
+ else if (report.verdict === "malicious")
879
+ maliciousCount++;
880
+ if (report.durationMs !== undefined) {
881
+ totalDuration += report.durationMs;
882
+ validDurationCount++;
883
+ }
884
+ if (report.file?.size !== undefined) {
885
+ totalBytes += report.file.size;
886
+ }
887
+ }
888
+ const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
889
+ const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
890
+ return {
891
+ totalScans: reports.length,
892
+ cleanCount,
893
+ suspiciousCount,
894
+ maliciousCount,
895
+ avgDurationMs: avgDuration,
896
+ avgThroughputBps: avgThroughput,
897
+ totalBytesScanned: totalBytes,
898
+ };
899
+ }
900
+
901
+ /** Mappa veloce estensione -> mime (basic) */
655
902
  function guessMimeByExt(name) {
656
903
  if (!name)
657
904
  return;
658
- const ext = name.toLowerCase().split('.').pop();
905
+ const ext = name.toLowerCase().split(".").pop();
659
906
  switch (ext) {
660
- case 'zip': return 'application/zip';
661
- case 'png': return 'image/png';
662
- case 'jpg':
663
- case 'jpeg': return 'image/jpeg';
664
- case 'pdf': return 'application/pdf';
665
- case 'txt': return 'text/plain';
666
- default: return;
907
+ case "zip":
908
+ return "application/zip";
909
+ case "png":
910
+ return "image/png";
911
+ case "jpg":
912
+ case "jpeg":
913
+ return "image/jpeg";
914
+ case "pdf":
915
+ return "application/pdf";
916
+ case "txt":
917
+ return "text/plain";
918
+ default:
919
+ return;
667
920
  }
668
921
  }
669
922
  /** Heuristica semplice per verdetto */
670
923
  function computeVerdict(matches) {
671
924
  if (!matches.length)
672
- return 'clean';
925
+ return "clean";
673
926
  // se la regola contiene 'zip_' lo marchiamo "suspicious"
674
- const anyHigh = matches.some(m => (m.tags ?? []).includes('critical') || (m.tags ?? []).includes('high'));
675
- return anyHigh ? 'malicious' : 'suspicious';
927
+ const anyHigh = matches.some((m) => (m.tags ?? []).includes("critical") || (m.tags ?? []).includes("high"));
928
+ return anyHigh ? "malicious" : "suspicious";
676
929
  }
677
930
  /** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
678
931
  function toYaraMatches(ms) {
679
- return ms.map(m => ({
932
+ return ms.map((m) => ({
680
933
  rule: m.rule,
681
- namespace: 'heuristics',
682
- tags: ['heuristics'].concat(m.severity ? [m.severity] : []),
934
+ namespace: "heuristics",
935
+ tags: ["heuristics"].concat(m.severity ? [m.severity] : []),
683
936
  meta: m.meta,
684
937
  }));
685
938
  }
@@ -693,26 +946,28 @@ async function scanBytes(input, opts = {}) {
693
946
  return cached;
694
947
  }
695
948
  }
696
- const perfTracker = (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)
949
+ const perfTracker = opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking
697
950
  ? new PerformanceTracker()
698
951
  : null;
699
- perfTracker?.checkpoint('prep_start');
700
- const preset = opts.preset ?? opts.config?.defaultPreset ?? 'zip-basic';
952
+ perfTracker?.checkpoint("prep_start");
953
+ const preset = opts.preset ?? opts.config?.defaultPreset ?? "zip-basic";
701
954
  const ctx = {
702
955
  ...opts.ctx,
703
956
  mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
704
957
  size: opts.ctx?.size ?? input.byteLength,
705
958
  };
706
- perfTracker?.checkpoint('prep_end');
707
- perfTracker?.checkpoint('heuristics_start');
959
+ perfTracker?.checkpoint("prep_end");
960
+ perfTracker?.checkpoint("heuristics_start");
708
961
  const scanFn = createPresetScanner(preset);
709
- const matchesH = await (typeof scanFn === "function" ? scanFn : scanFn.scan)(input, ctx);
710
- let allMatches = [...matchesH];
711
- perfTracker?.checkpoint('heuristics_end');
962
+ const matchesH = await (typeof scanFn === "function"
963
+ ? scanFn
964
+ : scanFn.scan)(input, ctx);
965
+ const allMatches = [...matchesH];
966
+ perfTracker?.checkpoint("heuristics_end");
712
967
  // Advanced detection (enabled by default, can be overridden by config)
713
968
  const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
714
969
  if (advancedEnabled) {
715
- perfTracker?.checkpoint('advanced_start');
970
+ perfTracker?.checkpoint("advanced_start");
716
971
  // Detect polyglot files
717
972
  if (opts.config?.advanced?.enablePolyglotDetection !== false) {
718
973
  const polyglotMatches = detectPolyglot(input);
@@ -727,37 +982,38 @@ async function scanBytes(input, opts = {}) {
727
982
  if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
728
983
  const nestingAnalysis = analyzeNestedArchives(input);
729
984
  const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
730
- if (nestingAnalysis.hasExcessiveNesting || (nestingAnalysis.depth > maxDepth)) {
985
+ if (nestingAnalysis.hasExcessiveNesting || nestingAnalysis.depth > maxDepth) {
731
986
  allMatches.push({
732
- rule: 'excessive_archive_nesting',
733
- severity: 'high',
987
+ rule: "excessive_archive_nesting",
988
+ severity: "high",
734
989
  meta: {
735
- description: 'Excessive archive nesting detected',
990
+ description: "Excessive archive nesting detected",
736
991
  depth: nestingAnalysis.depth,
737
992
  maxAllowed: maxDepth,
738
993
  },
739
994
  });
740
995
  }
741
996
  }
742
- perfTracker?.checkpoint('advanced_end');
997
+ perfTracker?.checkpoint("advanced_end");
743
998
  }
744
999
  const matches = toYaraMatches(allMatches);
745
1000
  const verdict = computeVerdict(matches);
746
1001
  perfTracker ? perfTracker.getDuration() : Date.now();
747
1002
  const durationMs = perfTracker ? perfTracker.getDuration() : 0;
748
1003
  const report = {
749
- ok: verdict === 'clean',
1004
+ ok: verdict === "clean",
750
1005
  verdict,
751
1006
  matches,
752
- reasons: matches.map(m => m.rule),
1007
+ reasons: matches.map((m) => m.rule),
753
1008
  file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
754
1009
  durationMs,
755
- engine: 'heuristics',
1010
+ engine: "heuristics",
756
1011
  truncated: false,
757
1012
  timedOut: false,
758
1013
  };
759
1014
  // Add performance metrics if tracking enabled
760
- if (perfTracker && (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
1015
+ if (perfTracker &&
1016
+ (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
761
1017
  report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
762
1018
  }
763
1019
  // Cache result if enabled
@@ -771,10 +1027,7 @@ async function scanBytes(input, opts = {}) {
771
1027
  }
772
1028
  /** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
773
1029
  async function scanFile(filePath, opts = {}) {
774
- const [{ readFile, stat }, path] = await Promise.all([
775
- import('fs/promises'),
776
- import('path'),
777
- ]);
1030
+ const [{ readFile, stat }, path] = await Promise.all([import('fs/promises'), import('path')]);
778
1031
  const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
779
1032
  const ctx = {
780
1033
  filename: path.basename(filePath),
@@ -798,21 +1051,6 @@ async function scanFiles(files, opts = {}) {
798
1051
  return out;
799
1052
  }
800
1053
 
801
- /**
802
- * Validates a File by MIME type and size (max 5 MB).
803
- */
804
- function validateFile(file) {
805
- const maxSize = 5 * 1024 * 1024;
806
- const allowedTypes = ['text/plain', 'application/json', 'text/csv'];
807
- if (!allowedTypes.includes(file.type)) {
808
- return { valid: false, error: 'Unsupported file type' };
809
- }
810
- if (file.size > maxSize) {
811
- return { valid: false, error: 'File too large (max 5 MB)' };
812
- }
813
- return { valid: true };
814
- }
815
-
816
1054
  const SIG_CEN = 0x02014b50;
817
1055
  const DEFAULTS = {
818
1056
  maxEntries: 1000,
@@ -829,7 +1067,7 @@ function r32(buf, off) {
829
1067
  }
830
1068
  function isZipLike(buf) {
831
1069
  // local file header at start is common
832
- return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
1070
+ return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
833
1071
  }
834
1072
  function lastIndexOfEOCD(buf, window) {
835
1073
  const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
@@ -838,7 +1076,7 @@ function lastIndexOfEOCD(buf, window) {
838
1076
  return idx >= start ? idx : -1;
839
1077
  }
840
1078
  function hasTraversal(name) {
841
- return name.includes('../') || name.includes('..\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name);
1079
+ return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
842
1080
  }
843
1081
  function createZipBombGuard(opts = {}) {
844
1082
  const cfg = { ...DEFAULTS, ...opts };
@@ -852,7 +1090,7 @@ function createZipBombGuard(opts = {}) {
852
1090
  const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
853
1091
  if (eocdPos < 0 || eocdPos + 22 > buf.length) {
854
1092
  // ZIP but no EOCD — malformed or polyglot → suspicious
855
- matches.push({ rule: 'zip_eocd_not_found', severity: 'medium' });
1093
+ matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
856
1094
  return matches;
857
1095
  }
858
1096
  const totalEntries = r16(buf, eocdPos + 10);
@@ -860,7 +1098,7 @@ function createZipBombGuard(opts = {}) {
860
1098
  const cdOffset = r32(buf, eocdPos + 16);
861
1099
  // Bounds check
862
1100
  if (cdOffset + cdSize > buf.length) {
863
- matches.push({ rule: 'zip_cd_out_of_bounds', severity: 'medium' });
1101
+ matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
864
1102
  return matches;
865
1103
  }
866
1104
  // Iterate central directory entries
@@ -881,287 +1119,68 @@ function createZipBombGuard(opts = {}) {
881
1119
  const nameEnd = nameStart + fnLen;
882
1120
  if (nameEnd > buf.length)
883
1121
  break;
884
- const name = buf.toString('utf8', nameStart, nameEnd);
1122
+ const name = buf.toString("utf8", nameStart, nameEnd);
885
1123
  sumComp += compSize;
886
1124
  sumUnc += uncSize;
887
1125
  seen++;
888
1126
  if (name.length > cfg.maxEntryNameLength) {
889
- matches.push({ rule: 'zip_entry_name_too_long', severity: 'medium', meta: { name, length: name.length } });
1127
+ matches.push({
1128
+ rule: "zip_entry_name_too_long",
1129
+ severity: "medium",
1130
+ meta: { name, length: name.length },
1131
+ });
890
1132
  }
891
1133
  if (hasTraversal(name)) {
892
- matches.push({ rule: 'zip_path_traversal_entry', severity: 'medium', meta: { name } });
1134
+ matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
893
1135
  }
894
1136
  // move to next entry
895
1137
  ptr = nameEnd + exLen + cmLen;
896
1138
  }
897
1139
  if (seen !== totalEntries) {
898
1140
  // central dir truncated/odd, still report what we found
899
- matches.push({ rule: 'zip_cd_truncated', severity: 'medium', meta: { seen, totalEntries } });
1141
+ matches.push({
1142
+ rule: "zip_cd_truncated",
1143
+ severity: "medium",
1144
+ meta: { seen, totalEntries },
1145
+ });
900
1146
  }
901
1147
  // Heuristics thresholds
902
1148
  if (seen > cfg.maxEntries) {
903
- matches.push({ rule: 'zip_too_many_entries', severity: 'medium', meta: { seen, limit: cfg.maxEntries } });
1149
+ matches.push({
1150
+ rule: "zip_too_many_entries",
1151
+ severity: "medium",
1152
+ meta: { seen, limit: cfg.maxEntries },
1153
+ });
904
1154
  }
905
1155
  if (sumUnc > cfg.maxTotalUncompressedBytes) {
906
1156
  matches.push({
907
- rule: 'zip_total_uncompressed_too_large',
908
- severity: 'medium',
909
- meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
1157
+ rule: "zip_total_uncompressed_too_large",
1158
+ severity: "medium",
1159
+ meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
910
1160
  });
911
1161
  }
912
1162
  if (sumComp === 0 && sumUnc > 0) {
913
- matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio: Infinity } });
1163
+ matches.push({
1164
+ rule: "zip_suspicious_ratio",
1165
+ severity: "medium",
1166
+ meta: { ratio: Infinity },
1167
+ });
914
1168
  }
915
1169
  else if (sumComp > 0) {
916
1170
  const ratio = sumUnc / Math.max(1, sumComp);
917
1171
  if (ratio >= cfg.maxCompressionRatio) {
918
- matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio, limit: cfg.maxCompressionRatio } });
1172
+ matches.push({
1173
+ rule: "zip_suspicious_ratio",
1174
+ severity: "medium",
1175
+ meta: { ratio, limit: cfg.maxCompressionRatio },
1176
+ });
919
1177
  }
920
1178
  }
921
1179
  return matches;
922
- }
1180
+ },
923
1181
  };
924
1182
  }
925
1183
 
926
- const MB$1 = 1024 * 1024;
927
- const DEFAULT_POLICY = {
928
- includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf'],
929
- allowedMimeTypes: ['application/zip', 'image/png', 'image/jpeg', 'application/pdf', 'text/plain'],
930
- maxFileSizeBytes: 20 * MB$1,
931
- timeoutMs: 5000,
932
- concurrency: 4,
933
- failClosed: true
934
- };
935
- function definePolicy(input = {}) {
936
- const p = { ...DEFAULT_POLICY, ...input };
937
- if (!Array.isArray(p.includeExtensions))
938
- throw new TypeError('includeExtensions must be string[]');
939
- if (!Array.isArray(p.allowedMimeTypes))
940
- throw new TypeError('allowedMimeTypes must be string[]');
941
- if (!(Number.isFinite(p.maxFileSizeBytes) && p.maxFileSizeBytes > 0))
942
- throw new TypeError('maxFileSizeBytes must be > 0');
943
- if (!(Number.isFinite(p.timeoutMs) && p.timeoutMs > 0))
944
- throw new TypeError('timeoutMs must be > 0');
945
- if (!(Number.isInteger(p.concurrency) && p.concurrency > 0))
946
- throw new TypeError('concurrency must be > 0');
947
- return p;
948
- }
949
-
950
- /**
951
- * Policy packs for Pompelmi.
952
- *
953
- * Pre-configured, named policies for common upload scenarios. Each pack
954
- * defines the file type allowlist, size limits, and timeout appropriate for
955
- * its use case.
956
- *
957
- * All packs are built on `definePolicy` and are fully overridable:
958
- *
959
- * ```ts
960
- * import { POLICY_PACKS } from 'pompelmi/policy-packs';
961
- *
962
- * // Use a pack as-is:
963
- * const policy = POLICY_PACKS['images-only'];
964
- *
965
- * // Or override individual fields:
966
- * import { definePolicy } from 'pompelmi';
967
- * const custom = definePolicy({ ...POLICY_PACKS['documents-only'], maxFileSizeBytes: 5 * 1024 * 1024 });
968
- * ```
969
- *
970
- * These packs are *deterministic* and *descriptor-based* — they do not
971
- * depend on any external threat intelligence feed.
972
- *
973
- * @module policy-packs
974
- */
975
- const KB = 1024;
976
- const MB = 1024 * KB;
977
- // ── Policy packs ──────────────────────────────────────────────────────────────
978
- /**
979
- * Documents-only policy.
980
- *
981
- * Appropriate for: document management APIs, PDF/Office file upload endpoints,
982
- * data import pipelines.
983
- *
984
- * Allowed: PDF, Word (.docx/.doc), Excel (.xlsx/.xls), PowerPoint (.pptx/.ppt),
985
- * CSV, plain text, JSON, YAML, ODT/ODS/ODP (OpenDocument).
986
- * Max size: 25 MB.
987
- */
988
- const DOCUMENTS_ONLY = definePolicy({
989
- includeExtensions: [
990
- 'pdf',
991
- 'doc', 'docx',
992
- 'xls', 'xlsx',
993
- 'ppt', 'pptx',
994
- 'odt', 'ods', 'odp',
995
- 'csv',
996
- 'txt',
997
- 'json',
998
- 'yaml', 'yml',
999
- 'md',
1000
- ],
1001
- allowedMimeTypes: [
1002
- 'application/pdf',
1003
- 'application/msword',
1004
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1005
- 'application/vnd.ms-excel',
1006
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1007
- 'application/vnd.ms-powerpoint',
1008
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1009
- 'application/vnd.oasis.opendocument.text',
1010
- 'application/vnd.oasis.opendocument.spreadsheet',
1011
- 'application/vnd.oasis.opendocument.presentation',
1012
- 'text/csv',
1013
- 'text/plain',
1014
- 'application/json',
1015
- 'text/yaml',
1016
- 'text/markdown',
1017
- ],
1018
- maxFileSizeBytes: 25 * MB,
1019
- timeoutMs: 10000,
1020
- concurrency: 4,
1021
- failClosed: true,
1022
- });
1023
- /**
1024
- * Images-only policy.
1025
- *
1026
- * Appropriate for: avatar uploads, product image APIs, content platforms with
1027
- * user-generated imagery.
1028
- *
1029
- * Allowed: JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP, ICO.
1030
- * Max size: 10 MB.
1031
- * Note: SVG is intentionally excluded — inline SVGs can contain scripts.
1032
- */
1033
- const IMAGES_ONLY = definePolicy({
1034
- includeExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'tiff', 'tif', 'bmp', 'ico'],
1035
- allowedMimeTypes: [
1036
- 'image/jpeg',
1037
- 'image/png',
1038
- 'image/gif',
1039
- 'image/webp',
1040
- 'image/avif',
1041
- 'image/tiff',
1042
- 'image/bmp',
1043
- 'image/x-icon',
1044
- 'image/vnd.microsoft.icon',
1045
- ],
1046
- maxFileSizeBytes: 10 * MB,
1047
- timeoutMs: 5000,
1048
- concurrency: 8,
1049
- failClosed: true,
1050
- });
1051
- /**
1052
- * Strict public-upload policy.
1053
- *
1054
- * Appropriate for: anonymous or low-trust upload endpoints, public APIs,
1055
- * any surface exposed to untrusted users.
1056
- *
1057
- * Aggressive size limit (5 MB), short timeout, fail-closed, narrow MIME
1058
- * allowlist. Only allows plain images and PDF.
1059
- */
1060
- const STRICT_PUBLIC_UPLOAD = definePolicy({
1061
- includeExtensions: ['jpg', 'jpeg', 'png', 'webp', 'pdf'],
1062
- allowedMimeTypes: [
1063
- 'image/jpeg',
1064
- 'image/png',
1065
- 'image/webp',
1066
- 'application/pdf',
1067
- ],
1068
- maxFileSizeBytes: 5 * MB,
1069
- timeoutMs: 4000,
1070
- concurrency: 2,
1071
- failClosed: true,
1072
- });
1073
- /**
1074
- * Conservative default policy.
1075
- *
1076
- * A hardened version of the built-in `DEFAULT_POLICY` suitable for
1077
- * production without further customisation. Stricter size limit and
1078
- * shorter timeout than the permissive default.
1079
- */
1080
- const CONSERVATIVE_DEFAULT = definePolicy({
1081
- includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf', 'txt', 'csv', 'docx', 'xlsx'],
1082
- allowedMimeTypes: [
1083
- 'application/zip',
1084
- 'image/png',
1085
- 'image/jpeg',
1086
- 'application/pdf',
1087
- 'text/plain',
1088
- 'text/csv',
1089
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1090
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1091
- ],
1092
- maxFileSizeBytes: 10 * MB,
1093
- timeoutMs: 8000,
1094
- concurrency: 4,
1095
- failClosed: true,
1096
- });
1097
- /**
1098
- * Archives policy.
1099
- *
1100
- * Appropriate for: endpoints that accept ZIP, tar, or compressed archives.
1101
- * Combines a generous size allowance with a longer timeout for deep inspection.
1102
- *
1103
- * NOTE: Pair this policy with `createZipBombGuard()` to defend against
1104
- * decompression-bomb attacks:
1105
- *
1106
- * ```ts
1107
- * import { composeScanners, createZipBombGuard, CommonHeuristicsScanner } from 'pompelmi';
1108
- * const scanner = composeScanners(
1109
- * [['zipGuard', createZipBombGuard()], ['heuristics', CommonHeuristicsScanner]]
1110
- * );
1111
- * ```
1112
- */
1113
- const ARCHIVES = definePolicy({
1114
- includeExtensions: ['zip', 'tar', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar'],
1115
- allowedMimeTypes: [
1116
- 'application/zip',
1117
- 'application/x-tar',
1118
- 'application/gzip',
1119
- 'application/x-bzip2',
1120
- 'application/x-xz',
1121
- 'application/x-7z-compressed',
1122
- 'application/x-rar-compressed',
1123
- ],
1124
- maxFileSizeBytes: 100 * MB,
1125
- timeoutMs: 30000,
1126
- concurrency: 2,
1127
- failClosed: true,
1128
- });
1129
- /**
1130
- * Named map of all built-in policy packs.
1131
- *
1132
- * ```ts
1133
- * import { POLICY_PACKS } from 'pompelmi/policy-packs';
1134
- * const policy = POLICY_PACKS['strict-public-upload'];
1135
- * ```
1136
- */
1137
- const POLICY_PACKS = {
1138
- 'documents-only': DOCUMENTS_ONLY,
1139
- 'images-only': IMAGES_ONLY,
1140
- 'strict-public-upload': STRICT_PUBLIC_UPLOAD,
1141
- 'conservative-default': CONSERVATIVE_DEFAULT,
1142
- 'archives': ARCHIVES,
1143
- };
1144
- /**
1145
- * Look up a policy pack by name.
1146
- * Throws if the name is not recognised.
1147
- */
1148
- function getPolicyPack(name) {
1149
- const policy = POLICY_PACKS[name];
1150
- if (!policy)
1151
- throw new Error(`Unknown policy pack: '${name}'. Valid names: ${Object.keys(POLICY_PACKS).join(', ')}`);
1152
- return policy;
1153
- }
1154
-
1155
- function mapMatchesToVerdict(matches = []) {
1156
- if (!matches.length)
1157
- return 'clean';
1158
- const malHints = ['trojan', 'ransom', 'worm', 'spy', 'rootkit', 'keylog', 'botnet'];
1159
- const tagSet = new Set(matches.flatMap(m => (m.tags ?? []).map(t => t.toLowerCase())));
1160
- const nameHit = (r) => malHints.some(h => r.toLowerCase().includes(h));
1161
- const isMal = matches.some(m => nameHit(m.rule)) || tagSet.has('malware') || tagSet.has('critical');
1162
- return isMal ? 'malicious' : 'suspicious';
1163
- }
1164
-
1165
1184
  /**
1166
1185
  * Export utilities for scan results
1167
1186
  * @module utils/export
@@ -1177,19 +1196,15 @@ class ScanResultExporter {
1177
1196
  const data = Array.isArray(reports) ? reports : [reports];
1178
1197
  if (!options.includeDetails) {
1179
1198
  // Simplified output
1180
- const simplified = data.map(r => ({
1199
+ const simplified = data.map((r) => ({
1181
1200
  verdict: r.verdict,
1182
1201
  file: r.file?.name,
1183
1202
  matches: r.matches.length,
1184
1203
  durationMs: r.durationMs,
1185
1204
  }));
1186
- return options.prettyPrint
1187
- ? JSON.stringify(simplified, null, 2)
1188
- : JSON.stringify(simplified);
1205
+ return options.prettyPrint ? JSON.stringify(simplified, null, 2) : JSON.stringify(simplified);
1189
1206
  }
1190
- return options.prettyPrint
1191
- ? JSON.stringify(data, null, 2)
1192
- : JSON.stringify(data);
1207
+ return options.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
1193
1208
  }
1194
1209
  /**
1195
1210
  * Export to CSV format
@@ -1197,68 +1212,68 @@ class ScanResultExporter {
1197
1212
  toCSV(reports, options = {}) {
1198
1213
  const data = Array.isArray(reports) ? reports : [reports];
1199
1214
  const headers = [
1200
- 'filename',
1201
- 'verdict',
1202
- 'matches_count',
1203
- 'file_size',
1204
- 'mime_type',
1205
- 'duration_ms',
1206
- 'engine',
1215
+ "filename",
1216
+ "verdict",
1217
+ "matches_count",
1218
+ "file_size",
1219
+ "mime_type",
1220
+ "duration_ms",
1221
+ "engine",
1207
1222
  ];
1208
1223
  if (options.includeDetails) {
1209
- headers.push('reasons', 'match_rules');
1224
+ headers.push("reasons", "match_rules");
1210
1225
  }
1211
- const rows = data.map(report => {
1226
+ const rows = data.map((report) => {
1212
1227
  const row = [
1213
- this.escapeCsv(report.file?.name || 'unknown'),
1228
+ this.escapeCsv(report.file?.name || "unknown"),
1214
1229
  report.verdict,
1215
1230
  report.matches.length.toString(),
1216
1231
  (report.file?.size || 0).toString(),
1217
- this.escapeCsv(report.file?.mimeType || 'unknown'),
1232
+ this.escapeCsv(report.file?.mimeType || "unknown"),
1218
1233
  (report.durationMs || 0).toString(),
1219
- report.engine || 'unknown',
1234
+ report.engine || "unknown",
1220
1235
  ];
1221
1236
  if (options.includeDetails) {
1222
- row.push(this.escapeCsv((report.reasons || []).join('; ')), this.escapeCsv(report.matches.map(m => m.rule).join('; ')));
1237
+ row.push(this.escapeCsv((report.reasons || []).join("; ")), this.escapeCsv(report.matches.map((m) => m.rule).join("; ")));
1223
1238
  }
1224
- return row.join(',');
1239
+ return row.join(",");
1225
1240
  });
1226
- return [headers.join(','), ...rows].join('\n');
1241
+ return [headers.join(","), ...rows].join("\n");
1227
1242
  }
1228
1243
  /**
1229
1244
  * Export to Markdown format
1230
1245
  */
1231
1246
  toMarkdown(reports, options = {}) {
1232
1247
  const data = Array.isArray(reports) ? reports : [reports];
1233
- let md = '# Scan Results\n\n';
1248
+ let md = "# Scan Results\n\n";
1234
1249
  md += `**Total Scans:** ${data.length}\n\n`;
1235
- const clean = data.filter(r => r.verdict === 'clean').length;
1236
- const suspicious = data.filter(r => r.verdict === 'suspicious').length;
1237
- const malicious = data.filter(r => r.verdict === 'malicious').length;
1238
- md += '## Summary\n\n';
1250
+ const clean = data.filter((r) => r.verdict === "clean").length;
1251
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
1252
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
1253
+ md += "## Summary\n\n";
1239
1254
  md += `- ✅ Clean: ${clean}\n`;
1240
1255
  md += `- ⚠️ Suspicious: ${suspicious}\n`;
1241
1256
  md += `- ❌ Malicious: ${malicious}\n\n`;
1242
- md += '## Detailed Results\n\n';
1257
+ md += "## Detailed Results\n\n";
1243
1258
  for (const report of data) {
1244
- const icon = report.verdict === 'clean' ? '' : report.verdict === 'suspicious' ? '⚠️' : '';
1245
- md += `### ${icon} ${report.file?.name || 'Unknown'}\n\n`;
1259
+ const icon = report.verdict === "clean" ? "" : report.verdict === "suspicious" ? "⚠️" : "";
1260
+ md += `### ${icon} ${report.file?.name || "Unknown"}\n\n`;
1246
1261
  md += `- **Verdict:** ${report.verdict}\n`;
1247
1262
  md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
1248
- md += `- **MIME Type:** ${report.file?.mimeType || 'unknown'}\n`;
1263
+ md += `- **MIME Type:** ${report.file?.mimeType || "unknown"}\n`;
1249
1264
  md += `- **Duration:** ${report.durationMs || 0}ms\n`;
1250
1265
  md += `- **Matches:** ${report.matches.length}\n`;
1251
1266
  if (options.includeDetails && report.matches.length > 0) {
1252
- md += '\n**Match Details:**\n';
1267
+ md += "\n**Match Details:**\n";
1253
1268
  for (const match of report.matches) {
1254
1269
  md += `- ${match.rule}`;
1255
1270
  if (match.tags && match.tags.length > 0) {
1256
- md += ` (${match.tags.join(', ')})`;
1271
+ md += ` (${match.tags.join(", ")})`;
1257
1272
  }
1258
- md += '\n';
1273
+ md += "\n";
1259
1274
  }
1260
1275
  }
1261
- md += '\n';
1276
+ md += "\n";
1262
1277
  }
1263
1278
  return md;
1264
1279
  }
@@ -1268,20 +1283,20 @@ class ScanResultExporter {
1268
1283
  */
1269
1284
  toSARIF(reports, options = {}) {
1270
1285
  const data = Array.isArray(reports) ? reports : [reports];
1271
- const results = data.flatMap(report => {
1272
- if (report.verdict === 'clean')
1286
+ const results = data.flatMap((report) => {
1287
+ if (report.verdict === "clean")
1273
1288
  return [];
1274
- return report.matches.map(match => ({
1289
+ return report.matches.map((match) => ({
1275
1290
  ruleId: match.rule,
1276
- level: report.verdict === 'malicious' ? 'error' : 'warning',
1291
+ level: report.verdict === "malicious" ? "error" : "warning",
1277
1292
  message: {
1278
- text: `${match.rule} detected in ${report.file?.name || 'unknown file'}`,
1293
+ text: `${match.rule} detected in ${report.file?.name || "unknown file"}`,
1279
1294
  },
1280
1295
  locations: [
1281
1296
  {
1282
1297
  physicalLocation: {
1283
1298
  artifactLocation: {
1284
- uri: report.file?.name || 'unknown',
1299
+ uri: report.file?.name || "unknown",
1285
1300
  },
1286
1301
  },
1287
1302
  },
@@ -1293,33 +1308,31 @@ class ScanResultExporter {
1293
1308
  }));
1294
1309
  });
1295
1310
  const sarif = {
1296
- version: '2.1.0',
1297
- $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
1311
+ version: "2.1.0",
1312
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
1298
1313
  runs: [
1299
1314
  {
1300
1315
  tool: {
1301
1316
  driver: {
1302
- name: 'Pompelmi',
1303
- version: '0.29.0',
1304
- informationUri: 'https://pompelmi.github.io/pompelmi/',
1317
+ name: "Pompelmi",
1318
+ version: "0.29.0",
1319
+ informationUri: "https://pompelmi.github.io/pompelmi/",
1305
1320
  },
1306
1321
  },
1307
1322
  results,
1308
1323
  },
1309
1324
  ],
1310
1325
  };
1311
- return options.prettyPrint
1312
- ? JSON.stringify(sarif, null, 2)
1313
- : JSON.stringify(sarif);
1326
+ return options.prettyPrint ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
1314
1327
  }
1315
1328
  /**
1316
1329
  * Export to HTML format
1317
1330
  */
1318
1331
  toHTML(reports, options = {}) {
1319
1332
  const data = Array.isArray(reports) ? reports : [reports];
1320
- const clean = data.filter(r => r.verdict === 'clean').length;
1321
- const suspicious = data.filter(r => r.verdict === 'suspicious').length;
1322
- const malicious = data.filter(r => r.verdict === 'malicious').length;
1333
+ const clean = data.filter((r) => r.verdict === "clean").length;
1334
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
1335
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
1323
1336
  let html = `<!DOCTYPE html>
1324
1337
  <html lang="en">
1325
1338
  <head>
@@ -1351,11 +1364,11 @@ class ScanResultExporter {
1351
1364
  for (const report of data) {
1352
1365
  const statusClass = report.verdict;
1353
1366
  html += `<div class="result ${statusClass}">`;
1354
- html += `<h3>${this.escapeHtml(report.file?.name || 'Unknown')}</h3>`;
1367
+ html += `<h3>${this.escapeHtml(report.file?.name || "Unknown")}</h3>`;
1355
1368
  html += `<table>`;
1356
1369
  html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
1357
1370
  html += `<tr><th>Size</th><td>${this.formatBytes(report.file?.size || 0)}</td></tr>`;
1358
- html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || 'unknown')}</td></tr>`;
1371
+ html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || "unknown")}</td></tr>`;
1359
1372
  html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
1360
1373
  html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
1361
1374
  html += `</table>`;
@@ -1364,7 +1377,7 @@ class ScanResultExporter {
1364
1377
  for (const match of report.matches) {
1365
1378
  html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
1366
1379
  if (match.tags && match.tags.length > 0) {
1367
- html += ` ${match.tags.map(tag => `<span class="badge">${this.escapeHtml(tag)}</span>`).join('')}`;
1380
+ html += ` ${match.tags.map((tag) => `<span class="badge">${this.escapeHtml(tag)}</span>`).join("")}`;
1368
1381
  }
1369
1382
  html += `</li>`;
1370
1383
  }
@@ -1380,41 +1393,41 @@ class ScanResultExporter {
1380
1393
  */
1381
1394
  export(reports, format, options = {}) {
1382
1395
  switch (format) {
1383
- case 'json':
1396
+ case "json":
1384
1397
  return this.toJSON(reports, options);
1385
- case 'csv':
1398
+ case "csv":
1386
1399
  return this.toCSV(reports, options);
1387
- case 'markdown':
1400
+ case "markdown":
1388
1401
  return this.toMarkdown(reports, options);
1389
- case 'html':
1402
+ case "html":
1390
1403
  return this.toHTML(reports, options);
1391
- case 'sarif':
1404
+ case "sarif":
1392
1405
  return this.toSARIF(reports, options);
1393
1406
  default:
1394
1407
  throw new Error(`Unsupported export format: ${format}`);
1395
1408
  }
1396
1409
  }
1397
1410
  escapeCsv(value) {
1398
- if (value.includes(',') || value.includes('"') || value.includes('\n')) {
1411
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1399
1412
  return `"${value.replace(/"/g, '""')}"`;
1400
1413
  }
1401
1414
  return value;
1402
1415
  }
1403
1416
  escapeHtml(value) {
1404
1417
  return value
1405
- .replace(/&/g, '&amp;')
1406
- .replace(/</g, '&lt;')
1407
- .replace(/>/g, '&gt;')
1408
- .replace(/"/g, '&quot;')
1409
- .replace(/'/g, '&#039;');
1418
+ .replace(/&/g, "&amp;")
1419
+ .replace(/</g, "&lt;")
1420
+ .replace(/>/g, "&gt;")
1421
+ .replace(/"/g, "&quot;")
1422
+ .replace(/'/g, "&#039;");
1410
1423
  }
1411
1424
  formatBytes(bytes) {
1412
1425
  if (bytes === 0)
1413
- return '0 Bytes';
1426
+ return "0 Bytes";
1414
1427
  const k = 1024;
1415
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1428
+ const sizes = ["Bytes", "KB", "MB", "GB"];
1416
1429
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1417
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
1430
+ return Math.round((bytes / k ** i) * 100) / 100 + " " + sizes[i];
1418
1431
  }
1419
1432
  }
1420
1433
  /**
@@ -1425,5 +1438,30 @@ function exportScanResults(reports, format, options) {
1425
1438
  return exporter.export(reports, format, options);
1426
1439
  }
1427
1440
 
1441
+ /**
1442
+ * Validates a File by MIME type and size (max 5 MB).
1443
+ */
1444
+ function validateFile(file) {
1445
+ const maxSize = 5 * 1024 * 1024;
1446
+ const allowedTypes = ["text/plain", "application/json", "text/csv"];
1447
+ if (!allowedTypes.includes(file.type)) {
1448
+ return { valid: false, error: "Unsupported file type" };
1449
+ }
1450
+ if (file.size > maxSize) {
1451
+ return { valid: false, error: "File too large (max 5 MB)" };
1452
+ }
1453
+ return { valid: true };
1454
+ }
1455
+
1456
+ function mapMatchesToVerdict(matches = []) {
1457
+ if (!matches.length)
1458
+ return "clean";
1459
+ const malHints = ["trojan", "ransom", "worm", "spy", "rootkit", "keylog", "botnet"];
1460
+ const tagSet = new Set(matches.flatMap((m) => (m.tags ?? []).map((t) => t.toLowerCase())));
1461
+ const nameHit = (r) => malHints.some((h) => r.toLowerCase().includes(h));
1462
+ const isMal = matches.some((m) => nameHit(m.rule)) || tagSet.has("malware") || tagSet.has("critical");
1463
+ return isMal ? "malicious" : "suspicious";
1464
+ }
1465
+
1428
1466
  export { ARCHIVES, CONSERVATIVE_DEFAULT, CommonHeuristicsScanner, DEFAULT_POLICY, DOCUMENTS_ONLY, IMAGES_ONLY, POLICY_PACKS, PerformanceTracker, STRICT_PUBLIC_UPLOAD, ScanResultExporter, aggregateScanStats, analyzeNestedArchives, composeScanners, createPresetScanner, createZipBombGuard, definePolicy, detectObfuscatedScripts, detectPolyglot, exportScanResults, getPolicyPack, mapMatchesToVerdict, scanBytes, scanFile, scanFiles, validateFile };
1429
1467
  //# sourceMappingURL=pompelmi.browser.esm.js.map