pompelmi 0.34.10 → 0.35.0

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 +585 -534
  7. package/dist/pompelmi.browser.cjs.map +1 -1
  8. package/dist/pompelmi.browser.esm.js +585 -534
  9. package/dist/pompelmi.browser.esm.js.map +1 -1
  10. package/dist/pompelmi.cjs +2066 -2016
  11. package/dist/pompelmi.cjs.map +1 -1
  12. package/dist/pompelmi.esm.js +2066 -2016
  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 +585 -534
  27. package/dist/pompelmi.react.cjs.map +1 -1
  28. package/dist/pompelmi.react.esm.js +585 -534
  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 +6 -6
@@ -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
  : {};
@@ -188,24 +430,32 @@ function createPresetScanner(preset, opts = {}) {
188
430
  // Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
189
431
  scanners.push(CommonHeuristicsScanner);
190
432
  // 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') {
433
+ if (preset === "decompilation-basic" ||
434
+ preset === "decompilation-deep" ||
435
+ preset === "malware-analysis" ||
436
+ opts.enableDecompilation) {
437
+ const depth = preset === "decompilation-deep"
438
+ ? "deep"
439
+ : preset === "decompilation-basic"
440
+ ? "basic"
441
+ : opts.decompilationDepth || "basic";
442
+ if (!opts.decompilationEngine ||
443
+ opts.decompilationEngine === "binaryninja-hlil" ||
444
+ opts.decompilationEngine === "both") {
197
445
  try {
198
446
  // 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) => {
447
+ const importModule = new Function("specifier", "return import(specifier)");
448
+ importModule("@pompelmi/engine-binaryninja")
449
+ .then((mod) => {
201
450
  const binjaScanner = mod.createBinaryNinjaScanner({
202
451
  timeout: opts.decompilationTimeout || opts.timeout || 30000,
203
452
  depth,
204
453
  pythonPath: opts.pythonPath,
205
- binaryNinjaPath: opts.binaryNinjaPath
454
+ binaryNinjaPath: opts.binaryNinjaPath,
206
455
  });
207
456
  scanners.push(binjaScanner);
208
- }).catch(() => {
457
+ })
458
+ .catch(() => {
209
459
  // Binary Ninja engine not available - silently skip
210
460
  });
211
461
  }
@@ -213,19 +463,23 @@ function createPresetScanner(preset, opts = {}) {
213
463
  // Engine not installed
214
464
  }
215
465
  }
216
- if (!opts.decompilationEngine || opts.decompilationEngine === 'ghidra-pcode' || opts.decompilationEngine === 'both') {
466
+ if (!opts.decompilationEngine ||
467
+ opts.decompilationEngine === "ghidra-pcode" ||
468
+ opts.decompilationEngine === "both") {
217
469
  try {
218
470
  // 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) => {
471
+ const importModule = new Function("specifier", "return import(specifier)");
472
+ importModule("@pompelmi/engine-ghidra")
473
+ .then((mod) => {
221
474
  const ghidraScanner = mod.createGhidraScanner({
222
475
  timeout: opts.decompilationTimeout || opts.timeout || 30000,
223
476
  depth,
224
477
  ghidraPath: opts.ghidraPath,
225
- analyzeHeadless: opts.analyzeHeadless
478
+ analyzeHeadless: opts.analyzeHeadless,
226
479
  });
227
480
  scanners.push(ghidraScanner);
228
- }).catch(() => {
481
+ })
482
+ .catch(() => {
229
483
  // Ghidra engine not available - silently skip
230
484
  });
231
485
  }
@@ -243,96 +497,6 @@ function createPresetScanner(preset, opts = {}) {
243
497
  return composeScanners(...scanners);
244
498
  }
245
499
 
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);
271
- }
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++;
318
- }
319
- if (report.file?.size !== undefined) {
320
- totalBytes += report.file.size;
321
- }
322
- }
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,
333
- };
334
- }
335
-
336
500
  /**
337
501
  * Advanced threat detection utilities
338
502
  * @module utils/advanced-detection
@@ -346,25 +510,25 @@ function detectPolyglot(bytes) {
346
510
  // Check for PDF/ZIP polyglot
347
511
  if (isPDFZipPolyglot(bytes)) {
348
512
  matches.push({
349
- rule: 'polyglot_pdf_zip',
350
- severity: 'high',
351
- meta: { description: 'File can be interpreted as both PDF and ZIP' },
513
+ rule: "polyglot_pdf_zip",
514
+ severity: "high",
515
+ meta: { description: "File can be interpreted as both PDF and ZIP" },
352
516
  });
353
517
  }
354
518
  // Check for image/script polyglot
355
519
  if (isImageScriptPolyglot(bytes)) {
356
520
  matches.push({
357
- rule: 'polyglot_image_script',
358
- severity: 'high',
359
- meta: { description: 'Image file contains executable script content' },
521
+ rule: "polyglot_image_script",
522
+ severity: "high",
523
+ meta: { description: "Image file contains executable script content" },
360
524
  });
361
525
  }
362
526
  // Check for GIFAR (GIF/JAR polyglot)
363
527
  if (isGIFAR(bytes)) {
364
528
  matches.push({
365
- rule: 'polyglot_gifar',
366
- severity: 'critical',
367
- meta: { description: 'GIF file contains Java archive' },
529
+ rule: "polyglot_gifar",
530
+ severity: "critical",
531
+ meta: { description: "GIF file contains Java archive" },
368
532
  });
369
533
  }
370
534
  return matches;
@@ -374,7 +538,7 @@ function detectPolyglot(bytes) {
374
538
  */
