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