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