375
539
  function detectObfuscatedScripts(bytes) {
376
540
  const matches = [];
377
- const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
541
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
378
542
  // Check for common obfuscation patterns
379
543
  const obfuscationPatterns = [
380
544
  /eval\s*\(\s*unescape\s*\(/gi,
@@ -386,10 +550,10 @@ function detectObfuscatedScripts(bytes) {
386
550
  for (const pattern of obfuscationPatterns) {
387
551
  if (pattern.test(text)) {
388
552
  matches.push({
389
- rule: 'obfuscated_script',
390
- severity: 'medium',
553
+ rule: "obfuscated_script",
554
+ severity: "medium",
391
555
  meta: {
392
- description: 'Detected obfuscated script content',
556
+ description: "Detected obfuscated script content",
393
557
  pattern: pattern.source,
394
558
  },
395
559
  });
@@ -429,7 +593,10 @@ function isPDFZipPolyglot(bytes) {
429
593
  // Check for ZIP signature anywhere in the file
430
594
  let hasZIP = false;
431
595
  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) {
596
+ if (bytes[i] === 0x50 &&
597
+ bytes[i + 1] === 0x4b &&
598
+ bytes[i + 2] === 0x03 &&
599
+ bytes[i + 3] === 0x04) {
433
600
  hasZIP = true;
434
601
  break;
435
602
  }
@@ -440,14 +607,13 @@ function isImageScriptPolyglot(bytes) {
440
607
  if (bytes.length < 100)
441
608
  return false;
442
609
  // 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
- );
610
+ const isImage = (bytes[0] === 0xff && bytes[1] === 0xd8) || // JPEG
611
+ (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) || // PNG
612
+ (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46); // GIF
447
613
  if (!isImage)
448
614
  return false;
449
615
  // Check for script content
450
- const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
616
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
451
617
  return /<script|javascript:|eval\(|function\s*\(/i.test(text);
452
618
  }
453
619
  function isGIFAR(bytes) {
@@ -458,7 +624,10 @@ function isGIFAR(bytes) {
458
624
  // Check for ZIP/JAR signature
459
625
  let hasZIP = false;
460
626
  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) {
627
+ if (bytes[i] === 0x50 &&
628
+ bytes[i + 1] === 0x4b &&
629
+ bytes[i + 2] === 0x03 &&
630
+ bytes[i + 3] === 0x04) {
462
631
  hasZIP = true;
463
632
  break;
464
633
  }
@@ -470,13 +639,13 @@ function isArchive(bytes) {
470
639
  return false;
471
640
  return (
472
641
  // ZIP
473
- (bytes[0] === 0x50 && bytes[1] === 0x4B && bytes[2] === 0x03 && bytes[3] === 0x04) ||
642
+ (bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) ||
474
643
  // RAR
475
644
  (bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
476
645
  // 7z
477
- (bytes[0] === 0x37 && bytes[1] === 0x7A && bytes[2] === 0xBC && bytes[3] === 0xAF) ||
646
+ (bytes[0] === 0x37 && bytes[1] === 0x7a && bytes[2] === 0xbc && bytes[3] === 0xaf) ||
478
647
  // tar.gz
479
- (bytes[0] === 0x1F && bytes[1] === 0x8B));
648
+ (bytes[0] === 0x1f && bytes[1] === 0x8b));
480
649
  }
481
650
 
482
651
  /**
@@ -504,10 +673,10 @@ class ScanCacheManager {
504
673
  * Generate cache key from file content
505
674
  */
506
675
  generateKey(content, preset) {
507
- const hash = createHash('sha256')
676
+ const hash = createHash("sha256")
508
677
  .update(content)
509
- .update(preset || 'default')
510
- .digest('hex');
678
+ .update(preset || "default")
679
+ .digest("hex");
511
680
  return hash;
512
681
  }
513
682
  /**
@@ -651,35 +820,132 @@ function getDefaultCache(options) {
651
820
  return defaultCache;
652
821
  }
653
822
 
654
- /** Mappa veloce estensione -> mime (basic) */
655
- function guessMimeByExt(name) {
656
- if (!name)
657
- return;
658
- const ext = name.toLowerCase().split('.').pop();
659
- 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;
667
- }
668
- }
669
- /** Heuristica semplice per verdetto */
670
- function computeVerdict(matches) {
671
- if (!matches.length)
672
- return 'clean';
673
- // 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';
676
- }
677
- /** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
678
- function toYaraMatches(ms) {
679
- return ms.map(m => ({
823
+ /**
824
+ * Performance monitoring utilities for pompelmi scans
825
+ * @module utils/performance-metrics
826
+ */
827
+ /**
828
+ * Track performance metrics for a scan operation
829
+ */
830
+ class PerformanceTracker {
831
+ constructor() {
832
+ this.checkpoints = new Map();
833
+ this.startTime = Date.now();
834
+ }
835
+ /**
836
+ * Mark a checkpoint in the scan process
837
+ */
838
+ checkpoint(name) {
839
+ this.checkpoints.set(name, Date.now());
840
+ }
841
+ /**
842
+ * Get duration since start or since a specific checkpoint
843
+ */
844
+ getDuration(since) {
845
+ const now = Date.now();
846
+ if (since && this.checkpoints.has(since)) {
847
+ return now - (this.checkpoints.get(since) ?? now);
848
+ }
849
+ return now - this.startTime;
850
+ }
851
+ /**
852
+ * Generate final metrics report
853
+ */
854
+ getMetrics(bytesScanned) {
855
+ const totalDuration = this.getDuration();
856
+ const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
857
+ return {
858
+ totalDurationMs: totalDuration,
859
+ heuristicsDurationMs: this.checkpoints.has("heuristics_end")
860
+ ? (this.checkpoints.get("heuristics_end") ?? 0) -
861
+ (this.checkpoints.get("heuristics_start") ?? 0)
862
+ : undefined,
863
+ yaraDurationMs: this.checkpoints.has("yara_end")
864
+ ? (this.checkpoints.get("yara_end") ?? 0) - (this.checkpoints.get("yara_start") ?? 0)
865
+ : undefined,
866
+ prepDurationMs: this.checkpoints.has("prep_end")
867
+ ? (this.checkpoints.get("prep_end") ?? 0) - this.startTime
868
+ : undefined,
869
+ throughputBps: throughput,
870
+ bytesScanned,
871
+ startedAt: this.startTime,
872
+ completedAt: Date.now(),
873
+ };
874
+ }
875
+ }
876
+ /**
877
+ * Aggregate statistics from multiple scan reports
878
+ */
879
+ function aggregateScanStats(reports) {
880
+ let cleanCount = 0;
881
+ let suspiciousCount = 0;
882
+ let maliciousCount = 0;
883
+ let totalDuration = 0;
884
+ let totalBytes = 0;
885
+ let validDurationCount = 0;
886
+ for (const report of reports) {
887
+ if (report.verdict === "clean")
888
+ cleanCount++;
889
+ else if (report.verdict === "suspicious")
890
+ suspiciousCount++;
891
+ else if (report.verdict === "malicious")
892
+ maliciousCount++;
893
+ if (report.durationMs !== undefined) {
894
+ totalDuration += report.durationMs;
895
+ validDurationCount++;
896
+ }
897
+ if (report.file?.size !== undefined) {
898
+ totalBytes += report.file.size;
899
+ }
900
+ }
901
+ const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
902
+ const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
903
+ return {
904
+ totalScans: reports.length,
905
+ cleanCount,
906
+ suspiciousCount,
907
+ maliciousCount,
908
+ avgDurationMs: avgDuration,
909
+ avgThroughputBps: avgThroughput,
910
+ totalBytesScanned: totalBytes,
911
+ };
912
+ }
913
+
914
+ /** Mappa veloce estensione -> mime (basic) */
915
+ function guessMimeByExt(name) {
916
+ if (!name)
917
+ return;
918
+ const ext = name.toLowerCase().split(".").pop();
919
+ switch (ext) {
920
+ case "zip":
921
+ return "application/zip";
922
+ case "png":
923
+ return "image/png";
924
+ case "jpg":
925
+ case "jpeg":
926
+ return "image/jpeg";
927
+ case "pdf":
928
+ return "application/pdf";
929
+ case "txt":
930
+ return "text/plain";
931
+ default:
932
+ return;
933
+ }
934
+ }
935
+ /** Heuristica semplice per verdetto */
936
+ function computeVerdict(matches) {
937
+ if (!matches.length)
938
+ return "clean";
939
+ // se la regola contiene 'zip_' lo marchiamo "suspicious"
940
+ const anyHigh = matches.some((m) => (m.tags ?? []).includes("critical") || (m.tags ?? []).includes("high"));
941
+ return anyHigh ? "malicious" : "suspicious";
942
+ }
943
+ /** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
944
+ function toYaraMatches(ms) {
945
+ return ms.map((m) => ({
680
946
  rule: m.rule,
681
- namespace: 'heuristics',
682
- tags: ['heuristics'].concat(m.severity ? [m.severity] : []),
947
+ namespace: "heuristics",
948
+ tags: ["heuristics"].concat(m.severity ? [m.severity] : []),
683
949
  meta: m.meta,
684
950
  }));
685
951
  }
@@ -693,26 +959,28 @@ async function scanBytes(input, opts = {}) {
693
959
  return cached;
694
960
  }
695
961
  }
696
- const perfTracker = (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)
962
+ const perfTracker = opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking
697
963
  ? new PerformanceTracker()
698
964
  : null;
699
- perfTracker?.checkpoint('prep_start');
700
- const preset = opts.preset ?? opts.config?.defaultPreset ?? 'zip-basic';
965
+ perfTracker?.checkpoint("prep_start");
966
+ const preset = opts.preset ?? opts.config?.defaultPreset ?? "zip-basic";
701
967
  const ctx = {
702
968
  ...opts.ctx,
703
969
  mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
704
970
  size: opts.ctx?.size ?? input.byteLength,
705
971
  };
706
- perfTracker?.checkpoint('prep_end');
707
- perfTracker?.checkpoint('heuristics_start');
972
+ perfTracker?.checkpoint("prep_end");
973
+ perfTracker?.checkpoint("heuristics_start");
708
974
  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');
975
+ const matchesH = await (typeof scanFn === "function"
976
+ ? scanFn
977
+ : scanFn.scan)(input, ctx);
978
+ const allMatches = [...matchesH];
979
+ perfTracker?.checkpoint("heuristics_end");
712
980
  // Advanced detection (enabled by default, can be overridden by config)
713
981
  const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
714
982
  if (advancedEnabled) {
715
- perfTracker?.checkpoint('advanced_start');
983
+ perfTracker?.checkpoint("advanced_start");
716
984
  // Detect polyglot files
717
985
  if (opts.config?.advanced?.enablePolyglotDetection !== false) {
718
986
  const polyglotMatches = detectPolyglot(input);
@@ -727,37 +995,38 @@ async function scanBytes(input, opts = {}) {
727
995
  if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
728
996
  const nestingAnalysis = analyzeNestedArchives(input);
729
997
  const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
730
- if (nestingAnalysis.hasExcessiveNesting || (nestingAnalysis.depth > maxDepth)) {
998
+ if (nestingAnalysis.hasExcessiveNesting || nestingAnalysis.depth > maxDepth) {
731
999
  allMatches.push({
732
- rule: 'excessive_archive_nesting',
733
- severity: 'high',
1000
+ rule: "excessive_archive_nesting",
1001
+ severity: "high",
734
1002
  meta: {
735
- description: 'Excessive archive nesting detected',
1003
+ description: "Excessive archive nesting detected",
736
1004
  depth: nestingAnalysis.depth,
737
1005
  maxAllowed: maxDepth,
738
1006
  },
739
1007
  });
740
1008
  }
741
1009
  }
742
- perfTracker?.checkpoint('advanced_end');
1010
+ perfTracker?.checkpoint("advanced_end");
743
1011
  }
744
1012
  const matches = toYaraMatches(allMatches);
745
1013
  const verdict = computeVerdict(matches);
746
1014
  perfTracker ? perfTracker.getDuration() : Date.now();
747
1015
  const durationMs = perfTracker ? perfTracker.getDuration() : 0;
748
1016
  const report = {
749
- ok: verdict === 'clean',
1017
+ ok: verdict === "clean",
750
1018
  verdict,
751
1019
  matches,
752
- reasons: matches.map(m => m.rule),
1020
+ reasons: matches.map((m) => m.rule),
753
1021
  file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
754
1022
  durationMs,
755
- engine: 'heuristics',
1023
+ engine: "heuristics",
756
1024
  truncated: false,
757
1025
  timedOut: false,
758
1026
  };
759
1027
  // Add performance metrics if tracking enabled
760
- if (perfTracker && (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
1028
+ if (perfTracker &&
1029
+ (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
761
1030
  report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
762
1031
  }
763
1032
  // Cache result if enabled
@@ -771,10 +1040,7 @@ async function scanBytes(input, opts = {}) {
771
1040
  }
772
1041
  /** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
773
1042
  async function scanFile(filePath, opts = {}) {
774
- const [{ readFile, stat }, path] = await Promise.all([
775
- import('fs/promises'),
776
- import('path'),
777
- ]);
1043
+ const [{ readFile, stat }, path] = await Promise.all([import('fs/promises'), import('path')]);
778
1044
  const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
779
1045
  const ctx = {
780
1046
  filename: path.basename(filePath),
@@ -798,21 +1064,6 @@ async function scanFiles(files, opts = {}) {
798
1064
  return out;
799
1065
  }
800
1066
 
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
1067
  const SIG_CEN = 0x02014b50;
817
1068
  const DEFAULTS = {
818
1069
  maxEntries: 1000,
@@ -829,7 +1080,7 @@ function r32(buf, off) {
829
1080
  }
830
1081
  function isZipLike(buf) {
831
1082
  // local file header at start is common
832
- return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
1083
+ return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
833
1084
  }
834
1085
  function lastIndexOfEOCD(buf, window) {
835
1086
  const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
@@ -838,7 +1089,7 @@ function lastIndexOfEOCD(buf, window) {
838
1089
  return idx >= start ? idx : -1;
839
1090
  }
840
1091
  function hasTraversal(name) {
841
- return name.includes('../') || name.includes('..\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name);
1092
+ return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
842
1093
  }
843
1094
  function createZipBombGuard(opts = {}) {
844
1095
  const cfg = { ...DEFAULTS, ...opts };
@@ -852,7 +1103,7 @@ function createZipBombGuard(opts = {}) {
852
1103
  const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
853
1104
  if (eocdPos < 0 || eocdPos + 22 > buf.length) {
854
1105
  // ZIP but no EOCD — malformed or polyglot → suspicious
855
- matches.push({ rule: 'zip_eocd_not_found', severity: 'medium' });
1106
+ matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
856
1107
  return matches;
857
1108
  }
858
1109
  const totalEntries = r16(buf, eocdPos + 10);
@@ -860,7 +1111,7 @@ function createZipBombGuard(opts = {}) {
860
1111
  const cdOffset = r32(buf, eocdPos + 16);
861
1112
  // Bounds check
862
1113
  if (cdOffset + cdSize > buf.length) {
863
- matches.push({ rule: 'zip_cd_out_of_bounds', severity: 'medium' });
1114
+ matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
864
1115
  return matches;
865
1116
  }
866
1117
  // Iterate central directory entries
@@ -881,287 +1132,68 @@ function createZipBombGuard(opts = {}) {
881
1132
  const nameEnd = nameStart + fnLen;
882
1133
  if (nameEnd > buf.length)
883
1134
  break;
884
- const name = buf.toString('utf8', nameStart, nameEnd);
1135
+ const name = buf.toString("utf8", nameStart, nameEnd);
885
1136
  sumComp += compSize;
886
1137
  sumUnc += uncSize;
887
1138
  seen++;
888
1139
  if (name.length > cfg.maxEntryNameLength) {
889
- matches.push({ rule: 'zip_entry_name_too_long', severity: 'medium', meta: { name, length: name.length } });
1140
+ matches.push({
1141
+ rule: "zip_entry_name_too_long",
1142
+ severity: "medium",
1143
+ meta: { name, length: name.length },
1144
+ });
890
1145
  }
891
1146
  if (hasTraversal(name)) {
892
- matches.push({ rule: 'zip_path_traversal_entry', severity: 'medium', meta: { name } });
1147
+ matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
893
1148
  }
894
1149
  // move to next entry
895
1150
  ptr = nameEnd + exLen + cmLen;
896
1151
  }
897
1152
  if (seen !== totalEntries) {
898
1153
  // central dir truncated/odd, still report what we found
899
- matches.push({ rule: 'zip_cd_truncated', severity: 'medium', meta: { seen, totalEntries } });
1154
+ matches.push({
1155
+ rule: "zip_cd_truncated",
1156
+ severity: "medium",
1157
+ meta: { seen, totalEntries },
1158
+ });
900
1159
  }
901
1160
  // Heuristics thresholds
902
1161
  if (seen > cfg.maxEntries) {
903
- matches.push({ rule: 'zip_too_many_entries', severity: 'medium', meta: { seen, limit: cfg.maxEntries } });
1162
+ matches.push({
1163
+ rule: "zip_too_many_entries",
1164
+ severity: "medium",
1165
+ meta: { seen, limit: cfg.maxEntries },
1166
+ });
904
1167
  }
905
1168
  if (sumUnc > cfg.maxTotalUncompressedBytes) {
906
1169
  matches.push({
907
- rule: 'zip_total_uncompressed_too_large',
908
- severity: 'medium',
909
- meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
1170
+ rule: "zip_total_uncompressed_too_large",
1171
+ severity: "medium",
1172
+ meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
910
1173
  });
911
1174
  }
912
1175
  if (sumComp === 0 && sumUnc > 0) {
913
- matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio: Infinity } });
1176
+ matches.push({
1177
+ rule: "zip_suspicious_ratio",
1178
+ severity: "medium",
1179
+ meta: { ratio: Infinity },
1180
+ });
914
1181
  }
915
1182
  else if (sumComp > 0) {
916
1183
  const ratio = sumUnc / Math.max(1, sumComp);
917
1184
  if (ratio >= cfg.maxCompressionRatio) {
918
- matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio, limit: cfg.maxCompressionRatio } });
1185
+ matches.push({
1186
+ rule: "zip_suspicious_ratio",
1187
+ severity: "medium",
1188
+ meta: { ratio, limit: cfg.maxCompressionRatio },
1189
+ });
919
1190
  }
920
1191
  }
921
1192
  return matches;
922
- }
1193
+ },
923
1194
  };
924
1195
  }
925
1196
 
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
1197
  /**
1166
1198
  * Export utilities for scan results
1167
1199
  * @module utils/export
@@ -1177,19 +1209,15 @@ class ScanResultExporter {
1177
1209
  const data = Array.isArray(reports) ? reports : [reports];
1178
1210
  if (!options.includeDetails) {
1179
1211
  // Simplified output
1180
- const simplified = data.map(r => ({
1212
+ const simplified = data.map((r) => ({
1181
1213
  verdict: r.verdict,
1182
1214
  file: r.file?.name,
1183
1215
  matches: r.matches.length,
1184
1216
  durationMs: r.durationMs,
1185
1217
  }));
1186
- return options.prettyPrint
1187
- ? JSON.stringify(simplified, null, 2)
1188
- : JSON.stringify(simplified);
1218
+ return options.prettyPrint ? JSON.stringify(simplified, null, 2) : JSON.stringify(simplified);
1189
1219
  }
1190
- return options.prettyPrint
1191
- ? JSON.stringify(data, null, 2)
1192
- : JSON.stringify(data);
1220
+ return options.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
1193
1221
  }
1194
1222
  /**
1195
1223
  * Export to CSV format
@@ -1197,68 +1225,68 @@ class ScanResultExporter {
1197
1225
  toCSV(reports, options = {}) {
1198
1226
  const data = Array.isArray(reports) ? reports : [reports];
1199
1227
  const headers = [
1200
- 'filename',
1201
- 'verdict',
1202
- 'matches_count',
1203
- 'file_size',
1204
- 'mime_type',
1205
- 'duration_ms',
1206
- 'engine',
1228
+ "filename",
1229
+ "verdict",
1230
+ "matches_count",
1231
+ "file_size",
1232
+ "mime_type",
1233
+ "duration_ms",
1234
+ "engine",
1207
1235
  ];
1208
1236
  if (options.includeDetails) {
1209
- headers.push('reasons', 'match_rules');
1237
+ headers.push("reasons", "match_rules");
1210
1238
  }
1211
- const rows = data.map(report => {
1239
+ const rows = data.map((report) => {
1212
1240
  const row = [
1213
- this.escapeCsv(report.file?.name || 'unknown'),
1241
+ this.escapeCsv(report.file?.name || "unknown"),
1214
1242
  report.verdict,
1215
1243
  report.matches.length.toString(),
1216
1244
  (report.file?.size || 0).toString(),
1217
- this.escapeCsv(report.file?.mimeType || 'unknown'),
1245
+ this.escapeCsv(report.file?.mimeType || "unknown"),
1218
1246
  (report.durationMs || 0).toString(),
1219
- report.engine || 'unknown',
1247
+ report.engine || "unknown",
1220
1248
  ];
1221
1249
  if (options.includeDetails) {
1222
- row.push(this.escapeCsv((report.reasons || []).join('; ')), this.escapeCsv(report.matches.map(m => m.rule).join('; ')));
1250
+ row.push(this.escapeCsv((report.reasons || []).join("; ")), this.escapeCsv(report.matches.map((m) => m.rule).join("; ")));
1223
1251
  }
1224
- return row.join(',');
1252
+ return row.join(",");
1225
1253
  });
1226
- return [headers.join(','), ...rows].join('\n');
1254
+ return [headers.join(","), ...rows].join("\n");
1227
1255
  }
1228
1256
  /**
1229
1257
  * Export to Markdown format
1230
1258
  */
1231
1259
  toMarkdown(reports, options = {}) {
1232
1260
  const data = Array.isArray(reports) ? reports : [reports];
1233
- let md = '# Scan Results\n\n';
1261
+ let md = "# Scan Results\n\n";
1234
1262
  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';
1263
+ const clean = data.filter((r) => r.verdict === "clean").length;
1264
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
1265
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
1266
+ md += "## Summary\n\n";
1239
1267
  md += `- ✅ Clean: ${clean}\n`;
1240
1268
  md += `- ⚠️ Suspicious: ${suspicious}\n`;
1241
1269
  md += `- ❌ Malicious: ${malicious}\n\n`;
1242
- md += '## Detailed Results\n\n';
1270
+ md += "## Detailed Results\n\n";
1243
1271
  for (const report of data) {
1244
- const icon = report.verdict === 'clean' ? '' : report.verdict === 'suspicious' ? '⚠️' : '';
1245
- md += `### ${icon} ${report.file?.name || 'Unknown'}\n\n`;
1272
+ const icon = report.verdict === "clean" ? "" : report.verdict === "suspicious" ? "⚠️" : "";
1273
+ md += `### ${icon} ${report.file?.name || "Unknown"}\n\n`;
1246
1274
  md += `- **Verdict:** ${report.verdict}\n`;
1247
1275
  md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
1248
- md += `- **MIME Type:** ${report.file?.mimeType || 'unknown'}\n`;
1276
+ md += `- **MIME Type:** ${report.file?.mimeType || "unknown"}\n`;
1249
1277
  md += `- **Duration:** ${report.durationMs || 0}ms\n`;
1250
1278
  md += `- **Matches:** ${report.matches.length}\n`;
1251
1279
  if (options.includeDetails && report.matches.length > 0) {
1252
- md += '\n**Match Details:**\n';
1280
+ md += "\n**Match Details:**\n";
1253
1281
  for (const match of report.matches) {
1254
1282
  md += `- ${match.rule}`;
1255
1283
  if (match.tags && match.tags.length > 0) {
1256
- md += ` (${match.tags.join(', ')})`;
1284
+ md += ` (${match.tags.join(", ")})`;
1257
1285
  }
1258
- md += '\n';
1286
+ md += "\n";
1259
1287
  }
1260
1288
  }
1261
- md += '\n';
1289
+ md += "\n";
1262
1290
  }
1263
1291
  return md;
1264
1292
  }
@@ -1268,20 +1296,20 @@ class ScanResultExporter {
1268
1296
  */
1269
1297
  toSARIF(reports, options = {}) {
1270
1298
  const data = Array.isArray(reports) ? reports : [reports];
1271
- const results = data.flatMap(report => {
1272
- if (report.verdict === 'clean')
1299
+ const results = data.flatMap((report) => {
1300
+ if (report.verdict === "clean")
1273
1301
  return [];
1274
- return report.matches.map(match => ({
1302
+ return report.matches.map((match) => ({
1275
1303
  ruleId: match.rule,
1276
- level: report.verdict === 'malicious' ? 'error' : 'warning',
1304
+ level: report.verdict === "malicious" ? "error" : "warning",
1277
1305
  message: {
1278
- text: `${match.rule} detected in ${report.file?.name || 'unknown file'}`,
1306
+ text: `${match.rule} detected in ${report.file?.name || "unknown file"}`,
1279
1307
  },
1280
1308
  locations: [
1281
1309
  {
1282
1310
  physicalLocation: {
1283
1311
  artifactLocation: {
1284
- uri: report.file?.name || 'unknown',
1312
+ uri: report.file?.name || "unknown",
1285
1313
  },
1286
1314
  },
1287
1315
  },
@@ -1293,33 +1321,31 @@ class ScanResultExporter {
1293
1321
  }));
1294
1322
  });
1295
1323
  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',
1324
+ version: "2.1.0",
1325
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
1298
1326
  runs: [
1299
1327
  {
1300
1328
  tool: {
1301
1329
  driver: {
1302
- name: 'Pompelmi',
1303
- version: '0.29.0',
1304
- informationUri: 'https://pompelmi.github.io/pompelmi/',
1330
+ name: "Pompelmi",
1331
+ version: "0.29.0",
1332
+ informationUri: "https://pompelmi.github.io/pompelmi/",
1305
1333
  },
1306
1334
  },
1307
1335
  results,
1308
1336
  },
1309
1337
  ],
1310
1338
  };
1311
- return options.prettyPrint
1312
- ? JSON.stringify(sarif, null, 2)
1313
- : JSON.stringify(sarif);
1339
+ return options.prettyPrint ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
1314
1340
  }
1315
1341
  /**
1316
1342
  * Export to HTML format
1317
1343
  */
1318
1344
  toHTML(reports, options = {}) {
1319
1345
  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;
1346
+ const clean = data.filter((r) => r.verdict === "clean").length;
1347
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
1348
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
1323
1349
  let html = `<!DOCTYPE html>
1324
1350
  <html lang="en">
1325
1351
  <head>
@@ -1351,11 +1377,11 @@ class ScanResultExporter {
1351
1377
  for (const report of data) {
1352
1378
  const statusClass = report.verdict;
1353
1379
  html += `<div class="result ${statusClass}">`;
1354
- html += `<h3>${this.escapeHtml(report.file?.name || 'Unknown')}</h3>`;
1380
+ html += `<h3>${this.escapeHtml(report.file?.name || "Unknown")}</h3>`;
1355
1381
  html += `<table>`;
1356
1382
  html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
1357
1383
  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>`;
1384
+ html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || "unknown")}</td></tr>`;
1359
1385
  html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
1360
1386
  html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
1361
1387
  html += `</table>`;
@@ -1364,7 +1390,7 @@ class ScanResultExporter {
1364
1390
  for (const match of report.matches) {
1365
1391
  html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
1366
1392
  if (match.tags && match.tags.length > 0) {
1367
- html += ` ${match.tags.map(tag => `<span class="badge">${this.escapeHtml(tag)}</span>`).join('')}`;
1393
+ html += ` ${match.tags.map((tag) => `<span class="badge">${this.escapeHtml(tag)}</span>`).join("")}`;
1368
1394
  }
1369
1395
  html += `</li>`;
1370
1396
  }
@@ -1380,41 +1406,41 @@ class ScanResultExporter {
1380
1406
  */
1381
1407
  export(reports, format, options = {}) {
1382
1408
  switch (format) {
1383
- case 'json':
1409
+ case "json":
1384
1410
  return this.toJSON(reports, options);
1385
- case 'csv':
1411
+ case "csv":
1386
1412
  return this.toCSV(reports, options);
1387
- case 'markdown':
1413
+ case "markdown":
1388
1414
  return this.toMarkdown(reports, options);
1389
- case 'html':
1415
+ case "html":
1390
1416
  return this.toHTML(reports, options);
1391
- case 'sarif':
1417
+ case "sarif":
1392
1418
  return this.toSARIF(reports, options);
1393
1419
  default:
1394
1420
  throw new Error(`Unsupported export format: ${format}`);
1395
1421
  }
1396
1422
  }
1397
1423
  escapeCsv(value) {
1398
- if (value.includes(',') || value.includes('"') || value.includes('\n')) {
1424
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1399
1425
  return `"${value.replace(/"/g, '""')}"`;
1400
1426
  }
1401
1427
  return value;
1402
1428
  }
1403
1429
  escapeHtml(value) {
1404
1430
  return value
1405
- .replace(/&/g, '&amp;')
1406
- .replace(/</g, '&lt;')
1407
- .replace(/>/g, '&gt;')
1408
- .replace(/"/g, '&quot;')
1409
- .replace(/'/g, '&#039;');
1431
+ .replace(/&/g, "&amp;")
1432
+ .replace(/</g, "&lt;")
1433
+ .replace(/>/g, "&gt;")
1434
+ .replace(/"/g, "&quot;")
1435
+ .replace(/'/g, "&#039;");
1410
1436
  }
1411
1437
  formatBytes(bytes) {
1412
1438
  if (bytes === 0)
1413
- return '0 Bytes';
1439
+ return "0 Bytes";
1414
1440
  const k = 1024;
1415
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1441
+ const sizes = ["Bytes", "KB", "MB", "GB"];
1416
1442
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1417
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
1443
+ return Math.round((bytes / k ** i) * 100) / 100 + " " + sizes[i];
1418
1444
  }
1419
1445
  }
1420
1446
  /**
@@ -1425,5 +1451,30 @@ function exportScanResults(reports, format, options) {
1425
1451
  return exporter.export(reports, format, options);
1426
1452
  }
1427
1453
 
1454
+ /**
1455
+ * Validates a File by MIME type and size (max 5 MB).
1456
+ */
1457
+ function validateFile(file) {
1458
+ const maxSize = 5 * 1024 * 1024;
1459
+ const allowedTypes = ["text/plain", "application/json", "text/csv"];
1460
+ if (!allowedTypes.includes(file.type)) {
1461
+ return { valid: false, error: "Unsupported file type" };
1462
+ }
1463
+ if (file.size > maxSize) {
1464
+ return { valid: false, error: "File too large (max 5 MB)" };
1465
+ }
1466
+ return { valid: true };
1467
+ }
1468
+
1469
+ function mapMatchesToVerdict(matches = []) {
1470
+ if (!matches.length)
1471
+ return "clean";
1472
+ const malHints = ["trojan", "ransom", "worm", "spy", "rootkit", "keylog", "botnet"];
1473
+ const tagSet = new Set(matches.flatMap((m) => (m.tags ?? []).map((t) => t.toLowerCase())));
1474
+ const nameHit = (r) => malHints.some((h) => r.toLowerCase().includes(h));
1475
+ const isMal = matches.some((m) => nameHit(m.rule)) || tagSet.has("malware") || tagSet.has("critical");
1476
+ return isMal ? "malicious" : "suspicious";
1477
+ }
1478
+
1428
1479
  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
1480
  //# sourceMappingURL=pompelmi.browser.esm.js.map