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