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