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
package/dist/pompelmi.cjs CHANGED
@@ -25,1044 +25,631 @@ var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
25
25
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
26
26
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
27
27
 
28
- function hasAsciiToken(buf, token) {
29
- // Use latin1 so we can safely search binary
30
- return buf.indexOf(token, 0, 'latin1') !== -1;
31
- }
32
- function startsWith(buf, bytes) {
33
- if (buf.length < bytes.length)
34
- return false;
35
- for (let i = 0; i < bytes.length; i++)
36
- if (buf[i] !== bytes[i])
37
- return false;
38
- return true;
39
- }
40
- function isPDF(buf) {
41
- // %PDF-
42
- return startsWith(buf, [0x25, 0x50, 0x44, 0x46, 0x2d]);
43
- }
44
- function isOleCfb(buf) {
45
- // D0 CF 11 E0 A1 B1 1A E1
46
- const sig = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
47
- return startsWith(buf, sig);
48
- }
49
- function isZipLike$1(buf) {
50
- // PK\x03\x04
51
- return startsWith(buf, [0x50, 0x4b, 0x03, 0x04]);
52
- }
53
- function isPeExecutable(buf) {
54
- // "MZ"
55
- return startsWith(buf, [0x4d, 0x5a]);
56
- }
57
- /** OOXML macro hint via filename token in ZIP container */
58
- function hasOoxmlMacros(buf) {
59
- if (!isZipLike$1(buf))
60
- return false;
61
- return hasAsciiToken(buf, 'vbaProject.bin');
62
- }
63
- /** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
64
- function pdfRiskTokens(buf) {
65
- const tokens = ['/JavaScript', '/OpenAction', '/AA', '/Launch'];
66
- return tokens.filter(t => hasAsciiToken(buf, t));
67
- }
68
- const CommonHeuristicsScanner = {
69
- async scan(input) {
70
- const buf = Buffer.from(input);
71
- const matches = [];
72
- // Office macros (OLE / OOXML)
73
- if (isOleCfb(buf)) {
74
- matches.push({ rule: 'office_ole_container', severity: 'suspicious' });
75
- }
76
- if (hasOoxmlMacros(buf)) {
77
- matches.push({ rule: 'office_ooxml_macros', severity: 'suspicious' });
78
- }
79
- // PDF risky tokens
80
- if (isPDF(buf)) {
81
- const toks = pdfRiskTokens(buf);
82
- if (toks.length) {
83
- matches.push({
84
- rule: 'pdf_risky_actions',
85
- severity: 'suspicious',
86
- meta: { tokens: toks }
87
- });
88
- }
89
- }
90
- // Executable header
91
- if (isPeExecutable(buf)) {
92
- matches.push({ rule: 'pe_executable_signature', severity: 'suspicious' });
93
- }
94
- // EICAR test file
95
- const EICAR_NEEDLE = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!";
96
- if (hasAsciiToken(buf, EICAR_NEEDLE)) {
97
- matches.push({ rule: 'eicar_test_file', severity: 'high', meta: { note: 'EICAR standard antivirus test file detected' } });
98
- }
99
- return matches;
100
- }
101
- };
102
-
103
- function toScanFn(s) {
104
- return (typeof s === "function" ? s : s.scan);
105
- }
106
- /** Map a Match's severity field to a Verdict for stopOn comparison. */
107
- function matchToVerdict(m) {
108
- const s = m.severity;
109
- if (s === "critical" || s === "high" || s === "malicious")
110
- return "malicious";
111
- if (s === "medium" || s === "low" || s === "suspicious" || s === "info")
112
- return "suspicious";
113
- return "clean";
114
- }
115
- /** Highest verdict across all matches in the list. */
116
- function highestSeverity(matches) {
117
- if (matches.length === 0)
118
- return null;
119
- if (matches.some((m) => matchToVerdict(m) === "malicious"))
120
- return "malicious";
121
- if (matches.some((m) => matchToVerdict(m) === "suspicious"))
122
- return "suspicious";
123
- return "clean";
124
- }
125
- const SEVERITY_RANK = { malicious: 2, suspicious: 1, clean: 0 };
126
- function shouldStop(matches, stopOn) {
127
- if (!stopOn)
128
- return false;
129
- const highest = highestSeverity(matches);
130
- if (!highest)
131
- return false;
132
- return SEVERITY_RANK[highest] >= SEVERITY_RANK[stopOn];
133
- }
134
- async function runWithTimeout(fn, timeoutMs) {
135
- if (!timeoutMs)
136
- return fn();
137
- return new Promise((resolve, reject) => {
138
- const timer = setTimeout(() => reject(new Error("scanner timeout")), timeoutMs);
139
- fn().then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
140
- });
141
- }
142
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
- function composeScanners(...args) {
144
- const first = args[0];
145
- const rest = args.slice(1);
146
- // ── Named-scanner array form ──────────────────────────────────────────────
147
- if (Array.isArray(first) &&
148
- (first.length === 0 || (Array.isArray(first[0]) && typeof first[0][0] === "string"))) {
149
- const entries = first;
150
- const opts = rest.length > 0 && !Array.isArray(rest[0]) && typeof rest[0] !== "function" &&
151
- !(typeof rest[0] === "object" && rest[0] !== null && "scan" in rest[0])
152
- ? rest[0]
153
- : {};
154
- return async (input, ctx) => {
155
- const all = [];
156
- if (opts.parallel) {
157
- // Parallel execution — collect all results then return
158
- const results = await Promise.allSettled(entries.map(([name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
159
- for (let i = 0; i < results.length; i++) {
160
- const result = results[i];
161
- if (result.status === "fulfilled" && Array.isArray(result.value)) {
162
- const matches = opts.tagSourceName
163
- ? result.value.map((m) => ({
164
- ...m,
165
- meta: { ...m.meta, _sourceName: entries[i][0] },
166
- }))
167
- : result.value;
168
- all.push(...matches);
169
- }
170
- }
171
- }
172
- else {
173
- // Sequential execution with optional stopOn short-circuit
174
- for (const [name, scanner] of entries) {
175
- try {
176
- const out = await runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner);
177
- if (Array.isArray(out)) {
178
- const matches = opts.tagSourceName
179
- ? out.map((m) => ({ ...m, meta: { ...m.meta, _sourceName: name } }))
180
- : out;
181
- all.push(...matches);
182
- if (shouldStop(all, opts.stopOn))
183
- break;
184
- }
185
- }
186
- catch {
187
- // individual scanner failure is non-fatal
188
- }
189
- }
190
- }
191
- return all;
192
- };
193
- }
194
- // ── Variadic form (backward-compatible) ───────────────────────────────────
195
- const scanners = [first, ...rest].filter(Boolean);
196
- return async (input, ctx) => {
197
- const all = [];
198
- for (const s of scanners) {
199
- try {
200
- const out = await toScanFn(s)(input, ctx);
201
- if (Array.isArray(out))
202
- all.push(...out);
203
- }
204
- catch {
205
- // ignore individual scanner failures
206
- }
207
- }
208
- return all;
209
- };
210
- }
211
- function createPresetScanner(preset, opts = {}) {
212
- const scanners = [];
213
- // Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
214
- scanners.push(CommonHeuristicsScanner);
215
- // Add decompilation scanners based on preset
216
- if (preset === 'decompilation-basic' || preset === 'decompilation-deep' ||
217
- preset === 'malware-analysis' || opts.enableDecompilation) {
218
- const depth = preset === 'decompilation-deep' ? 'deep' :
219
- preset === 'decompilation-basic' ? 'basic' :
220
- opts.decompilationDepth || 'basic';
221
- if (!opts.decompilationEngine || opts.decompilationEngine === 'binaryninja-hlil' || opts.decompilationEngine === 'both') {
222
- try {
223
- // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
224
- const importModule = new Function('specifier', 'return import(specifier)');
225
- importModule('@pompelmi/engine-binaryninja').then((mod) => {
226
- const binjaScanner = mod.createBinaryNinjaScanner({
227
- timeout: opts.decompilationTimeout || opts.timeout || 30000,
228
- depth,
229
- pythonPath: opts.pythonPath,
230
- binaryNinjaPath: opts.binaryNinjaPath
231
- });
232
- scanners.push(binjaScanner);
233
- }).catch(() => {
234
- // Binary Ninja engine not available - silently skip
235
- });
236
- }
237
- catch {
238
- // Engine not installed
239
- }
240
- }
241
- if (!opts.decompilationEngine || opts.decompilationEngine === 'ghidra-pcode' || opts.decompilationEngine === 'both') {
242
- try {
243
- // Dynamic import for Ghidra engine (when implemented) - using Function to bypass TypeScript type checking
244
- const importModule = new Function('specifier', 'return import(specifier)');
245
- importModule('@pompelmi/engine-ghidra').then((mod) => {
246
- const ghidraScanner = mod.createGhidraScanner({
247
- timeout: opts.decompilationTimeout || opts.timeout || 30000,
248
- depth,
249
- ghidraPath: opts.ghidraPath,
250
- analyzeHeadless: opts.analyzeHeadless
251
- });
252
- scanners.push(ghidraScanner);
253
- }).catch(() => {
254
- // Ghidra engine not available - silently skip
255
- });
256
- }
257
- catch {
258
- // Engine not installed
259
- }
260
- }
261
- }
262
- if (scanners.length === 0) {
263
- // Fallback scanner that returns no matches
264
- return async (_input, _ctx) => {
265
- return [];
266
- };
267
- }
268
- return composeScanners(...scanners);
269
- }
270
-
271
- /**
272
- * Performance monitoring utilities for pompelmi scans
273
- * @module utils/performance-metrics
274
- */
275
28
  /**
276
- * Track performance metrics for a scan operation
277
- */
278
- class PerformanceTracker {
279
- constructor() {
280
- this.checkpoints = new Map();
281
- this.startTime = Date.now();
282
- }
283
- /**
284
- * Mark a checkpoint in the scan process
285
- */
286
- checkpoint(name) {
287
- this.checkpoints.set(name, Date.now());
288
- }
289
- /**
290
- * Get duration since start or since a specific checkpoint
291
- */
292
- getDuration(since) {
293
- const now = Date.now();
294
- if (since && this.checkpoints.has(since)) {
295
- return now - (this.checkpoints.get(since) ?? now);
296
- }
297
- return now - this.startTime;
298
- }
299
- /**
300
- * Generate final metrics report
301
- */
302
- getMetrics(bytesScanned) {
303
- const totalDuration = this.getDuration();
304
- const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
305
- return {
306
- totalDurationMs: totalDuration,
307
- heuristicsDurationMs: this.checkpoints.has('heuristics_end')
308
- ? (this.checkpoints.get('heuristics_end') ?? 0) - (this.checkpoints.get('heuristics_start') ?? 0)
309
- : undefined,
310
- yaraDurationMs: this.checkpoints.has('yara_end')
311
- ? (this.checkpoints.get('yara_end') ?? 0) - (this.checkpoints.get('yara_start') ?? 0)
312
- : undefined,
313
- prepDurationMs: this.checkpoints.has('prep_end')
314
- ? (this.checkpoints.get('prep_end') ?? 0) - this.startTime
315
- : undefined,
316
- throughputBps: throughput,
317
- bytesScanned,
318
- startedAt: this.startTime,
319
- completedAt: Date.now(),
320
- };
321
- }
322
- }
323
- /**
324
- * Aggregate statistics from multiple scan reports
325
- */
326
- function aggregateScanStats(reports) {
327
- let cleanCount = 0;
328
- let suspiciousCount = 0;
329
- let maliciousCount = 0;
330
- let totalDuration = 0;
331
- let totalBytes = 0;
332
- let validDurationCount = 0;
333
- for (const report of reports) {
334
- if (report.verdict === 'clean')
335
- cleanCount++;
336
- else if (report.verdict === 'suspicious')
337
- suspiciousCount++;
338
- else if (report.verdict === 'malicious')
339
- maliciousCount++;
340
- if (report.durationMs !== undefined) {
341
- totalDuration += report.durationMs;
342
- validDurationCount++;
343
- }
344
- if (report.file?.size !== undefined) {
345
- totalBytes += report.file.size;
346
- }
347
- }
348
- const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
349
- const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
350
- return {
351
- totalScans: reports.length,
352
- cleanCount,
353
- suspiciousCount,
354
- maliciousCount,
355
- avgDurationMs: avgDuration,
356
- avgThroughputBps: avgThroughput,
357
- totalBytesScanned: totalBytes,
358
- };
359
- }
360
-
361
- /**
362
- * Advanced threat detection utilities
363
- * @module utils/advanced-detection
364
- */
365
- /**
366
- * Enhanced polyglot file detection
367
- * Detects files that can be interpreted as multiple formats
29
+ * Advanced configuration system for pompelmi
30
+ * @module config
368
31
  */
369
- function detectPolyglot(bytes) {
370
- const matches = [];
371
- // Check for PDF/ZIP polyglot
372
- if (isPDFZipPolyglot(bytes)) {
373
- matches.push({
374
- rule: 'polyglot_pdf_zip',
375
- severity: 'high',
376
- meta: { description: 'File can be interpreted as both PDF and ZIP' },
377
- });
378
- }
379
- // Check for image/script polyglot
380
- if (isImageScriptPolyglot(bytes)) {
381
- matches.push({
382
- rule: 'polyglot_image_script',
383
- severity: 'high',
384
- meta: { description: 'Image file contains executable script content' },
385
- });
386
- }
387
- // Check for GIFAR (GIF/JAR polyglot)
388
- if (isGIFAR(bytes)) {
389
- matches.push({
390
- rule: 'polyglot_gifar',
391
- severity: 'critical',
392
- meta: { description: 'GIF file contains Java archive' },
393
- });
394
- }
395
- return matches;
396
- }
397
32
  /**
398
- * Detect obfuscated JavaScript/VBScript
399
- */
400
- function detectObfuscatedScripts(bytes) {
401
- const matches = [];
402
- const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
403
- // Check for common obfuscation patterns
404
- const obfuscationPatterns = [
405
- /eval\s*\(\s*unescape\s*\(/gi,
406
- /eval\s*\(\s*atob\s*\(/gi,
407
- /String\.fromCharCode\s*\(\s*\d+(?:\s*,\s*\d+){10,}/gi,
408
- /[a-z0-9]{100,}/gi, // Long encoded strings
409
- /\\x[0-9a-f]{2}/gi, // Hex escapes
410
- ];
411
- for (const pattern of obfuscationPatterns) {
412
- if (pattern.test(text)) {
413
- matches.push({
414
- rule: 'obfuscated_script',
415
- severity: 'medium',
416
- meta: {
417
- description: 'Detected obfuscated script content',
418
- pattern: pattern.source,
419
- },
420
- });
421
- break;
422
- }
423
- }
424
- return matches;
425
- }
426
- /**
427
- * Enhanced nested archive detection with depth limits
33
+ * Default configuration
428
34
  */
429
- function analyzeNestedArchives(bytes, maxDepth = 10) {
430
- let depth = 0;
431
- let currentBytes = bytes;
432
- while (depth < maxDepth) {
433
- if (isArchive(currentBytes)) {
434
- depth++;
435
- {
436
- break;
437
- }
438
- }
439
- else {
440
- break;
441
- }
442
- }
443
- return {
444
- depth,
445
- hasExcessiveNesting: depth >= 5,
446
- };
447
- }
448
- // Helper functions
449
- function isPDFZipPolyglot(bytes) {
450
- if (bytes.length < 8)
451
- return false;
452
- // Check for PDF signature
453
- const hasPDF = bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46;
454
- // Check for ZIP signature anywhere in the file
455
- let hasZIP = false;
456
- for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
457
- if (bytes[i] === 0x50 && bytes[i + 1] === 0x4B && bytes[i + 2] === 0x03 && bytes[i + 3] === 0x04) {
458
- hasZIP = true;
459
- break;
460
- }
461
- }
462
- return hasPDF && hasZIP;
463
- }
464
- function isImageScriptPolyglot(bytes) {
465
- if (bytes.length < 100)
466
- return false;
467
- // Check for image signatures
468
- const isImage = ((bytes[0] === 0xFF && bytes[1] === 0xD8) || // JPEG
469
- (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) || // PNG
470
- (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) // GIF
471
- );
472
- if (!isImage)
473
- return false;
474
- // Check for script content
475
- const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
476
- return /<script|javascript:|eval\(|function\s*\(/i.test(text);
477
- }
478
- function isGIFAR(bytes) {
479
- if (bytes.length < 100)
480
- return false;
481
- // Check for GIF signature
482
- const isGIF = bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46;
483
- // Check for ZIP/JAR signature
484
- let hasZIP = false;
485
- for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
486
- if (bytes[i] === 0x50 && bytes[i + 1] === 0x4B && bytes[i + 2] === 0x03 && bytes[i + 3] === 0x04) {
487
- hasZIP = true;
488
- break;
489
- }
490
- }
491
- return isGIF && hasZIP;
492
- }
493
- function isArchive(bytes) {
494
- if (bytes.length < 4)
495
- return false;
496
- return (
497
- // ZIP
498
- (bytes[0] === 0x50 && bytes[1] === 0x4B && bytes[2] === 0x03 && bytes[3] === 0x04) ||
499
- // RAR
500
- (bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
501
- // 7z
502
- (bytes[0] === 0x37 && bytes[1] === 0x7A && bytes[2] === 0xBC && bytes[3] === 0xAF) ||
503
- // tar.gz
504
- (bytes[0] === 0x1F && bytes[1] === 0x8B));
505
- }
506
-
35
+ const DEFAULT_CONFIG = {
36
+ defaultPreset: "zip-basic",
37
+ performance: {
38
+ enableCache: false,
39
+ enablePerformanceTracking: false,
40
+ enableParallel: true,
41
+ maxConcurrency: 5,
42
+ cacheOptions: {
43
+ maxSize: 1000,
44
+ ttl: 3600000, // 1 hour
45
+ enableLRU: true,
46
+ enableStats: false,
47
+ },
48
+ },
49
+ security: {
50
+ maxFileSize: 100 * 1024 * 1024, // 100MB
51
+ enableThreatIntel: false,
52
+ scanTimeout: 30000, // 30 seconds
53
+ strictMode: false,
54
+ },
55
+ advanced: {
56
+ enablePolyglotDetection: true,
57
+ enableObfuscationDetection: true,
58
+ enableNestedArchiveAnalysis: true,
59
+ maxArchiveDepth: 5,
60
+ },
61
+ logging: {
62
+ verbose: false,
63
+ level: "info",
64
+ enableStats: false,
65
+ },
66
+ };
507
67
  /**
508
- * Cache management system for scan results
509
- * @module utils/cache-manager
68
+ * Configuration presets for common use cases
510
69
  */
70
+ const CONFIG_PRESETS = {
71
+ /** Fast scanning with minimal features */
72
+ fast: {
73
+ defaultPreset: "basic",
74
+ performance: {
75
+ enableCache: true,
76
+ enablePerformanceTracking: false,
77
+ maxConcurrency: 10,
78
+ },
79
+ advanced: {
80
+ enablePolyglotDetection: false,
81
+ enableObfuscationDetection: false,
82
+ enableNestedArchiveAnalysis: false,
83
+ },
84
+ },
85
+ /** Balanced scanning (recommended) */
86
+ balanced: DEFAULT_CONFIG,
87
+ /** Thorough scanning with all features */
88
+ thorough: {
89
+ defaultPreset: "advanced",
90
+ performance: {
91
+ enableCache: true,
92
+ enablePerformanceTracking: true,
93
+ maxConcurrency: 3,
94
+ },
95
+ security: {
96
+ maxFileSize: 500 * 1024 * 1024, // 500MB
97
+ enableThreatIntel: true,
98
+ scanTimeout: 60000, // 60 seconds
99
+ strictMode: true,
100
+ },
101
+ advanced: {
102
+ enablePolyglotDetection: true,
103
+ enableObfuscationDetection: true,
104
+ enableNestedArchiveAnalysis: true,
105
+ maxArchiveDepth: 10,
106
+ },
107
+ logging: {
108
+ verbose: true,
109
+ level: "debug",
110
+ enableStats: true,
111
+ },
112
+ },
113
+ /** Production-ready configuration */
114
+ production: {
115
+ defaultPreset: "advanced",
116
+ performance: {
117
+ enableCache: true,
118
+ enablePerformanceTracking: true,
119
+ maxConcurrency: 5,
120
+ cacheOptions: {
121
+ maxSize: 5000,
122
+ ttl: 7200000, // 2 hours
123
+ enableLRU: true,
124
+ enableStats: true,
125
+ },
126
+ },
127
+ security: {
128
+ maxFileSize: 200 * 1024 * 1024, // 200MB
129
+ enableThreatIntel: true,
130
+ scanTimeout: 45000,
131
+ strictMode: false,
132
+ },
133
+ advanced: {
134
+ enablePolyglotDetection: true,
135
+ enableObfuscationDetection: true,
136
+ enableNestedArchiveAnalysis: true,
137
+ maxArchiveDepth: 7,
138
+ },
139
+ logging: {
140
+ verbose: false,
141
+ level: "warn",
142
+ enableStats: true,
143
+ },
144
+ },
145
+ /** Development configuration */
146
+ development: {
147
+ defaultPreset: "basic",
148
+ performance: {
149
+ enableCache: false,
150
+ enablePerformanceTracking: true,
151
+ maxConcurrency: 3,
152
+ },
153
+ security: {
154
+ maxFileSize: 50 * 1024 * 1024, // 50MB
155
+ scanTimeout: 15000,
156
+ strictMode: false,
157
+ },
158
+ logging: {
159
+ verbose: true,
160
+ level: "debug",
161
+ enableStats: true,
162
+ },
163
+ },
164
+ };
511
165
  /**
512
- * LRU cache for scan results with TTL support
166
+ * Configuration manager
513
167
  */
514
- class ScanCacheManager {
515
- constructor(options = {}) {
516
- this.cache = new Map();
517
- // Statistics
518
- this.stats = {
519
- hits: 0,
520
- misses: 0,
521
- evictions: 0,
522
- };
523
- this.maxSize = options.maxSize ?? 1000;
524
- this.ttl = options.ttl ?? 3600000; // 1 hour default
525
- this.enableLRU = options.enableLRU ?? true;
526
- this.enableStats = options.enableStats ?? false;
168
+ class ConfigManager {
169
+ constructor(initialConfig) {
170
+ this.config = this.mergeConfig(DEFAULT_CONFIG, initialConfig || {});
527
171
  }
528
172
  /**
529
- * Generate cache key from file content
173
+ * Get current configuration
530
174
  */
531
- generateKey(content, preset) {
532
- const hash = crypto.createHash('sha256')
533
- .update(content)
534
- .update(preset || 'default')
535
- .digest('hex');
536
- return hash;
175
+ getConfig() {
176
+ return { ...this.config };
537
177
  }
538
178
  /**
539
- * Check if cache entry is still valid
179
+ * Update configuration
540
180
  */
541
- isValid(entry) {
542
- return Date.now() - entry.timestamp < this.ttl;
181
+ updateConfig(updates) {
182
+ this.config = this.mergeConfig(this.config, updates);
543
183
  }
544
184
  /**
545
- * Evict oldest or least-used entry when cache is full
185
+ * Load a preset configuration
546
186
  */
547
- evict() {
548
- if (this.cache.size === 0)
549
- return;
550
- let targetKey = null;
551
- let oldestTime = Infinity;
552
- let lowestAccess = Infinity;
553
- for (const [key, entry] of this.cache.entries()) {
554
- if (this.enableLRU) {
555
- // LRU: evict least recently used
556
- if (entry.timestamp < oldestTime) {
557
- oldestTime = entry.timestamp;
558
- targetKey = key;
559
- }
560
- }
561
- else {
562
- // LFU: evict least frequently used
563
- if (entry.accessCount < lowestAccess) {
564
- lowestAccess = entry.accessCount;
565
- targetKey = key;
566
- }
567
- }
568
- }
569
- if (targetKey) {
570
- this.cache.delete(targetKey);
571
- if (this.enableStats)
572
- this.stats.evictions++;
573
- }
187
+ loadPreset(preset) {
188
+ const presetConfig = CONFIG_PRESETS[preset];
189
+ this.config = this.mergeConfig(DEFAULT_CONFIG, presetConfig);
574
190
  }
575
191
  /**
576
- * Store scan result in cache
192
+ * Reset to default configuration
577
193
  */
578
- set(content, report, preset) {
579
- const key = this.generateKey(content, preset);
580
- // Evict if necessary
581
- if (this.cache.size >= this.maxSize) {
582
- this.evict();
583
- }
584
- this.cache.set(key, {
585
- report,
586
- timestamp: Date.now(),
587
- accessCount: 0,
588
- });
194
+ reset() {
195
+ this.config = { ...DEFAULT_CONFIG };
589
196
  }
590
197
  /**
591
- * Retrieve scan result from cache
198
+ * Get a specific configuration value
592
199
  */
593
- get(content, preset) {
594
- const key = this.generateKey(content, preset);
595
- const entry = this.cache.get(key);
596
- if (!entry) {
597
- if (this.enableStats)
598
- this.stats.misses++;
599
- return null;
600
- }
601
- if (!this.isValid(entry)) {
602
- this.cache.delete(key);
603
- if (this.enableStats)
604
- this.stats.misses++;
605
- return null;
606
- }
607
- // Update access tracking
608
- entry.accessCount++;
609
- entry.timestamp = Date.now(); // Update for LRU
610
- if (this.enableStats)
611
- this.stats.hits++;
612
- return entry.report;
200
+ get(key) {
201
+ return this.config[key];
613
202
  }
614
203
  /**
615
- * Check if result exists in cache
204
+ * Set a specific configuration value
616
205
  */
617
- has(content, preset) {
618
- const key = this.generateKey(content, preset);
619
- const entry = this.cache.get(key);
620
- return entry !== undefined && this.isValid(entry);
206
+ set(key, value) {
207
+ this.config[key] = value;
621
208
  }
622
209
  /**
623
- * Clear entire cache
210
+ * Validate configuration
624
211
  */
625
- clear() {
626
- this.cache.clear();
627
- if (this.enableStats) {
628
- this.stats.hits = 0;
629
- this.stats.misses = 0;
630
- this.stats.evictions = 0;
212
+ validate() {
213
+ const errors = [];
214
+ // Validate performance settings
215
+ if (this.config.performance?.maxConcurrency !== undefined) {
216
+ if (this.config.performance.maxConcurrency < 1) {
217
+ errors.push("maxConcurrency must be at least 1");
218
+ }
219
+ if (this.config.performance.maxConcurrency > 50) {
220
+ errors.push("maxConcurrency should not exceed 50");
221
+ }
631
222
  }
632
- }
633
- /**
634
- * Remove expired entries
635
- */
636
- prune() {
637
- let removed = 0;
638
- for (const [key, entry] of this.cache.entries()) {
639
- if (!this.isValid(entry)) {
640
- this.cache.delete(key);
641
- removed++;
223
+ // Validate security settings
224
+ if (this.config.security?.maxFileSize !== undefined) {
225
+ if (this.config.security.maxFileSize < 1024) {
226
+ errors.push("maxFileSize must be at least 1KB");
642
227
  }
643
228
  }
644
- return removed;
229
+ if (this.config.security?.scanTimeout !== undefined) {
230
+ if (this.config.security.scanTimeout < 1000) {
231
+ errors.push("scanTimeout must be at least 1000ms");
232
+ }
233
+ }
234
+ // Validate advanced settings
235
+ if (this.config.advanced?.maxArchiveDepth !== undefined) {
236
+ if (this.config.advanced.maxArchiveDepth < 1) {
237
+ errors.push("maxArchiveDepth must be at least 1");
238
+ }
239
+ if (this.config.advanced.maxArchiveDepth > 20) {
240
+ errors.push("maxArchiveDepth should not exceed 20");
241
+ }
242
+ }
243
+ return {
244
+ valid: errors.length === 0,
245
+ errors,
246
+ };
645
247
  }
646
248
  /**
647
- * Get cache statistics
249
+ * Deep merge configuration objects
648
250
  */
649
- getStats() {
650
- const total = this.stats.hits + this.stats.misses;
651
- const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
251
+ mergeConfig(base, updates) {
652
252
  return {
653
- hits: this.stats.hits,
654
- misses: this.stats.misses,
655
- size: this.cache.size,
656
- hitRate,
657
- evictions: this.stats.evictions,
253
+ ...base,
254
+ ...updates,
255
+ performance: {
256
+ ...base.performance,
257
+ ...updates.performance,
258
+ cacheOptions: {
259
+ ...base.performance?.cacheOptions,
260
+ ...updates.performance?.cacheOptions,
261
+ },
262
+ },
263
+ security: {
264
+ ...base.security,
265
+ ...updates.security,
266
+ },
267
+ advanced: {
268
+ ...base.advanced,
269
+ ...updates.advanced,
270
+ },
271
+ logging: {
272
+ ...base.logging,
273
+ ...updates.logging,
274
+ },
275
+ callbacks: {
276
+ ...base.callbacks,
277
+ ...updates.callbacks,
278
+ },
279
+ presetOptions: {
280
+ ...base.presetOptions,
281
+ ...updates.presetOptions,
282
+ },
658
283
  };
659
284
  }
660
285
  /**
661
- * Get current cache size
286
+ * Export configuration as JSON
662
287
  */
663
- get size() {
664
- return this.cache.size;
288
+ toJSON() {
289
+ return JSON.stringify(this.config, null, 2);
290
+ }
291
+ /**
292
+ * Load configuration from JSON
293
+ */
294
+ fromJSON(json) {
295
+ try {
296
+ const parsed = JSON.parse(json);
297
+ this.config = this.mergeConfig(DEFAULT_CONFIG, parsed);
298
+ }
299
+ catch (error) {
300
+ throw new Error(`Failed to parse configuration JSON: ${error}`);
301
+ }
665
302
  }
666
303
  }
667
- // Export singleton instance for convenience
668
- let defaultCache = null;
669
304
  /**
670
- * Get or create the default cache instance
305
+ * Create a new configuration manager
671
306
  */
672
- function getDefaultCache(options) {
673
- if (!defaultCache) {
674
- defaultCache = new ScanCacheManager(options);
675
- }
676
- return defaultCache;
307
+ function createConfig(config) {
308
+ return new ConfigManager(config);
677
309
  }
678
310
  /**
679
- * Reset the default cache instance
311
+ * Get a preset configuration
680
312
  */
681
- function resetDefaultCache() {
682
- defaultCache = null;
683
- }
684
-
685
- /** Mappa veloce estensione -> mime (basic) */
686
- function guessMimeByExt(name) {
687
- if (!name)
688
- return;
689
- const ext = name.toLowerCase().split('.').pop();
690
- switch (ext) {
691
- case 'zip': return 'application/zip';
692
- case 'png': return 'image/png';
693
- case 'jpg':
694
- case 'jpeg': return 'image/jpeg';
695
- case 'pdf': return 'application/pdf';
696
- case 'txt': return 'text/plain';
697
- default: return;
698
- }
699
- }
700
- /** Heuristica semplice per verdetto */
701
- function computeVerdict(matches) {
702
- if (!matches.length)
703
- return 'clean';
704
- // se la regola contiene 'zip_' lo marchiamo "suspicious"
705
- const anyHigh = matches.some(m => (m.tags ?? []).includes('critical') || (m.tags ?? []).includes('high'));
706
- return anyHigh ? 'malicious' : 'suspicious';
707
- }
708
- /** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
709
- function toYaraMatches(ms) {
710
- return ms.map(m => ({
711
- rule: m.rule,
712
- namespace: 'heuristics',
713
- tags: ['heuristics'].concat(m.severity ? [m.severity] : []),
714
- meta: m.meta,
715
- }));
716
- }
717
- /** Scan di bytes (browser/node) usando preset (default: zip-basic) */
718
- async function scanBytes(input, opts = {}) {
719
- // Check cache first if enabled
720
- if (opts.enableCache || opts.config?.performance?.enableCache) {
721
- const cache = getDefaultCache(opts.config?.performance?.cacheOptions);
722
- const cached = cache.get(input, opts.preset);
723
- if (cached) {
724
- return cached;
725
- }
726
- }
727
- const perfTracker = (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)
728
- ? new PerformanceTracker()
729
- : null;
730
- perfTracker?.checkpoint('prep_start');
731
- const preset = opts.preset ?? opts.config?.defaultPreset ?? 'zip-basic';
732
- const ctx = {
733
- ...opts.ctx,
734
- mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
735
- size: opts.ctx?.size ?? input.byteLength,
736
- };
737
- perfTracker?.checkpoint('prep_end');
738
- perfTracker?.checkpoint('heuristics_start');
739
- const scanFn = createPresetScanner(preset);
740
- const matchesH = await (typeof scanFn === "function" ? scanFn : scanFn.scan)(input, ctx);
741
- let allMatches = [...matchesH];
742
- perfTracker?.checkpoint('heuristics_end');
743
- // Advanced detection (enabled by default, can be overridden by config)
744
- const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
745
- if (advancedEnabled) {
746
- perfTracker?.checkpoint('advanced_start');
747
- // Detect polyglot files
748
- if (opts.config?.advanced?.enablePolyglotDetection !== false) {
749
- const polyglotMatches = detectPolyglot(input);
750
- allMatches.push(...polyglotMatches);
751
- }
752
- // Detect obfuscated scripts
753
- if (opts.config?.advanced?.enableObfuscationDetection !== false) {
754
- const obfuscatedMatches = detectObfuscatedScripts(input);
755
- allMatches.push(...obfuscatedMatches);
756
- }
757
- // Check for excessive nesting in archives
758
- if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
759
- const nestingAnalysis = analyzeNestedArchives(input);
760
- const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
761
- if (nestingAnalysis.hasExcessiveNesting || (nestingAnalysis.depth > maxDepth)) {
762
- allMatches.push({
763
- rule: 'excessive_archive_nesting',
764
- severity: 'high',
765
- meta: {
766
- description: 'Excessive archive nesting detected',
767
- depth: nestingAnalysis.depth,
768
- maxAllowed: maxDepth,
769
- },
770
- });
771
- }
772
- }
773
- perfTracker?.checkpoint('advanced_end');
774
- }
775
- const matches = toYaraMatches(allMatches);
776
- const verdict = computeVerdict(matches);
777
- perfTracker ? perfTracker.getDuration() : Date.now();
778
- const durationMs = perfTracker ? perfTracker.getDuration() : 0;
779
- const report = {
780
- ok: verdict === 'clean',
781
- verdict,
782
- matches,
783
- reasons: matches.map(m => m.rule),
784
- file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
785
- durationMs,
786
- engine: 'heuristics',
787
- truncated: false,
788
- timedOut: false,
789
- };
790
- // Add performance metrics if tracking enabled
791
- if (perfTracker && (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
792
- report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
793
- }
794
- // Cache result if enabled
795
- if (opts.enableCache || opts.config?.performance?.enableCache) {
796
- const cache = getDefaultCache(opts.config?.performance?.cacheOptions);
797
- cache.set(input, report, opts.preset);
798
- }
799
- // Invoke callbacks if configured
800
- opts.config?.callbacks?.onScanComplete?.(report);
801
- return report;
802
- }
803
- /** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
804
- async function scanFile(filePath, opts = {}) {
805
- const [{ readFile, stat }, path] = await Promise.all([
806
- import('fs/promises'),
807
- import('path'),
808
- ]);
809
- const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
810
- const ctx = {
811
- filename: path.basename(filePath),
812
- mimeType: guessMimeByExt(filePath),
813
- size: st.size,
814
- };
815
- return scanBytes(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), { ...opts, ctx });
816
- }
817
- /** Scan multipli File (browser) usando scanBytes + preset di default */
818
- async function scanFiles(files, opts = {}) {
819
- const list = Array.from(files);
820
- const out = [];
821
- for (const f of list) {
822
- const buf = new Uint8Array(await f.arrayBuffer());
823
- const rep = await scanBytes(buf, {
824
- ...opts,
825
- ctx: { filename: f.name, mimeType: f.type || guessMimeByExt(f.name), size: f.size },
826
- });
827
- out.push(rep);
828
- }
829
- return out;
313
+ function getPresetConfig(preset) {
314
+ return { ...DEFAULT_CONFIG, ...CONFIG_PRESETS[preset] };
830
315
  }
831
316
 
832
317
  /**
833
- * Validates a File by MIME type and size (max 5 MB).
318
+ * HIPAA Compliance Module for Pompelmi
319
+ *
320
+ * This module provides comprehensive HIPAA compliance features for healthcare environments
321
+ * where Pompelmi is used to analyze potentially compromised systems containing PHI.
322
+ *
323
+ * Key protections:
324
+ * - Data sanitization and redaction
325
+ * - Secure temporary file handling
326
+ * - Audit logging
327
+ * - Memory protection
328
+ * - Error message sanitization
834
329
  */
835
- function validateFile(file) {
836
- const maxSize = 5 * 1024 * 1024;
837
- const allowedTypes = ['text/plain', 'application/json', 'text/csv'];
838
- if (!allowedTypes.includes(file.type)) {
839
- return { valid: false, error: 'Unsupported file type' };
330
+ class HipaaComplianceManager {
331
+ constructor(config) {
332
+ this.auditEvents = [];
333
+ this.config = {
334
+ sanitizeErrors: true,
335
+ sanitizeFilenames: true,
336
+ encryptTempFiles: true,
337
+ memoryProtection: true,
338
+ requireSecureTransport: true,
339
+ ...config,
340
+ enabled: config.enabled !== undefined ? config.enabled : true,
341
+ };
342
+ this.sessionId = this.generateSessionId();
840
343
  }
841
- if (file.size > maxSize) {
842
- return { valid: false, error: 'File too large (max 5 MB)' };
344
+ /**
345
+ * Sanitize filename to prevent PHI leakage in logs
346
+ */
347
+ sanitizeFilename(filename) {
348
+ if (!this.config.enabled || !this.config.sanitizeFilenames || !filename) {
349
+ return filename || "unknown";
350
+ }
351
+ // Remove potentially sensitive path information
352
+ const basename = path__namespace.basename(filename);
353
+ // Hash the filename to create a consistent but non-revealing identifier
354
+ const hash = crypto__namespace.createHash("sha256").update(basename).digest("hex").substring(0, 8);
355
+ // Preserve file extension for analysis purposes
356
+ const ext = path__namespace.extname(basename);
357
+ return `file_${hash}${ext}`;
843
358
  }
844
- return { valid: true };
845
- }
846
-
847
- async function createRemoteEngine(opts) {
848
- const { endpoint, headers = {}, rulesField = 'rules', fileField = 'file', mode = 'multipart', rulesAsBase64 = false, } = opts;
849
- const engine = {
850
- async compile(rulesSource) {
851
- return {
852
- async scan(data) {
853
- const fetchFn = globalThis.fetch;
854
- if (!fetchFn)
855
- throw new Error('[remote-yara] fetch non disponibile in questo ambiente');
856
- let res;
857
- if (mode === 'multipart') {
858
- const FormDataCtor = globalThis.FormData;
859
- const BlobCtor = globalThis.Blob;
860
- if (!FormDataCtor || !BlobCtor) {
861
- throw new Error('[remote-yara] FormData/Blob non disponibili (usa json-base64 oppure esegui in browser)');
862
- }
863
- const form = new FormDataCtor();
864
- form.set(rulesField, new BlobCtor([rulesSource], { type: 'text/plain' }), 'rules.yar');
865
- form.set(fileField, new BlobCtor([data], { type: 'application/octet-stream' }), 'sample.bin');
866
- res = await fetchFn(endpoint, { method: 'POST', body: form, headers });
867
- }
868
- else {
869
- const b64 = base64FromBytes(data);
870
- const payload = { [fileField]: b64 };
871
- if (rulesAsBase64) {
872
- payload['rulesB64'] = base64FromString(rulesSource);
873
- }
874
- else {
875
- payload[rulesField] = rulesSource;
876
- }
877
- res = await fetchFn(endpoint, {
878
- method: 'POST',
879
- headers: { 'Content-Type': 'application/json', ...headers },
880
- body: JSON.stringify(payload),
881
- });
882
- }
883
- if (!res.ok) {
884
- throw new Error(`[remote-yara] HTTP ${res.status} ${res.statusText}`);
885
- }
886
- const json = await res.json().catch(() => null);
887
- const arr = Array.isArray(json) ? json : (json?.matches ?? []);
888
- return (arr ?? []).map((m) => ({
889
- rule: m.rule ?? m.ruleIdentifier ?? 'unknown',
890
- tags: m.tags ?? [],
891
- }));
892
- },
893
- };
894
- },
895
- };
896
- return engine;
897
- }
898
- // Helpers
899
- function base64FromBytes(bytes) {
900
- // usa btoa se disponibile (browser); altrimenti fallback manuale
901
- const btoaFn = globalThis.btoa;
902
- let bin = '';
903
- for (let i = 0; i < bytes.byteLength; i++)
904
- bin += String.fromCharCode(bytes[i]);
905
- return btoaFn ? btoaFn(bin) : Buffer.from(bin, 'binary').toString('base64');
906
- }
907
- function base64FromString(s) {
908
- const btoaFn = globalThis.btoa;
909
- return btoaFn ? btoaFn(s) : Buffer.from(s, 'utf8').toString('base64');
910
- }
911
-
912
- // src/scan/remote.ts
913
- /**
914
- * Scansiona una lista di File nel browser usando il motore remoto via HTTP.
915
- * Non richiede WASM né dipendenze native sul client.
916
- */
917
- async function scanFilesWithRemoteYara(files, rulesSource, remote) {
918
- const engine = await createRemoteEngine(remote);
919
- const compiled = await engine.compile(rulesSource);
920
- const results = [];
921
- for (const file of files) {
359
+ /**
360
+ * Sanitize error messages to prevent PHI exposure
361
+ */
362
+ sanitizeError(error) {
363
+ if (!this.config.enabled || !this.config.sanitizeErrors) {
364
+ return typeof error === "string" ? error : error.message;
365
+ }
366
+ const message = typeof error === "string" ? error : error.message;
367
+ // Remove common patterns that might contain PHI
368
+ const sanitized = message
369
+ // Remove file paths
370
+ .replace(/[A-Za-z]:\\\\[^\\s]+/g, "[REDACTED_PATH]")
371
+ .replace(/\/[^\\s]+/g, "[REDACTED_PATH]")
372
+ // Remove potential patient identifiers (numbers that could be MRNs, SSNs)
373
+ .replace(/\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g, "[REDACTED_ID]")
374
+ .replace(/\\b\\d{6,}\\b/g, "[REDACTED_ID]")
375
+ // Remove email addresses
376
+ .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g, "[REDACTED_EMAIL]")
377
+ // Remove potential names (capitalize words in error messages)
378
+ .replace(/\\b[A-Z][a-z]+\\s+[A-Z][a-z]+\\b/g, "[REDACTED_NAME]")
379
+ // Remove IP addresses
380
+ .replace(/\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, "[REDACTED_IP]");
381
+ return sanitized;
382
+ }
383
+ /**
384
+ * Create secure temporary file path with encryption if enabled
385
+ */
386
+ createSecureTempPath(prefix = "pompelmi") {
387
+ if (!this.config.enabled) {
388
+ return path__namespace.join(os__namespace.tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
389
+ }
390
+ // Use cryptographically secure random names
391
+ const randomId = crypto__namespace.randomBytes(16).toString("hex");
392
+ const timestamp = Date.now();
393
+ // Create path in secure temp directory
394
+ const secureTempDir = this.getSecureTempDir();
395
+ const tempPath = path__namespace.join(secureTempDir, `${prefix}-${timestamp}-${randomId}`);
396
+ this.auditLog("temp_file_created", {
397
+ action: "create_temp_file",
398
+ success: true,
399
+ metadata: { path: this.sanitizeFilename(tempPath) },
400
+ });
401
+ return tempPath;
402
+ }
403
+ /**
404
+ * Get or create secure temporary directory with restricted permissions
405
+ */
406
+ getSecureTempDir() {
407
+ const secureTempPath = path__namespace.join(os__namespace.tmpdir(), "pompelmi-secure");
922
408
  try {
923
- const bytes = new Uint8Array(await file.arrayBuffer());
924
- const matches = await compiled.scan(bytes);
925
- results.push({ file, matches });
409
+ const fs = require("fs");
410
+ if (!fs.existsSync(secureTempPath)) {
411
+ fs.mkdirSync(secureTempPath, { mode: 0o700 }); // Owner read/write/execute only
412
+ }
926
413
  }
927
- catch (err) {
928
- console.warn('[remote-yara] scan error for', file.name, err);
929
- results.push({ file, matches: [], error: String(err?.message ?? err) });
414
+ catch (error) {
415
+ // Fallback to system temp
416
+ return os__namespace.tmpdir();
930
417
  }
418
+ return secureTempPath;
931
419
  }
932
- return results;
933
- }
934
-
935
- const SIG_CEN = 0x02014b50;
936
- const DEFAULTS = {
937
- maxEntries: 1000,
938
- maxTotalUncompressedBytes: 500 * 1024 * 1024,
939
- maxEntryNameLength: 255,
940
- maxCompressionRatio: 1000,
941
- eocdSearchWindow: 70000,
942
- };
943
- function r16(buf, off) {
944
- return buf.readUInt16LE(off);
945
- }
946
- function r32(buf, off) {
947
- return buf.readUInt32LE(off);
948
- }
949
- function isZipLike(buf) {
950
- // local file header at start is common
951
- return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
952
- }
953
- function lastIndexOfEOCD(buf, window) {
954
- const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
955
- const start = Math.max(0, buf.length - window);
956
- const idx = buf.lastIndexOf(sig, Math.min(buf.length - sig.length, buf.length - 1));
957
- return idx >= start ? idx : -1;
958
- }
959
- function hasTraversal(name) {
960
- return name.includes('../') || name.includes('..\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name);
961
- }
962
- function createZipBombGuard(opts = {}) {
963
- const cfg = { ...DEFAULTS, ...opts };
964
- return {
965
- async scan(input) {
966
- const buf = Buffer.from(input);
967
- const matches = [];
968
- if (!isZipLike(buf))
969
- return matches;
970
- // Find EOCD near the end
971
- const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
972
- if (eocdPos < 0 || eocdPos + 22 > buf.length) {
973
- // ZIP but no EOCD — malformed or polyglot → suspicious
974
- matches.push({ rule: 'zip_eocd_not_found', severity: 'medium' });
975
- return matches;
420
+ /**
421
+ * Secure file cleanup with multiple overwrite passes
422
+ */
423
+ async secureFileCleanup(filePath) {
424
+ if (!this.config.enabled) {
425
+ try {
426
+ const fs = await import('fs/promises');
427
+ await fs.unlink(filePath);
976
428
  }
977
- const totalEntries = r16(buf, eocdPos + 10);
978
- const cdSize = r32(buf, eocdPos + 12);
979
- const cdOffset = r32(buf, eocdPos + 16);
980
- // Bounds check
981
- if (cdOffset + cdSize > buf.length) {
982
- matches.push({ rule: 'zip_cd_out_of_bounds', severity: 'medium' });
983
- return matches;
429
+ catch {
430
+ // Ignore cleanup errors
984
431
  }
985
- // Iterate central directory entries
986
- let ptr = cdOffset;
987
- let seen = 0;
988
- let sumComp = 0;
989
- let sumUnc = 0;
990
- while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
991
- const sig = r32(buf, ptr);
992
- if (sig !== SIG_CEN)
993
- break; // stop if structure breaks
994
- const compSize = r32(buf, ptr + 20);
995
- const uncSize = r32(buf, ptr + 24);
996
- const fnLen = r16(buf, ptr + 28);
997
- const exLen = r16(buf, ptr + 30);
998
- const cmLen = r16(buf, ptr + 32);
999
- const nameStart = ptr + 46;
1000
- const nameEnd = nameStart + fnLen;
1001
- if (nameEnd > buf.length)
1002
- break;
1003
- const name = buf.toString('utf8', nameStart, nameEnd);
1004
- sumComp += compSize;
1005
- sumUnc += uncSize;
1006
- seen++;
1007
- if (name.length > cfg.maxEntryNameLength) {
1008
- matches.push({ rule: 'zip_entry_name_too_long', severity: 'medium', meta: { name, length: name.length } });
1009
- }
1010
- if (hasTraversal(name)) {
1011
- matches.push({ rule: 'zip_path_traversal_entry', severity: 'medium', meta: { name } });
432
+ return;
433
+ }
434
+ try {
435
+ const fs = await import('fs/promises');
436
+ const stats = await fs.stat(filePath);
437
+ if (this.config.memoryProtection) {
438
+ // Overwrite file with random data multiple times (DoD 5220.22-M standard)
439
+ const fileSize = stats.size;
440
+ const buffer = crypto__namespace.randomBytes(Math.min(fileSize, 64 * 1024)); // 64KB chunks
441
+ for (let pass = 0; pass < 3; pass++) {
442
+ const handle = await fs.open(filePath, "r+");
443
+ try {
444
+ for (let offset = 0; offset < fileSize; offset += buffer.length) {
445
+ const chunk = offset + buffer.length > fileSize ? buffer.subarray(0, fileSize - offset) : buffer;
446
+ await handle.write(chunk, 0, chunk.length, offset);
447
+ }
448
+ await handle.sync();
449
+ }
450
+ finally {
451
+ await handle.close();
452
+ }
1012
453
  }
1013
- // move to next entry
1014
- ptr = nameEnd + exLen + cmLen;
1015
- }
1016
- if (seen !== totalEntries) {
1017
- // central dir truncated/odd, still report what we found
1018
- matches.push({ rule: 'zip_cd_truncated', severity: 'medium', meta: { seen, totalEntries } });
1019
- }
1020
- // Heuristics thresholds
1021
- if (seen > cfg.maxEntries) {
1022
- matches.push({ rule: 'zip_too_many_entries', severity: 'medium', meta: { seen, limit: cfg.maxEntries } });
1023
454
  }
1024
- if (sumUnc > cfg.maxTotalUncompressedBytes) {
1025
- matches.push({
1026
- rule: 'zip_total_uncompressed_too_large',
1027
- severity: 'medium',
1028
- meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
455
+ // Final deletion
456
+ await fs.unlink(filePath);
457
+ this.auditLog("temp_file_deleted", {
458
+ action: "secure_delete",
459
+ success: true,
460
+ metadata: {
461
+ path: this.sanitizeFilename(filePath),
462
+ overwritePasses: this.config.memoryProtection ? 3 : 0,
463
+ },
464
+ });
465
+ }
466
+ catch (error) {
467
+ this.auditLog("temp_file_deleted", {
468
+ action: "secure_delete",
469
+ success: false,
470
+ sanitizedError: this.sanitizeError(error),
471
+ metadata: { path: this.sanitizeFilename(filePath) },
472
+ });
473
+ }
474
+ }
475
+ /**
476
+ * Calculate secure file hash for audit purposes
477
+ */
478
+ calculateFileHash(data) {
479
+ return crypto__namespace.createHash("sha256").update(data).digest("hex");
480
+ }
481
+ /**
482
+ * Log audit event
483
+ */
484
+ auditLog(eventType, details) {
485
+ if (!this.config.enabled)
486
+ return;
487
+ const event = {
488
+ timestamp: new Date().toISOString(),
489
+ eventType,
490
+ sessionId: this.sessionId,
491
+ details: {
492
+ action: details.action || "unknown",
493
+ success: details.success ?? true,
494
+ ...details,
495
+ },
496
+ };
497
+ this.auditEvents.push(event);
498
+ // Write to audit log file if configured
499
+ if (this.config.auditLogPath) {
500
+ this.writeAuditLog(event).catch(() => {
501
+ // Silent failure to prevent error loops
502
+ });
503
+ }
504
+ }
505
+ /**
506
+ * Write audit event to file
507
+ */
508
+ async writeAuditLog(event) {
509
+ if (!this.config.auditLogPath)
510
+ return;
511
+ try {
512
+ const fs = await import('fs/promises');
513
+ const logLine = JSON.stringify(event) + "\\n";
514
+ await fs.appendFile(this.config.auditLogPath, logLine, { flag: "a" });
515
+ }
516
+ catch {
517
+ // Silent failure
518
+ }
519
+ }
520
+ /**
521
+ * Generate cryptographically secure session ID
522
+ */
523
+ generateSessionId() {
524
+ return crypto__namespace.randomBytes(16).toString("hex");
525
+ }
526
+ /**
527
+ * Get current audit events for this session
528
+ */
529
+ getAuditEvents() {
530
+ return [...this.auditEvents];
531
+ }
532
+ /**
533
+ * Clear sensitive data from memory
534
+ */
535
+ clearSensitiveData() {
536
+ if (!this.config.enabled || !this.config.memoryProtection)
537
+ return;
538
+ // Clear audit events
539
+ this.auditEvents.length = 0;
540
+ // Force garbage collection if available
541
+ if (global.gc) {
542
+ global.gc();
543
+ }
544
+ }
545
+ /**
546
+ * Validate transport security
547
+ */
548
+ validateTransportSecurity(url) {
549
+ if (!this.config.enabled || !this.config.requireSecureTransport) {
550
+ return true;
551
+ }
552
+ if (!url)
553
+ return true;
554
+ try {
555
+ const urlObj = new URL(url);
556
+ const isSecure = urlObj.protocol === "https:" ||
557
+ urlObj.hostname === "localhost" ||
558
+ urlObj.hostname === "127.0.0.1";
559
+ if (!isSecure) {
560
+ this.auditLog("security_violation", {
561
+ action: "insecure_transport",
562
+ success: false,
563
+ metadata: { protocol: urlObj.protocol, hostname: urlObj.hostname },
1029
564
  });
1030
565
  }
1031
- if (sumComp === 0 && sumUnc > 0) {
1032
- matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio: Infinity } });
566
+ return isSecure;
567
+ }
568
+ catch {
569
+ return false;
570
+ }
571
+ }
572
+ }
573
+ // Global HIPAA compliance instance
574
+ let hipaaManager = null;
575
+ /**
576
+ * Initialize HIPAA compliance
577
+ */
578
+ function initializeHipaaCompliance(config) {
579
+ hipaaManager = new HipaaComplianceManager(config);
580
+ return hipaaManager;
581
+ }
582
+ /**
583
+ * Get current HIPAA compliance manager
584
+ */
585
+ function getHipaaManager() {
586
+ return hipaaManager;
587
+ }
588
+ /**
589
+ * HIPAA-compliant error wrapper
590
+ */
591
+ function createHipaaError(error, context) {
592
+ const manager = getHipaaManager();
593
+ if (!manager) {
594
+ return typeof error === "string" ? new Error(error) : error;
595
+ }
596
+ const sanitizedMessage = manager.sanitizeError(error);
597
+ const hipaaError = new Error(sanitizedMessage);
598
+ manager.auditLog("error_occurred", {
599
+ action: context || "error",
600
+ success: false,
601
+ sanitizedError: sanitizedMessage,
602
+ });
603
+ return hipaaError;
604
+ }
605
+ /**
606
+ * HIPAA-compliant temporary file utilities
607
+ */
608
+ const HipaaTemp = {
609
+ createPath: (prefix) => {
610
+ const manager = getHipaaManager();
611
+ return manager
612
+ ? manager.createSecureTempPath(prefix)
613
+ : path__namespace.join(os__namespace.tmpdir(), `${prefix || "pompelmi"}-${Date.now()}`);
614
+ },
615
+ cleanup: async (filePath) => {
616
+ const manager = getHipaaManager();
617
+ if (manager) {
618
+ await manager.secureFileCleanup(filePath);
619
+ }
620
+ else {
621
+ try {
622
+ const fs = await import('fs/promises');
623
+ await fs.unlink(filePath);
1033
624
  }
1034
- else if (sumComp > 0) {
1035
- const ratio = sumUnc / Math.max(1, sumComp);
1036
- if (ratio >= cfg.maxCompressionRatio) {
1037
- matches.push({ rule: 'zip_suspicious_ratio', severity: 'medium', meta: { ratio, limit: cfg.maxCompressionRatio } });
1038
- }
625
+ catch {
626
+ // Ignore errors
1039
627
  }
1040
- return matches;
1041
628
  }
1042
- };
1043
- }
629
+ },
630
+ };
1044
631
 
1045
632
  const MB$1 = 1024 * 1024;
1046
633
  const DEFAULT_POLICY = {
1047
- includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf'],
1048
- allowedMimeTypes: ['application/zip', 'image/png', 'image/jpeg', 'application/pdf', 'text/plain'],
634
+ includeExtensions: ["zip", "png", "jpg", "jpeg", "pdf"],
635
+ allowedMimeTypes: ["application/zip", "image/png", "image/jpeg", "application/pdf", "text/plain"],
1049
636
  maxFileSizeBytes: 20 * MB$1,
1050
637
  timeoutMs: 5000,
1051
638
  concurrency: 4,
1052
- failClosed: true
639
+ failClosed: true,
1053
640
  };
1054
641
  function definePolicy(input = {}) {
1055
642
  const p = { ...DEFAULT_POLICY, ...input };
1056
643
  if (!Array.isArray(p.includeExtensions))
1057
- throw new TypeError('includeExtensions must be string[]');
644
+ throw new TypeError("includeExtensions must be string[]");
1058
645
  if (!Array.isArray(p.allowedMimeTypes))
1059
- throw new TypeError('allowedMimeTypes must be string[]');
646
+ throw new TypeError("allowedMimeTypes must be string[]");
1060
647
  if (!(Number.isFinite(p.maxFileSizeBytes) && p.maxFileSizeBytes > 0))
1061
- throw new TypeError('maxFileSizeBytes must be > 0');
648
+ throw new TypeError("maxFileSizeBytes must be > 0");
1062
649
  if (!(Number.isFinite(p.timeoutMs) && p.timeoutMs > 0))
1063
- throw new TypeError('timeoutMs must be > 0');
650
+ throw new TypeError("timeoutMs must be > 0");
1064
651
  if (!(Number.isInteger(p.concurrency) && p.concurrency > 0))
1065
- throw new TypeError('concurrency must be > 0');
652
+ throw new TypeError("concurrency must be > 0");
1066
653
  return p;
1067
654
  }
1068
655
 
@@ -1106,33 +693,39 @@ const MB = 1024 * KB;
1106
693
  */
1107
694
  const DOCUMENTS_ONLY = definePolicy({
1108
695
  includeExtensions: [
1109
- 'pdf',
1110
- 'doc', 'docx',
1111
- 'xls', 'xlsx',
1112
- 'ppt', 'pptx',
1113
- 'odt', 'ods', 'odp',
1114
- 'csv',
1115
- 'txt',
1116
- 'json',
1117
- 'yaml', 'yml',
1118
- 'md',
696
+ "pdf",
697
+ "doc",
698
+ "docx",
699
+ "xls",
700
+ "xlsx",
701
+ "ppt",
702
+ "pptx",
703
+ "odt",
704
+ "ods",
705
+ "odp",
706
+ "csv",
707
+ "txt",
708
+ "json",
709
+ "yaml",
710
+ "yml",
711
+ "md",
1119
712
  ],
1120
713
  allowedMimeTypes: [
1121
- 'application/pdf',
1122
- 'application/msword',
1123
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1124
- 'application/vnd.ms-excel',
1125
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1126
- 'application/vnd.ms-powerpoint',
1127
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1128
- 'application/vnd.oasis.opendocument.text',
1129
- 'application/vnd.oasis.opendocument.spreadsheet',
1130
- 'application/vnd.oasis.opendocument.presentation',
1131
- 'text/csv',
1132
- 'text/plain',
1133
- 'application/json',
1134
- 'text/yaml',
1135
- 'text/markdown',
714
+ "application/pdf",
715
+ "application/msword",
716
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
717
+ "application/vnd.ms-excel",
718
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
719
+ "application/vnd.ms-powerpoint",
720
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
721
+ "application/vnd.oasis.opendocument.text",
722
+ "application/vnd.oasis.opendocument.spreadsheet",
723
+ "application/vnd.oasis.opendocument.presentation",
724
+ "text/csv",
725
+ "text/plain",
726
+ "application/json",
727
+ "text/yaml",
728
+ "text/markdown",
1136
729
  ],
1137
730
  maxFileSizeBytes: 25 * MB,
1138
731
  timeoutMs: 10000,
@@ -1150,17 +743,17 @@ const DOCUMENTS_ONLY = definePolicy({
1150
743
  * Note: SVG is intentionally excluded — inline SVGs can contain scripts.
1151
744
  */
1152
745
  const IMAGES_ONLY = definePolicy({
1153
- includeExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'tiff', 'tif', 'bmp', 'ico'],
746
+ includeExtensions: ["jpg", "jpeg", "png", "gif", "webp", "avif", "tiff", "tif", "bmp", "ico"],
1154
747
  allowedMimeTypes: [
1155
- 'image/jpeg',
1156
- 'image/png',
1157
- 'image/gif',
1158
- 'image/webp',
1159
- 'image/avif',
1160
- 'image/tiff',
1161
- 'image/bmp',
1162
- 'image/x-icon',
1163
- 'image/vnd.microsoft.icon',
748
+ "image/jpeg",
749
+ "image/png",
750
+ "image/gif",
751
+ "image/webp",
752
+ "image/avif",
753
+ "image/tiff",
754
+ "image/bmp",
755
+ "image/x-icon",
756
+ "image/vnd.microsoft.icon",
1164
757
  ],
1165
758
  maxFileSizeBytes: 10 * MB,
1166
759
  timeoutMs: 5000,
@@ -1177,13 +770,8 @@ const IMAGES_ONLY = definePolicy({
1177
770
  * allowlist. Only allows plain images and PDF.
1178
771
  */
1179
772
  const STRICT_PUBLIC_UPLOAD = definePolicy({
1180
- includeExtensions: ['jpg', 'jpeg', 'png', 'webp', 'pdf'],
1181
- allowedMimeTypes: [
1182
- 'image/jpeg',
1183
- 'image/png',
1184
- 'image/webp',
1185
- 'application/pdf',
1186
- ],
773
+ includeExtensions: ["jpg", "jpeg", "png", "webp", "pdf"],
774
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
1187
775
  maxFileSizeBytes: 5 * MB,
1188
776
  timeoutMs: 4000,
1189
777
  concurrency: 2,
@@ -1197,16 +785,16 @@ const STRICT_PUBLIC_UPLOAD = definePolicy({
1197
785
  * shorter timeout than the permissive default.
1198
786
  */
1199
787
  const CONSERVATIVE_DEFAULT = definePolicy({
1200
- includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf', 'txt', 'csv', 'docx', 'xlsx'],
788
+ includeExtensions: ["zip", "png", "jpg", "jpeg", "pdf", "txt", "csv", "docx", "xlsx"],
1201
789
  allowedMimeTypes: [
1202
- 'application/zip',
1203
- 'image/png',
1204
- 'image/jpeg',
1205
- 'application/pdf',
1206
- 'text/plain',
1207
- 'text/csv',
1208
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1209
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
790
+ "application/zip",
791
+ "image/png",
792
+ "image/jpeg",
793
+ "application/pdf",
794
+ "text/plain",
795
+ "text/csv",
796
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
797
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1210
798
  ],
1211
799
  maxFileSizeBytes: 10 * MB,
1212
800
  timeoutMs: 8000,
@@ -1230,15 +818,15 @@ const CONSERVATIVE_DEFAULT = definePolicy({
1230
818
  * ```
1231
819
  */
1232
820
  const ARCHIVES = definePolicy({
1233
- includeExtensions: ['zip', 'tar', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar'],
821
+ includeExtensions: ["zip", "tar", "gz", "tgz", "bz2", "xz", "7z", "rar"],
1234
822
  allowedMimeTypes: [
1235
- 'application/zip',
1236
- 'application/x-tar',
1237
- 'application/gzip',
1238
- 'application/x-bzip2',
1239
- 'application/x-xz',
1240
- 'application/x-7z-compressed',
1241
- 'application/x-rar-compressed',
823
+ "application/zip",
824
+ "application/x-tar",
825
+ "application/gzip",
826
+ "application/x-bzip2",
827
+ "application/x-xz",
828
+ "application/x-7z-compressed",
829
+ "application/x-rar-compressed",
1242
830
  ],
1243
831
  maxFileSizeBytes: 100 * MB,
1244
832
  timeoutMs: 30000,
@@ -1254,11 +842,11 @@ const ARCHIVES = definePolicy({
1254
842
  * ```
1255
843
  */
1256
844
  const POLICY_PACKS = {
1257
- 'documents-only': DOCUMENTS_ONLY,
1258
- 'images-only': IMAGES_ONLY,
1259
- 'strict-public-upload': STRICT_PUBLIC_UPLOAD,
1260
- 'conservative-default': CONSERVATIVE_DEFAULT,
1261
- 'archives': ARCHIVES,
845
+ "documents-only": DOCUMENTS_ONLY,
846
+ "images-only": IMAGES_ONLY,
847
+ "strict-public-upload": STRICT_PUBLIC_UPLOAD,
848
+ "conservative-default": CONSERVATIVE_DEFAULT,
849
+ archives: ARCHIVES,
1262
850
  };
1263
851
  /**
1264
852
  * Look up a policy pack by name.
@@ -1267,1184 +855,1646 @@ const POLICY_PACKS = {
1267
855
  function getPolicyPack(name) {
1268
856
  const policy = POLICY_PACKS[name];
1269
857
  if (!policy)
1270
- throw new Error(`Unknown policy pack: '${name}'. Valid names: ${Object.keys(POLICY_PACKS).join(', ')}`);
858
+ throw new Error(`Unknown policy pack: '${name}'. Valid names: ${Object.keys(POLICY_PACKS).join(", ")}`);
1271
859
  return policy;
1272
860
  }
1273
861
 
1274
- function mapMatchesToVerdict(matches = []) {
1275
- if (!matches.length)
1276
- return 'clean';
1277
- const malHints = ['trojan', 'ransom', 'worm', 'spy', 'rootkit', 'keylog', 'botnet'];
1278
- const tagSet = new Set(matches.flatMap(m => (m.tags ?? []).map(t => t.toLowerCase())));
1279
- const nameHit = (r) => malHints.some(h => r.toLowerCase().includes(h));
1280
- const isMal = matches.some(m => nameHit(m.rule)) || tagSet.has('malware') || tagSet.has('critical');
1281
- return isMal ? 'malicious' : 'suspicious';
862
+ function hasAsciiToken(buf, token) {
863
+ // Use latin1 so we can safely search binary
864
+ return buf.indexOf(token, 0, "latin1") !== -1;
1282
865
  }
1283
-
1284
- /** Decompilation-specific types for Pompelmi */
1285
- const SUSPICIOUS_PATTERNS = [
1286
- {
1287
- name: 'syscall_direct',
1288
- description: 'Direct system call without library wrapper',
1289
- severity: 'medium',
1290
- pattern: /syscall|sysenter|int\s+0x80/i
1291
- },
1292
- {
1293
- name: 'process_injection',
1294
- description: 'Process injection techniques',
1295
- severity: 'high',
1296
- pattern: /CreateRemoteThread|WriteProcessMemory|VirtualAllocEx/i
1297
- },
1298
- {
1299
- name: 'anti_debug',
1300
- description: 'Anti-debugging techniques',
1301
- severity: 'medium',
1302
- pattern: /IsDebuggerPresent|CheckRemoteDebuggerPresent|OutputDebugString/i
1303
- },
1304
- {
1305
- name: 'obfuscation_xor',
1306
- description: 'XOR-based obfuscation pattern',
1307
- severity: 'medium',
1308
- pattern: /xor.*0x[0-9a-f]+.*xor/i
866
+ function startsWith(buf, bytes) {
867
+ if (buf.length < bytes.length)
868
+ return false;
869
+ for (let i = 0; i < bytes.length; i++)
870
+ if (buf[i] !== bytes[i])
871
+ return false;
872
+ return true;
873
+ }
874
+ function isPDF(buf) {
875
+ // %PDF-
876
+ return startsWith(buf, [0x25, 0x50, 0x44, 0x46, 0x2d]);
877
+ }
878
+ function isOleCfb(buf) {
879
+ // D0 CF 11 E0 A1 B1 1A E1
880
+ const sig = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
881
+ return startsWith(buf, sig);
882
+ }
883
+ function isZipLike$1(buf) {
884
+ // PK\x03\x04
885
+ return startsWith(buf, [0x50, 0x4b, 0x03, 0x04]);
886
+ }
887
+ function isPeExecutable(buf) {
888
+ // "MZ"
889
+ return startsWith(buf, [0x4d, 0x5a]);
890
+ }
891
+ /** OOXML macro hint via filename token in ZIP container */
892
+ function hasOoxmlMacros(buf) {
893
+ if (!isZipLike$1(buf))
894
+ return false;
895
+ return hasAsciiToken(buf, "vbaProject.bin");
896
+ }
897
+ /** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
898
+ function pdfRiskTokens(buf) {
899
+ const tokens = ["/JavaScript", "/OpenAction", "/AA", "/Launch"];
900
+ return tokens.filter((t) => hasAsciiToken(buf, t));
901
+ }
902
+ const CommonHeuristicsScanner = {
903
+ async scan(input) {
904
+ const buf = Buffer.from(input);
905
+ const matches = [];
906
+ // Office macros (OLE / OOXML)
907
+ if (isOleCfb(buf)) {
908
+ matches.push({ rule: "office_ole_container", severity: "suspicious" });
909
+ }
910
+ if (hasOoxmlMacros(buf)) {
911
+ matches.push({ rule: "office_ooxml_macros", severity: "suspicious" });
912
+ }
913
+ // PDF risky tokens
914
+ if (isPDF(buf)) {
915
+ const toks = pdfRiskTokens(buf);
916
+ if (toks.length) {
917
+ matches.push({
918
+ rule: "pdf_risky_actions",
919
+ severity: "suspicious",
920
+ meta: { tokens: toks },
921
+ });
922
+ }
923
+ }
924
+ // Executable header
925
+ if (isPeExecutable(buf)) {
926
+ matches.push({ rule: "pe_executable_signature", severity: "suspicious" });
927
+ }
928
+ // EICAR test file
929
+ const EICAR_NEEDLE = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!";
930
+ if (hasAsciiToken(buf, EICAR_NEEDLE)) {
931
+ matches.push({
932
+ rule: "eicar_test_file",
933
+ severity: "high",
934
+ meta: { note: "EICAR standard antivirus test file detected" },
935
+ });
936
+ }
937
+ return matches;
1309
938
  },
1310
- {
1311
- name: 'crypto_constants',
1312
- description: 'Cryptographic constants',
1313
- severity: 'low',
1314
- pattern: /0x67452301|0xefcdab89|0x98badcfe|0x10325476/i
1315
- }
1316
- ];
939
+ };
1317
940
 
1318
- /**
1319
- * Batch scanning with concurrency control
1320
- * @module utils/batch-scanner
1321
- */
1322
- /**
1323
- * Batch file scanner with concurrency control and progress tracking
1324
- */
1325
- class BatchScanner {
1326
- constructor(options = {}) {
1327
- this.options = {
1328
- concurrency: 5,
1329
- continueOnError: true,
1330
- ...options,
941
+ function toScanFn(s) {
942
+ return (typeof s === "function" ? s : s.scan);
943
+ }
944
+ /** Map a Match's severity field to a Verdict for stopOn comparison. */
945
+ function matchToVerdict(m) {
946
+ const s = m.severity;
947
+ if (s === "critical" || s === "high" || s === "malicious")
948
+ return "malicious";
949
+ if (s === "medium" || s === "low" || s === "suspicious" || s === "info")
950
+ return "suspicious";
951
+ return "clean";
952
+ }
953
+ /** Highest verdict across all matches in the list. */
954
+ function highestSeverity(matches) {
955
+ if (matches.length === 0)
956
+ return null;
957
+ if (matches.some((m) => matchToVerdict(m) === "malicious"))
958
+ return "malicious";
959
+ if (matches.some((m) => matchToVerdict(m) === "suspicious"))
960
+ return "suspicious";
961
+ return "clean";
962
+ }
963
+ const SEVERITY_RANK = { malicious: 2, suspicious: 1, clean: 0 };
964
+ function shouldStop(matches, stopOn) {
965
+ if (!stopOn)
966
+ return false;
967
+ const highest = highestSeverity(matches);
968
+ if (!highest)
969
+ return false;
970
+ return SEVERITY_RANK[highest] >= SEVERITY_RANK[stopOn];
971
+ }
972
+ async function runWithTimeout(fn, timeoutMs) {
973
+ if (!timeoutMs)
974
+ return fn();
975
+ return new Promise((resolve, reject) => {
976
+ const timer = setTimeout(() => reject(new Error("scanner timeout")), timeoutMs);
977
+ fn().then((v) => {
978
+ clearTimeout(timer);
979
+ resolve(v);
980
+ }, (e) => {
981
+ clearTimeout(timer);
982
+ reject(e);
983
+ });
984
+ });
985
+ }
986
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
987
+ function composeScanners(...args) {
988
+ const first = args[0];
989
+ const rest = args.slice(1);
990
+ // ── Named-scanner array form ──────────────────────────────────────────────
991
+ if (Array.isArray(first) &&
992
+ (first.length === 0 || (Array.isArray(first[0]) && typeof first[0][0] === "string"))) {
993
+ const entries = first;
994
+ const opts = rest.length > 0 &&
995
+ !Array.isArray(rest[0]) &&
996
+ typeof rest[0] !== "function" &&
997
+ !(typeof rest[0] === "object" && rest[0] !== null && "scan" in rest[0])
998
+ ? rest[0]
999
+ : {};
1000
+ return async (input, ctx) => {
1001
+ const all = [];
1002
+ if (opts.parallel) {
1003
+ // Parallel execution — collect all results then return
1004
+ const results = await Promise.allSettled(entries.map(([name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
1005
+ for (let i = 0; i < results.length; i++) {
1006
+ const result = results[i];
1007
+ if (result.status === "fulfilled" && Array.isArray(result.value)) {
1008
+ const matches = opts.tagSourceName
1009
+ ? result.value.map((m) => ({
1010
+ ...m,
1011
+ meta: { ...m.meta, _sourceName: entries[i][0] },
1012
+ }))
1013
+ : result.value;
1014
+ all.push(...matches);
1015
+ }
1016
+ }
1017
+ }
1018
+ else {
1019
+ // Sequential execution with optional stopOn short-circuit
1020
+ for (const [name, scanner] of entries) {
1021
+ try {
1022
+ const out = await runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner);
1023
+ if (Array.isArray(out)) {
1024
+ const matches = opts.tagSourceName
1025
+ ? out.map((m) => ({ ...m, meta: { ...m.meta, _sourceName: name } }))
1026
+ : out;
1027
+ all.push(...matches);
1028
+ if (shouldStop(all, opts.stopOn))
1029
+ break;
1030
+ }
1031
+ }
1032
+ catch {
1033
+ // individual scanner failure is non-fatal
1034
+ }
1035
+ }
1036
+ }
1037
+ return all;
1331
1038
  };
1332
1039
  }
1333
- /**
1334
- * Scan multiple files with controlled concurrency
1335
- */
1336
- async scanBatch(tasks) {
1337
- const startTime = Date.now();
1338
- const results = new Array(tasks.length);
1339
- const errors = [];
1340
- let successCount = 0;
1341
- let errorCount = 0;
1342
- let completedCount = 0;
1343
- const concurrency = this.options.concurrency ?? 5;
1344
- // Process tasks in chunks with controlled concurrency
1345
- const processingQueue = [];
1346
- let currentIndex = 0;
1347
- const processTask = async (index) => {
1040
+ // ── Variadic form (backward-compatible) ───────────────────────────────────
1041
+ const scanners = [first, ...rest].filter(Boolean);
1042
+ return async (input, ctx) => {
1043
+ const all = [];
1044
+ for (const s of scanners) {
1045
+ try {
1046
+ const out = await toScanFn(s)(input, ctx);
1047
+ if (Array.isArray(out))
1048
+ all.push(...out);
1049
+ }
1050
+ catch {
1051
+ // ignore individual scanner failures
1052
+ }
1053
+ }
1054
+ return all;
1055
+ };
1056
+ }
1057
+ function createPresetScanner(preset, opts = {}) {
1058
+ const scanners = [];
1059
+ // Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
1060
+ scanners.push(CommonHeuristicsScanner);
1061
+ // Add decompilation scanners based on preset
1062
+ if (preset === "decompilation-basic" ||
1063
+ preset === "decompilation-deep" ||
1064
+ preset === "malware-analysis" ||
1065
+ opts.enableDecompilation) {
1066
+ const depth = preset === "decompilation-deep"
1067
+ ? "deep"
1068
+ : preset === "decompilation-basic"
1069
+ ? "basic"
1070
+ : opts.decompilationDepth || "basic";
1071
+ if (!opts.decompilationEngine ||
1072
+ opts.decompilationEngine === "binaryninja-hlil" ||
1073
+ opts.decompilationEngine === "both") {
1348
1074
  try {
1349
- const task = tasks[index];
1350
- const report = await scanBytes(task.content, {
1351
- ...this.options,
1352
- ctx: task.context,
1075
+ // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
1076
+ const importModule = new Function("specifier", "return import(specifier)");
1077
+ importModule("@pompelmi/engine-binaryninja")
1078
+ .then((mod) => {
1079
+ const binjaScanner = mod.createBinaryNinjaScanner({
1080
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
1081
+ depth,
1082
+ pythonPath: opts.pythonPath,
1083
+ binaryNinjaPath: opts.binaryNinjaPath,
1084
+ });
1085
+ scanners.push(binjaScanner);
1086
+ })
1087
+ .catch(() => {
1088
+ // Binary Ninja engine not available - silently skip
1353
1089
  });
1354
- results[index] = report;
1355
- successCount++;
1356
- completedCount++;
1357
- if (this.options.onProgress) {
1358
- this.options.onProgress(completedCount, tasks.length, report);
1359
- }
1360
1090
  }
1361
- catch (error) {
1362
- errorCount++;
1363
- completedCount++;
1364
- const err = error instanceof Error ? error : new Error(String(error));
1365
- if (this.options.onError) {
1366
- this.options.onError(err, index);
1367
- }
1368
- errors.push({ index, error: err });
1369
- if (!this.options.continueOnError) {
1370
- throw err;
1371
- }
1372
- results[index] = null;
1091
+ catch {
1092
+ // Engine not installed
1373
1093
  }
1374
- };
1375
- // Start initial batch of concurrent tasks
1376
- while (currentIndex < tasks.length) {
1377
- while (processingQueue.length < concurrency && currentIndex < tasks.length) {
1378
- const promise = processTask(currentIndex);
1379
- processingQueue.push(promise);
1380
- currentIndex++;
1381
- // Remove completed promises from queue
1382
- promise.finally(() => {
1383
- const idx = processingQueue.indexOf(promise);
1384
- if (idx > -1)
1385
- processingQueue.splice(idx, 1);
1094
+ }
1095
+ if (!opts.decompilationEngine ||
1096
+ opts.decompilationEngine === "ghidra-pcode" ||
1097
+ opts.decompilationEngine === "both") {
1098
+ try {
1099
+ // Dynamic import for Ghidra engine (when implemented) - using Function to bypass TypeScript type checking
1100
+ const importModule = new Function("specifier", "return import(specifier)");
1101
+ importModule("@pompelmi/engine-ghidra")
1102
+ .then((mod) => {
1103
+ const ghidraScanner = mod.createGhidraScanner({
1104
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
1105
+ depth,
1106
+ ghidraPath: opts.ghidraPath,
1107
+ analyzeHeadless: opts.analyzeHeadless,
1108
+ });
1109
+ scanners.push(ghidraScanner);
1110
+ })
1111
+ .catch(() => {
1112
+ // Ghidra engine not available - silently skip
1386
1113
  });
1387
1114
  }
1388
- // Wait for at least one task to complete before continuing
1389
- if (processingQueue.length >= concurrency) {
1390
- await Promise.race(processingQueue);
1115
+ catch {
1116
+ // Engine not installed
1391
1117
  }
1392
1118
  }
1393
- // Wait for all remaining tasks
1394
- await Promise.all(processingQueue);
1395
- const totalDurationMs = Date.now() - startTime;
1396
- return {
1397
- reports: results,
1398
- successCount,
1399
- errorCount,
1400
- totalDurationMs,
1401
- errors,
1119
+ }
1120
+ if (scanners.length === 0) {
1121
+ // Fallback scanner that returns no matches
1122
+ return async (_input, _ctx) => {
1123
+ return [];
1402
1124
  };
1403
1125
  }
1404
- /**
1405
- * Scan files from File objects (browser environment)
1406
- */
1407
- async scanFiles(files) {
1408
- const tasks = await Promise.all(files.map(async (file) => ({
1409
- content: new Uint8Array(await file.arrayBuffer()),
1410
- context: {
1411
- filename: file.name,
1412
- mimeType: file.type,
1413
- size: file.size,
1414
- },
1415
- })));
1416
- return this.scanBatch(tasks);
1126
+ return composeScanners(...scanners);
1127
+ }
1128
+
1129
+ /**
1130
+ * Advanced threat detection utilities
1131
+ * @module utils/advanced-detection
1132
+ */
1133
+ /**
1134
+ * Enhanced polyglot file detection
1135
+ * Detects files that can be interpreted as multiple formats
1136
+ */
1137
+ function detectPolyglot(bytes) {
1138
+ const matches = [];
1139
+ // Check for PDF/ZIP polyglot
1140
+ if (isPDFZipPolyglot(bytes)) {
1141
+ matches.push({
1142
+ rule: "polyglot_pdf_zip",
1143
+ severity: "high",
1144
+ meta: { description: "File can be interpreted as both PDF and ZIP" },
1145
+ });
1417
1146
  }
1418
- /**
1419
- * Scan files from file paths (Node.js environment)
1420
- */
1421
- async scanFilePaths(filePaths) {
1422
- const fs = await import('fs/promises');
1423
- const path = await import('path');
1424
- const tasks = await Promise.all(filePaths.map(async (filePath) => {
1425
- const [content, stats] = await Promise.all([
1426
- fs.readFile(filePath),
1427
- fs.stat(filePath),
1428
- ]);
1429
- return {
1430
- content: new Uint8Array(content),
1431
- context: {
1432
- filename: path.basename(filePath),
1433
- size: stats.size,
1147
+ // Check for image/script polyglot
1148
+ if (isImageScriptPolyglot(bytes)) {
1149
+ matches.push({
1150
+ rule: "polyglot_image_script",
1151
+ severity: "high",
1152
+ meta: { description: "Image file contains executable script content" },
1153
+ });
1154
+ }
1155
+ // Check for GIFAR (GIF/JAR polyglot)
1156
+ if (isGIFAR(bytes)) {
1157
+ matches.push({
1158
+ rule: "polyglot_gifar",
1159
+ severity: "critical",
1160
+ meta: { description: "GIF file contains Java archive" },
1161
+ });
1162
+ }
1163
+ return matches;
1164
+ }
1165
+ /**
1166
+ * Detect obfuscated JavaScript/VBScript
1167
+ */
1168
+ function detectObfuscatedScripts(bytes) {
1169
+ const matches = [];
1170
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
1171
+ // Check for common obfuscation patterns
1172
+ const obfuscationPatterns = [
1173
+ /eval\s*\(\s*unescape\s*\(/gi,
1174
+ /eval\s*\(\s*atob\s*\(/gi,
1175
+ /String\.fromCharCode\s*\(\s*\d+(?:\s*,\s*\d+){10,}/gi,
1176
+ /[a-z0-9]{100,}/gi, // Long encoded strings
1177
+ /\\x[0-9a-f]{2}/gi, // Hex escapes
1178
+ ];
1179
+ for (const pattern of obfuscationPatterns) {
1180
+ if (pattern.test(text)) {
1181
+ matches.push({
1182
+ rule: "obfuscated_script",
1183
+ severity: "medium",
1184
+ meta: {
1185
+ description: "Detected obfuscated script content",
1186
+ pattern: pattern.source,
1434
1187
  },
1435
- };
1436
- }));
1437
- return this.scanBatch(tasks);
1188
+ });
1189
+ break;
1190
+ }
1438
1191
  }
1192
+ return matches;
1439
1193
  }
1440
1194
  /**
1441
- * Quick helper for batch scanning with default options
1195
+ * Enhanced nested archive detection with depth limits
1442
1196
  */
1443
- async function batchScan(tasks, options) {
1444
- const scanner = new BatchScanner(options);
1445
- return scanner.scanBatch(tasks);
1197
+ function analyzeNestedArchives(bytes, maxDepth = 10) {
1198
+ let depth = 0;
1199
+ let currentBytes = bytes;
1200
+ while (depth < maxDepth) {
1201
+ if (isArchive(currentBytes)) {
1202
+ depth++;
1203
+ {
1204
+ break;
1205
+ }
1206
+ }
1207
+ else {
1208
+ break;
1209
+ }
1210
+ }
1211
+ return {
1212
+ depth,
1213
+ hasExcessiveNesting: depth >= 5,
1214
+ };
1215
+ }
1216
+ // Helper functions
1217
+ function isPDFZipPolyglot(bytes) {
1218
+ if (bytes.length < 8)
1219
+ return false;
1220
+ // Check for PDF signature
1221
+ const hasPDF = bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46;
1222
+ // Check for ZIP signature anywhere in the file
1223
+ let hasZIP = false;
1224
+ for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
1225
+ if (bytes[i] === 0x50 &&
1226
+ bytes[i + 1] === 0x4b &&
1227
+ bytes[i + 2] === 0x03 &&
1228
+ bytes[i + 3] === 0x04) {
1229
+ hasZIP = true;
1230
+ break;
1231
+ }
1232
+ }
1233
+ return hasPDF && hasZIP;
1234
+ }
1235
+ function isImageScriptPolyglot(bytes) {
1236
+ if (bytes.length < 100)
1237
+ return false;
1238
+ // Check for image signatures
1239
+ const isImage = (bytes[0] === 0xff && bytes[1] === 0xd8) || // JPEG
1240
+ (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) || // PNG
1241
+ (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46); // GIF
1242
+ if (!isImage)
1243
+ return false;
1244
+ // Check for script content
1245
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1246
+ return /<script|javascript:|eval\(|function\s*\(/i.test(text);
1247
+ }
1248
+ function isGIFAR(bytes) {
1249
+ if (bytes.length < 100)
1250
+ return false;
1251
+ // Check for GIF signature
1252
+ const isGIF = bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46;
1253
+ // Check for ZIP/JAR signature
1254
+ let hasZIP = false;
1255
+ for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
1256
+ if (bytes[i] === 0x50 &&
1257
+ bytes[i + 1] === 0x4b &&
1258
+ bytes[i + 2] === 0x03 &&
1259
+ bytes[i + 3] === 0x04) {
1260
+ hasZIP = true;
1261
+ break;
1262
+ }
1263
+ }
1264
+ return isGIF && hasZIP;
1265
+ }
1266
+ function isArchive(bytes) {
1267
+ if (bytes.length < 4)
1268
+ return false;
1269
+ return (
1270
+ // ZIP
1271
+ (bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) ||
1272
+ // RAR
1273
+ (bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
1274
+ // 7z
1275
+ (bytes[0] === 0x37 && bytes[1] === 0x7a && bytes[2] === 0xbc && bytes[3] === 0xaf) ||
1276
+ // tar.gz
1277
+ (bytes[0] === 0x1f && bytes[1] === 0x8b));
1446
1278
  }
1447
1279
 
1448
1280
  /**
1449
- * Threat intelligence integration and enhanced detection
1450
- * @module utils/threat-intelligence
1281
+ * Cache management system for scan results
1282
+ * @module utils/cache-manager
1451
1283
  */
1452
1284
  /**
1453
- * Built-in threat intelligence - known malware hashes
1454
- * In production, this would connect to real threat intel APIs
1285
+ * LRU cache for scan results with TTL support
1455
1286
  */
1456
- class LocalThreatIntelligence {
1457
- constructor() {
1458
- this.name = 'Local Database';
1459
- this.knownThreats = new Map();
1460
- // Initialize with some example known threats (in production, load from database)
1461
- this.initializeKnownThreats();
1462
- }
1463
- initializeKnownThreats() {
1464
- // Example: EICAR test file hash
1465
- this.knownThreats.set('275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f', {
1466
- threatLevel: 100,
1467
- category: 'test-malware',
1468
- source: 'local',
1469
- metadata: { name: 'EICAR Test File' },
1470
- });
1287
+ class ScanCacheManager {
1288
+ constructor(options = {}) {
1289
+ this.cache = new Map();
1290
+ // Statistics
1291
+ this.stats = {
1292
+ hits: 0,
1293
+ misses: 0,
1294
+ evictions: 0,
1295
+ };
1296
+ this.maxSize = options.maxSize ?? 1000;
1297
+ this.ttl = options.ttl ?? 3600000; // 1 hour default
1298
+ this.enableLRU = options.enableLRU ?? true;
1299
+ this.enableStats = options.enableStats ?? false;
1471
1300
  }
1472
- async checkHash(hash) {
1473
- return this.knownThreats.get(hash.toLowerCase()) || null;
1301
+ /**
1302
+ * Generate cache key from file content
1303
+ */
1304
+ generateKey(content, preset) {
1305
+ const hash = crypto.createHash("sha256")
1306
+ .update(content)
1307
+ .update(preset || "default")
1308
+ .digest("hex");
1309
+ return hash;
1474
1310
  }
1475
1311
  /**
1476
- * Add a known threat to the local database
1312
+ * Check if cache entry is still valid
1477
1313
  */
1478
- addThreat(hash, info) {
1479
- this.knownThreats.set(hash.toLowerCase(), info);
1314
+ isValid(entry) {
1315
+ return Date.now() - entry.timestamp < this.ttl;
1480
1316
  }
1481
1317
  /**
1482
- * Remove a threat from the local database
1318
+ * Evict oldest or least-used entry when cache is full
1483
1319
  */
1484
- removeThreat(hash) {
1485
- return this.knownThreats.delete(hash.toLowerCase());
1320
+ evict() {
1321
+ if (this.cache.size === 0)
1322
+ return;
1323
+ let targetKey = null;
1324
+ let oldestTime = Infinity;
1325
+ let lowestAccess = Infinity;
1326
+ for (const [key, entry] of this.cache.entries()) {
1327
+ if (this.enableLRU) {
1328
+ // LRU: evict least recently used
1329
+ if (entry.timestamp < oldestTime) {
1330
+ oldestTime = entry.timestamp;
1331
+ targetKey = key;
1332
+ }
1333
+ }
1334
+ else {
1335
+ // LFU: evict least frequently used
1336
+ if (entry.accessCount < lowestAccess) {
1337
+ lowestAccess = entry.accessCount;
1338
+ targetKey = key;
1339
+ }
1340
+ }
1341
+ }
1342
+ if (targetKey) {
1343
+ this.cache.delete(targetKey);
1344
+ if (this.enableStats)
1345
+ this.stats.evictions++;
1346
+ }
1486
1347
  }
1487
1348
  /**
1488
- * Get all known threats
1349
+ * Store scan result in cache
1489
1350
  */
1490
- getAllThreats() {
1491
- return new Map(this.knownThreats);
1351
+ set(content, report, preset) {
1352
+ const key = this.generateKey(content, preset);
1353
+ // Evict if necessary
1354
+ if (this.cache.size >= this.maxSize) {
1355
+ this.evict();
1356
+ }
1357
+ this.cache.set(key, {
1358
+ report,
1359
+ timestamp: Date.now(),
1360
+ accessCount: 0,
1361
+ });
1492
1362
  }
1493
- }
1494
- /**
1495
- * Threat intelligence aggregator
1496
- */
1497
- class ThreatIntelligenceAggregator {
1498
- constructor(sources) {
1499
- this.sources = [];
1500
- if (sources) {
1501
- this.sources = sources;
1363
+ /**
1364
+ * Retrieve scan result from cache
1365
+ */
1366
+ get(content, preset) {
1367
+ const key = this.generateKey(content, preset);
1368
+ const entry = this.cache.get(key);
1369
+ if (!entry) {
1370
+ if (this.enableStats)
1371
+ this.stats.misses++;
1372
+ return null;
1502
1373
  }
1503
- else {
1504
- // Default to local intelligence
1505
- this.sources = [new LocalThreatIntelligence()];
1374
+ if (!this.isValid(entry)) {
1375
+ this.cache.delete(key);
1376
+ if (this.enableStats)
1377
+ this.stats.misses++;
1378
+ return null;
1506
1379
  }
1380
+ // Update access tracking
1381
+ entry.accessCount++;
1382
+ entry.timestamp = Date.now(); // Update for LRU
1383
+ if (this.enableStats)
1384
+ this.stats.hits++;
1385
+ return entry.report;
1507
1386
  }
1508
1387
  /**
1509
- * Add a threat intelligence source
1388
+ * Check if result exists in cache
1510
1389
  */
1511
- addSource(source) {
1512
- this.sources.push(source);
1390
+ has(content, preset) {
1391
+ const key = this.generateKey(content, preset);
1392
+ const entry = this.cache.get(key);
1393
+ return entry !== undefined && this.isValid(entry);
1513
1394
  }
1514
1395
  /**
1515
- * Check file hash against all sources
1396
+ * Clear entire cache
1516
1397
  */
1517
- async checkHash(hash) {
1518
- const results = await Promise.allSettled(this.sources.map(source => source.checkHash(hash)));
1519
- const threats = [];
1520
- for (const result of results) {
1521
- if (result.status === 'fulfilled' && result.value) {
1522
- threats.push(result.value);
1398
+ clear() {
1399
+ this.cache.clear();
1400
+ if (this.enableStats) {
1401
+ this.stats.hits = 0;
1402
+ this.stats.misses = 0;
1403
+ this.stats.evictions = 0;
1404
+ }
1405
+ }
1406
+ /**
1407
+ * Remove expired entries
1408
+ */
1409
+ prune() {
1410
+ let removed = 0;
1411
+ for (const [key, entry] of this.cache.entries()) {
1412
+ if (!this.isValid(entry)) {
1413
+ this.cache.delete(key);
1414
+ removed++;
1523
1415
  }
1524
1416
  }
1525
- return threats;
1417
+ return removed;
1526
1418
  }
1527
1419
  /**
1528
- * Enhance scan report with threat intelligence
1420
+ * Get cache statistics
1529
1421
  */
1530
- async enhanceScanReport(content, report) {
1531
- // Calculate file hash
1532
- const hash = crypto.createHash('sha256').update(content).digest('hex');
1533
- // Check threat intelligence
1534
- const threatIntel = await this.checkHash(hash);
1535
- // Calculate risk score
1536
- const riskScore = this.calculateRiskScore(report, threatIntel);
1422
+ getStats() {
1423
+ const total = this.stats.hits + this.stats.misses;
1424
+ const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
1537
1425
  return {
1538
- ...report,
1539
- fileHash: hash,
1540
- threatIntel: threatIntel.length > 0 ? threatIntel : undefined,
1541
- riskScore,
1426
+ hits: this.stats.hits,
1427
+ misses: this.stats.misses,
1428
+ size: this.cache.size,
1429
+ hitRate,
1430
+ evictions: this.stats.evictions,
1542
1431
  };
1543
1432
  }
1544
1433
  /**
1545
- * Calculate overall risk score based on scan results and threat intel
1434
+ * Get current cache size
1546
1435
  */
1547
- calculateRiskScore(report, threats) {
1548
- let score = 0;
1549
- // Base score from verdict
1550
- switch (report.verdict) {
1551
- case 'malicious':
1552
- score += 70;
1553
- break;
1554
- case 'suspicious':
1555
- score += 40;
1556
- break;
1557
- case 'clean':
1558
- score += 0;
1559
- break;
1560
- }
1561
- // Add points for number of matches
1562
- score += Math.min(report.matches.length * 5, 20);
1563
- // Add points from threat intelligence
1564
- if (threats.length > 0) {
1565
- const maxThreat = Math.max(...threats.map(t => t.threatLevel));
1566
- score = Math.max(score, maxThreat);
1567
- }
1568
- return Math.min(score, 100);
1436
+ get size() {
1437
+ return this.cache.size;
1569
1438
  }
1570
1439
  }
1440
+ // Export singleton instance for convenience
1441
+ let defaultCache = null;
1571
1442
  /**
1572
- * Create default threat intelligence aggregator
1443
+ * Get or create the default cache instance
1573
1444
  */
1574
- function createThreatIntelligence() {
1575
- return new ThreatIntelligenceAggregator();
1445
+ function getDefaultCache(options) {
1446
+ if (!defaultCache) {
1447
+ defaultCache = new ScanCacheManager(options);
1448
+ }
1449
+ return defaultCache;
1576
1450
  }
1577
1451
  /**
1578
- * Helper to get file hash
1452
+ * Reset the default cache instance
1579
1453
  */
1580
- function getFileHash(content) {
1581
- return crypto.createHash('sha256').update(content).digest('hex');
1454
+ function resetDefaultCache() {
1455
+ defaultCache = null;
1582
1456
  }
1583
1457
 
1584
1458
  /**
1585
- * Export utilities for scan results
1586
- * @module utils/export
1587
- */
1588
- /**
1589
- * Export scan results to various formats
1590
- */
1591
- class ScanResultExporter {
1592
- /**
1593
- * Export to JSON format
1594
- */
1595
- toJSON(reports, options = {}) {
1596
- const data = Array.isArray(reports) ? reports : [reports];
1597
- if (!options.includeDetails) {
1598
- // Simplified output
1599
- const simplified = data.map(r => ({
1600
- verdict: r.verdict,
1601
- file: r.file?.name,
1602
- matches: r.matches.length,
1603
- durationMs: r.durationMs,
1604
- }));
1605
- return options.prettyPrint
1606
- ? JSON.stringify(simplified, null, 2)
1607
- : JSON.stringify(simplified);
1608
- }
1609
- return options.prettyPrint
1610
- ? JSON.stringify(data, null, 2)
1611
- : JSON.stringify(data);
1612
- }
1613
- /**
1614
- * Export to CSV format
1615
- */
1616
- toCSV(reports, options = {}) {
1617
- const data = Array.isArray(reports) ? reports : [reports];
1618
- const headers = [
1619
- 'filename',
1620
- 'verdict',
1621
- 'matches_count',
1622
- 'file_size',
1623
- 'mime_type',
1624
- 'duration_ms',
1625
- 'engine',
1626
- ];
1627
- if (options.includeDetails) {
1628
- headers.push('reasons', 'match_rules');
1629
- }
1630
- const rows = data.map(report => {
1631
- const row = [
1632
- this.escapeCsv(report.file?.name || 'unknown'),
1633
- report.verdict,
1634
- report.matches.length.toString(),
1635
- (report.file?.size || 0).toString(),
1636
- this.escapeCsv(report.file?.mimeType || 'unknown'),
1637
- (report.durationMs || 0).toString(),
1638
- report.engine || 'unknown',
1639
- ];
1640
- if (options.includeDetails) {
1641
- row.push(this.escapeCsv((report.reasons || []).join('; ')), this.escapeCsv(report.matches.map(m => m.rule).join('; ')));
1642
- }
1643
- return row.join(',');
1644
- });
1645
- return [headers.join(','), ...rows].join('\n');
1646
- }
1647
- /**
1648
- * Export to Markdown format
1649
- */
1650
- toMarkdown(reports, options = {}) {
1651
- const data = Array.isArray(reports) ? reports : [reports];
1652
- let md = '# Scan Results\n\n';
1653
- md += `**Total Scans:** ${data.length}\n\n`;
1654
- const clean = data.filter(r => r.verdict === 'clean').length;
1655
- const suspicious = data.filter(r => r.verdict === 'suspicious').length;
1656
- const malicious = data.filter(r => r.verdict === 'malicious').length;
1657
- md += '## Summary\n\n';
1658
- md += `- ✅ Clean: ${clean}\n`;
1659
- md += `- ⚠️ Suspicious: ${suspicious}\n`;
1660
- md += `- ❌ Malicious: ${malicious}\n\n`;
1661
- md += '## Detailed Results\n\n';
1662
- for (const report of data) {
1663
- const icon = report.verdict === 'clean' ? '✅' : report.verdict === 'suspicious' ? '⚠️' : '❌';
1664
- md += `### ${icon} ${report.file?.name || 'Unknown'}\n\n`;
1665
- md += `- **Verdict:** ${report.verdict}\n`;
1666
- md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
1667
- md += `- **MIME Type:** ${report.file?.mimeType || 'unknown'}\n`;
1668
- md += `- **Duration:** ${report.durationMs || 0}ms\n`;
1669
- md += `- **Matches:** ${report.matches.length}\n`;
1670
- if (options.includeDetails && report.matches.length > 0) {
1671
- md += '\n**Match Details:**\n';
1672
- for (const match of report.matches) {
1673
- md += `- ${match.rule}`;
1674
- if (match.tags && match.tags.length > 0) {
1675
- md += ` (${match.tags.join(', ')})`;
1676
- }
1677
- md += '\n';
1678
- }
1679
- }
1680
- md += '\n';
1681
- }
1682
- return md;
1459
+ * Performance monitoring utilities for pompelmi scans
1460
+ * @module utils/performance-metrics
1461
+ */
1462
+ /**
1463
+ * Track performance metrics for a scan operation
1464
+ */
1465
+ class PerformanceTracker {
1466
+ constructor() {
1467
+ this.checkpoints = new Map();
1468
+ this.startTime = Date.now();
1683
1469
  }
1684
1470
  /**
1685
- * Export to SARIF format (Static Analysis Results Interchange Format)
1686
- * Useful for CI/CD integration
1471
+ * Mark a checkpoint in the scan process
1687
1472
  */
1688
- toSARIF(reports, options = {}) {
1689
- const data = Array.isArray(reports) ? reports : [reports];
1690
- const results = data.flatMap(report => {
1691
- if (report.verdict === 'clean')
1692
- return [];
1693
- return report.matches.map(match => ({
1694
- ruleId: match.rule,
1695
- level: report.verdict === 'malicious' ? 'error' : 'warning',
1696
- message: {
1697
- text: `${match.rule} detected in ${report.file?.name || 'unknown file'}`,
1698
- },
1699
- locations: [
1700
- {
1701
- physicalLocation: {
1702
- artifactLocation: {
1703
- uri: report.file?.name || 'unknown',
1704
- },
1705
- },
1706
- },
1707
- ],
1708
- properties: {
1709
- tags: match.tags,
1710
- metadata: match.meta,
1711
- },
1712
- }));
1713
- });
1714
- const sarif = {
1715
- version: '2.1.0',
1716
- $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
1717
- runs: [
1718
- {
1719
- tool: {
1720
- driver: {
1721
- name: 'Pompelmi',
1722
- version: '0.29.0',
1723
- informationUri: 'https://pompelmi.github.io/pompelmi/',
1724
- },
1725
- },
1726
- results,
1727
- },
1728
- ],
1729
- };
1730
- return options.prettyPrint
1731
- ? JSON.stringify(sarif, null, 2)
1732
- : JSON.stringify(sarif);
1473
+ checkpoint(name) {
1474
+ this.checkpoints.set(name, Date.now());
1733
1475
  }
1734
1476
  /**
1735
- * Export to HTML format
1477
+ * Get duration since start or since a specific checkpoint
1736
1478
  */
1737
- toHTML(reports, options = {}) {
1738
- const data = Array.isArray(reports) ? reports : [reports];
1739
- const clean = data.filter(r => r.verdict === 'clean').length;
1740
- const suspicious = data.filter(r => r.verdict === 'suspicious').length;
1741
- const malicious = data.filter(r => r.verdict === 'malicious').length;
1742
- let html = `<!DOCTYPE html>
1743
- <html lang="en">
1744
- <head>
1745
- <meta charset="UTF-8">
1746
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1747
- <title>Pompelmi Scan Results</title>
1748
- <style>
1749
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
1750
- .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
1751
- .card { padding: 20px; border-radius: 8px; text-align: center; }
1752
- .clean { background: #d4edda; color: #155724; }
1753
- .suspicious { background: #fff3cd; color: #856404; }
1754
- .malicious { background: #f8d7da; color: #721c24; }
1755
- .result { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 10px 0; }
1756
- .result h3 { margin-top: 0; }
1757
- .badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; margin: 2px; }
1758
- table { width: 100%; border-collapse: collapse; }
1759
- th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
1760
- </style>
1761
- </head>
1762
- <body>
1763
- <h1>🛡️ Pompelmi Scan Results</h1>
1764
- <div class="summary">
1765
- <div class="card clean"><h2>${clean}</h2><p>Clean Files</p></div>
1766
- <div class="card suspicious"><h2>${suspicious}</h2><p>Suspicious Files</p></div>
1767
- <div class="card malicious"><h2>${malicious}</h2><p>Malicious Files</p></div>
1768
- </div>
1769
- <h2>Detailed Results</h2>`;
1770
- for (const report of data) {
1771
- const statusClass = report.verdict;
1772
- html += `<div class="result ${statusClass}">`;
1773
- html += `<h3>${this.escapeHtml(report.file?.name || 'Unknown')}</h3>`;
1774
- html += `<table>`;
1775
- html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
1776
- html += `<tr><th>Size</th><td>${this.formatBytes(report.file?.size || 0)}</td></tr>`;
1777
- html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || 'unknown')}</td></tr>`;
1778
- html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
1779
- html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
1780
- html += `</table>`;
1781
- if (options.includeDetails && report.matches.length > 0) {
1782
- html += `<h4>Match Details:</h4><ul>`;
1783
- for (const match of report.matches) {
1784
- html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
1785
- if (match.tags && match.tags.length > 0) {
1786
- html += ` ${match.tags.map(tag => `<span class="badge">${this.escapeHtml(tag)}</span>`).join('')}`;
1787
- }
1788
- html += `</li>`;
1789
- }
1790
- html += `</ul>`;
1791
- }
1792
- html += `</div>`;
1479
+ getDuration(since) {
1480
+ const now = Date.now();
1481
+ if (since && this.checkpoints.has(since)) {
1482
+ return now - (this.checkpoints.get(since) ?? now);
1793
1483
  }
1794
- html += `</body></html>`;
1795
- return html;
1484
+ return now - this.startTime;
1796
1485
  }
1797
1486
  /**
1798
- * Export to specified format
1487
+ * Generate final metrics report
1799
1488
  */
1800
- export(reports, format, options = {}) {
1801
- switch (format) {
1802
- case 'json':
1803
- return this.toJSON(reports, options);
1804
- case 'csv':
1805
- return this.toCSV(reports, options);
1806
- case 'markdown':
1807
- return this.toMarkdown(reports, options);
1808
- case 'html':
1809
- return this.toHTML(reports, options);
1810
- case 'sarif':
1811
- return this.toSARIF(reports, options);
1812
- default:
1813
- throw new Error(`Unsupported export format: ${format}`);
1489
+ getMetrics(bytesScanned) {
1490
+ const totalDuration = this.getDuration();
1491
+ const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
1492
+ return {
1493
+ totalDurationMs: totalDuration,
1494
+ heuristicsDurationMs: this.checkpoints.has("heuristics_end")
1495
+ ? (this.checkpoints.get("heuristics_end") ?? 0) -
1496
+ (this.checkpoints.get("heuristics_start") ?? 0)
1497
+ : undefined,
1498
+ yaraDurationMs: this.checkpoints.has("yara_end")
1499
+ ? (this.checkpoints.get("yara_end") ?? 0) - (this.checkpoints.get("yara_start") ?? 0)
1500
+ : undefined,
1501
+ prepDurationMs: this.checkpoints.has("prep_end")
1502
+ ? (this.checkpoints.get("prep_end") ?? 0) - this.startTime
1503
+ : undefined,
1504
+ throughputBps: throughput,
1505
+ bytesScanned,
1506
+ startedAt: this.startTime,
1507
+ completedAt: Date.now(),
1508
+ };
1509
+ }
1510
+ }
1511
+ /**
1512
+ * Aggregate statistics from multiple scan reports
1513
+ */
1514
+ function aggregateScanStats(reports) {
1515
+ let cleanCount = 0;
1516
+ let suspiciousCount = 0;
1517
+ let maliciousCount = 0;
1518
+ let totalDuration = 0;
1519
+ let totalBytes = 0;
1520
+ let validDurationCount = 0;
1521
+ for (const report of reports) {
1522
+ if (report.verdict === "clean")
1523
+ cleanCount++;
1524
+ else if (report.verdict === "suspicious")
1525
+ suspiciousCount++;
1526
+ else if (report.verdict === "malicious")
1527
+ maliciousCount++;
1528
+ if (report.durationMs !== undefined) {
1529
+ totalDuration += report.durationMs;
1530
+ validDurationCount++;
1531
+ }
1532
+ if (report.file?.size !== undefined) {
1533
+ totalBytes += report.file.size;
1814
1534
  }
1815
1535
  }
1816
- escapeCsv(value) {
1817
- if (value.includes(',') || value.includes('"') || value.includes('\n')) {
1818
- return `"${value.replace(/"/g, '""')}"`;
1536
+ const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
1537
+ const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
1538
+ return {
1539
+ totalScans: reports.length,
1540
+ cleanCount,
1541
+ suspiciousCount,
1542
+ maliciousCount,
1543
+ avgDurationMs: avgDuration,
1544
+ avgThroughputBps: avgThroughput,
1545
+ totalBytesScanned: totalBytes,
1546
+ };
1547
+ }
1548
+
1549
+ /** Mappa veloce estensione -> mime (basic) */
1550
+ function guessMimeByExt(name) {
1551
+ if (!name)
1552
+ return;
1553
+ const ext = name.toLowerCase().split(".").pop();
1554
+ switch (ext) {
1555
+ case "zip":
1556
+ return "application/zip";
1557
+ case "png":
1558
+ return "image/png";
1559
+ case "jpg":
1560
+ case "jpeg":
1561
+ return "image/jpeg";
1562
+ case "pdf":
1563
+ return "application/pdf";
1564
+ case "txt":
1565
+ return "text/plain";
1566
+ default:
1567
+ return;
1568
+ }
1569
+ }
1570
+ /** Heuristica semplice per verdetto */
1571
+ function computeVerdict(matches) {
1572
+ if (!matches.length)
1573
+ return "clean";
1574
+ // se la regola contiene 'zip_' lo marchiamo "suspicious"
1575
+ const anyHigh = matches.some((m) => (m.tags ?? []).includes("critical") || (m.tags ?? []).includes("high"));
1576
+ return anyHigh ? "malicious" : "suspicious";
1577
+ }
1578
+ /** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
1579
+ function toYaraMatches(ms) {
1580
+ return ms.map((m) => ({
1581
+ rule: m.rule,
1582
+ namespace: "heuristics",
1583
+ tags: ["heuristics"].concat(m.severity ? [m.severity] : []),
1584
+ meta: m.meta,
1585
+ }));
1586
+ }
1587
+ /** Scan di bytes (browser/node) usando preset (default: zip-basic) */
1588
+ async function scanBytes(input, opts = {}) {
1589
+ // Check cache first if enabled
1590
+ if (opts.enableCache || opts.config?.performance?.enableCache) {
1591
+ const cache = getDefaultCache(opts.config?.performance?.cacheOptions);
1592
+ const cached = cache.get(input, opts.preset);
1593
+ if (cached) {
1594
+ return cached;
1819
1595
  }
1820
- return value;
1821
1596
  }
1822
- escapeHtml(value) {
1823
- return value
1824
- .replace(/&/g, '&amp;')
1825
- .replace(/</g, '&lt;')
1826
- .replace(/>/g, '&gt;')
1827
- .replace(/"/g, '&quot;')
1828
- .replace(/'/g, '&#039;');
1597
+ const perfTracker = opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking
1598
+ ? new PerformanceTracker()
1599
+ : null;
1600
+ perfTracker?.checkpoint("prep_start");
1601
+ const preset = opts.preset ?? opts.config?.defaultPreset ?? "zip-basic";
1602
+ const ctx = {
1603
+ ...opts.ctx,
1604
+ mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
1605
+ size: opts.ctx?.size ?? input.byteLength,
1606
+ };
1607
+ perfTracker?.checkpoint("prep_end");
1608
+ perfTracker?.checkpoint("heuristics_start");
1609
+ const scanFn = createPresetScanner(preset);
1610
+ const matchesH = await (typeof scanFn === "function"
1611
+ ? scanFn
1612
+ : scanFn.scan)(input, ctx);
1613
+ const allMatches = [...matchesH];
1614
+ perfTracker?.checkpoint("heuristics_end");
1615
+ // Advanced detection (enabled by default, can be overridden by config)
1616
+ const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
1617
+ if (advancedEnabled) {
1618
+ perfTracker?.checkpoint("advanced_start");
1619
+ // Detect polyglot files
1620
+ if (opts.config?.advanced?.enablePolyglotDetection !== false) {
1621
+ const polyglotMatches = detectPolyglot(input);
1622
+ allMatches.push(...polyglotMatches);
1623
+ }
1624
+ // Detect obfuscated scripts
1625
+ if (opts.config?.advanced?.enableObfuscationDetection !== false) {
1626
+ const obfuscatedMatches = detectObfuscatedScripts(input);
1627
+ allMatches.push(...obfuscatedMatches);
1628
+ }
1629
+ // Check for excessive nesting in archives
1630
+ if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
1631
+ const nestingAnalysis = analyzeNestedArchives(input);
1632
+ const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
1633
+ if (nestingAnalysis.hasExcessiveNesting || nestingAnalysis.depth > maxDepth) {
1634
+ allMatches.push({
1635
+ rule: "excessive_archive_nesting",
1636
+ severity: "high",
1637
+ meta: {
1638
+ description: "Excessive archive nesting detected",
1639
+ depth: nestingAnalysis.depth,
1640
+ maxAllowed: maxDepth,
1641
+ },
1642
+ });
1643
+ }
1644
+ }
1645
+ perfTracker?.checkpoint("advanced_end");
1829
1646
  }
1830
- formatBytes(bytes) {
1831
- if (bytes === 0)
1832
- return '0 Bytes';
1833
- const k = 1024;
1834
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1835
- const i = Math.floor(Math.log(bytes) / Math.log(k));
1836
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
1647
+ const matches = toYaraMatches(allMatches);
1648
+ const verdict = computeVerdict(matches);
1649
+ perfTracker ? perfTracker.getDuration() : Date.now();
1650
+ const durationMs = perfTracker ? perfTracker.getDuration() : 0;
1651
+ const report = {
1652
+ ok: verdict === "clean",
1653
+ verdict,
1654
+ matches,
1655
+ reasons: matches.map((m) => m.rule),
1656
+ file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
1657
+ durationMs,
1658
+ engine: "heuristics",
1659
+ truncated: false,
1660
+ timedOut: false,
1661
+ };
1662
+ // Add performance metrics if tracking enabled
1663
+ if (perfTracker &&
1664
+ (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
1665
+ report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
1666
+ }
1667
+ // Cache result if enabled
1668
+ if (opts.enableCache || opts.config?.performance?.enableCache) {
1669
+ const cache = getDefaultCache(opts.config?.performance?.cacheOptions);
1670
+ cache.set(input, report, opts.preset);
1671
+ }
1672
+ // Invoke callbacks if configured
1673
+ opts.config?.callbacks?.onScanComplete?.(report);
1674
+ return report;
1675
+ }
1676
+ /** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
1677
+ async function scanFile(filePath, opts = {}) {
1678
+ const [{ readFile, stat }, path] = await Promise.all([import('fs/promises'), import('path')]);
1679
+ const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
1680
+ const ctx = {
1681
+ filename: path.basename(filePath),
1682
+ mimeType: guessMimeByExt(filePath),
1683
+ size: st.size,
1684
+ };
1685
+ return scanBytes(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), { ...opts, ctx });
1686
+ }
1687
+ /** Scan multipli File (browser) usando scanBytes + preset di default */
1688
+ async function scanFiles(files, opts = {}) {
1689
+ const list = Array.from(files);
1690
+ const out = [];
1691
+ for (const f of list) {
1692
+ const buf = new Uint8Array(await f.arrayBuffer());
1693
+ const rep = await scanBytes(buf, {
1694
+ ...opts,
1695
+ ctx: { filename: f.name, mimeType: f.type || guessMimeByExt(f.name), size: f.size },
1696
+ });
1697
+ out.push(rep);
1837
1698
  }
1699
+ return out;
1838
1700
  }
1839
- /**
1840
- * Quick export helper
1841
- */
1842
- function exportScanResults(reports, format, options) {
1843
- const exporter = new ScanResultExporter();
1844
- return exporter.export(reports, format, options);
1701
+
1702
+ async function createRemoteEngine(opts) {
1703
+ const { endpoint, headers = {}, rulesField = "rules", fileField = "file", mode = "multipart", rulesAsBase64 = false, } = opts;
1704
+ const engine = {
1705
+ async compile(rulesSource) {
1706
+ return {
1707
+ async scan(data) {
1708
+ const fetchFn = globalThis.fetch;
1709
+ if (!fetchFn)
1710
+ throw new Error("[remote-yara] fetch non disponibile in questo ambiente");
1711
+ let res;
1712
+ if (mode === "multipart") {
1713
+ const FormDataCtor = globalThis.FormData;
1714
+ const BlobCtor = globalThis.Blob;
1715
+ if (!FormDataCtor || !BlobCtor) {
1716
+ throw new Error("[remote-yara] FormData/Blob non disponibili (usa json-base64 oppure esegui in browser)");
1717
+ }
1718
+ const form = new FormDataCtor();
1719
+ form.set(rulesField, new BlobCtor([rulesSource], { type: "text/plain" }), "rules.yar");
1720
+ form.set(fileField, new BlobCtor([data], { type: "application/octet-stream" }), "sample.bin");
1721
+ res = await fetchFn(endpoint, { method: "POST", body: form, headers });
1722
+ }
1723
+ else {
1724
+ const b64 = base64FromBytes(data);
1725
+ const payload = { [fileField]: b64 };
1726
+ if (rulesAsBase64) {
1727
+ payload["rulesB64"] = base64FromString(rulesSource);
1728
+ }
1729
+ else {
1730
+ payload[rulesField] = rulesSource;
1731
+ }
1732
+ res = await fetchFn(endpoint, {
1733
+ method: "POST",
1734
+ headers: { "Content-Type": "application/json", ...headers },
1735
+ body: JSON.stringify(payload),
1736
+ });
1737
+ }
1738
+ if (!res.ok) {
1739
+ throw new Error(`[remote-yara] HTTP ${res.status} ${res.statusText}`);
1740
+ }
1741
+ const json = await res.json().catch(() => null);
1742
+ const arr = Array.isArray(json) ? json : (json?.matches ?? []);
1743
+ return (arr ?? []).map((m) => ({
1744
+ rule: m.rule ?? m.ruleIdentifier ?? "unknown",
1745
+ tags: m.tags ?? [],
1746
+ }));
1747
+ },
1748
+ };
1749
+ },
1750
+ };
1751
+ return engine;
1752
+ }
1753
+ // Helpers
1754
+ function base64FromBytes(bytes) {
1755
+ // usa btoa se disponibile (browser); altrimenti fallback manuale
1756
+ const btoaFn = globalThis.btoa;
1757
+ let bin = "";
1758
+ for (let i = 0; i < bytes.byteLength; i++)
1759
+ bin += String.fromCharCode(bytes[i]);
1760
+ return btoaFn ? btoaFn(bin) : Buffer.from(bin, "binary").toString("base64");
1761
+ }
1762
+ function base64FromString(s) {
1763
+ const btoaFn = globalThis.btoa;
1764
+ return btoaFn ? btoaFn(s) : Buffer.from(s, "utf8").toString("base64");
1845
1765
  }
1846
1766
 
1767
+ // src/scan/remote.ts
1847
1768
  /**
1848
- * Advanced configuration system for pompelmi
1849
- * @module config
1850
- */
1851
- /**
1852
- * Default configuration
1769
+ * Scansiona una lista di File nel browser usando il motore remoto via HTTP.
1770
+ * Non richiede WASM né dipendenze native sul client.
1853
1771
  */
1854
- const DEFAULT_CONFIG = {
1855
- defaultPreset: 'zip-basic',
1856
- performance: {
1857
- enableCache: false,
1858
- enablePerformanceTracking: false,
1859
- enableParallel: true,
1860
- maxConcurrency: 5,
1861
- cacheOptions: {
1862
- maxSize: 1000,
1863
- ttl: 3600000, // 1 hour
1864
- enableLRU: true,
1865
- enableStats: false,
1866
- },
1867
- },
1868
- security: {
1869
- maxFileSize: 100 * 1024 * 1024, // 100MB
1870
- enableThreatIntel: false,
1871
- scanTimeout: 30000, // 30 seconds
1872
- strictMode: false,
1873
- },
1874
- advanced: {
1875
- enablePolyglotDetection: true,
1876
- enableObfuscationDetection: true,
1877
- enableNestedArchiveAnalysis: true,
1878
- maxArchiveDepth: 5,
1879
- },
1880
- logging: {
1881
- verbose: false,
1882
- level: 'info',
1883
- enableStats: false,
1884
- },
1772
+ async function scanFilesWithRemoteYara(files, rulesSource, remote) {
1773
+ const engine = await createRemoteEngine(remote);
1774
+ const compiled = await engine.compile(rulesSource);
1775
+ const results = [];
1776
+ for (const file of files) {
1777
+ try {
1778
+ const bytes = new Uint8Array(await file.arrayBuffer());
1779
+ const matches = await compiled.scan(bytes);
1780
+ results.push({ file, matches });
1781
+ }
1782
+ catch (err) {
1783
+ console.warn("[remote-yara] scan error for", file.name, err);
1784
+ results.push({ file, matches: [], error: String(err?.message ?? err) });
1785
+ }
1786
+ }
1787
+ return results;
1788
+ }
1789
+
1790
+ const SIG_CEN = 0x02014b50;
1791
+ const DEFAULTS = {
1792
+ maxEntries: 1000,
1793
+ maxTotalUncompressedBytes: 500 * 1024 * 1024,
1794
+ maxEntryNameLength: 255,
1795
+ maxCompressionRatio: 1000,
1796
+ eocdSearchWindow: 70000,
1885
1797
  };
1886
- /**
1887
- * Configuration presets for common use cases
1888
- */
1889
- const CONFIG_PRESETS = {
1890
- /** Fast scanning with minimal features */
1891
- fast: {
1892
- defaultPreset: 'basic',
1893
- performance: {
1894
- enableCache: true,
1895
- enablePerformanceTracking: false,
1896
- maxConcurrency: 10,
1897
- },
1898
- advanced: {
1899
- enablePolyglotDetection: false,
1900
- enableObfuscationDetection: false,
1901
- enableNestedArchiveAnalysis: false,
1902
- },
1903
- },
1904
- /** Balanced scanning (recommended) */
1905
- balanced: DEFAULT_CONFIG,
1906
- /** Thorough scanning with all features */
1907
- thorough: {
1908
- defaultPreset: 'advanced',
1909
- performance: {
1910
- enableCache: true,
1911
- enablePerformanceTracking: true,
1912
- maxConcurrency: 3,
1913
- },
1914
- security: {
1915
- maxFileSize: 500 * 1024 * 1024, // 500MB
1916
- enableThreatIntel: true,
1917
- scanTimeout: 60000, // 60 seconds
1918
- strictMode: true,
1919
- },
1920
- advanced: {
1921
- enablePolyglotDetection: true,
1922
- enableObfuscationDetection: true,
1923
- enableNestedArchiveAnalysis: true,
1924
- maxArchiveDepth: 10,
1925
- },
1926
- logging: {
1927
- verbose: true,
1928
- level: 'debug',
1929
- enableStats: true,
1798
+ function r16(buf, off) {
1799
+ return buf.readUInt16LE(off);
1800
+ }
1801
+ function r32(buf, off) {
1802
+ return buf.readUInt32LE(off);
1803
+ }
1804
+ function isZipLike(buf) {
1805
+ // local file header at start is common
1806
+ return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
1807
+ }
1808
+ function lastIndexOfEOCD(buf, window) {
1809
+ const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
1810
+ const start = Math.max(0, buf.length - window);
1811
+ const idx = buf.lastIndexOf(sig, Math.min(buf.length - sig.length, buf.length - 1));
1812
+ return idx >= start ? idx : -1;
1813
+ }
1814
+ function hasTraversal(name) {
1815
+ return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
1816
+ }
1817
+ function createZipBombGuard(opts = {}) {
1818
+ const cfg = { ...DEFAULTS, ...opts };
1819
+ return {
1820
+ async scan(input) {
1821
+ const buf = Buffer.from(input);
1822
+ const matches = [];
1823
+ if (!isZipLike(buf))
1824
+ return matches;
1825
+ // Find EOCD near the end
1826
+ const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
1827
+ if (eocdPos < 0 || eocdPos + 22 > buf.length) {
1828
+ // ZIP but no EOCD — malformed or polyglot → suspicious
1829
+ matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
1830
+ return matches;
1831
+ }
1832
+ const totalEntries = r16(buf, eocdPos + 10);
1833
+ const cdSize = r32(buf, eocdPos + 12);
1834
+ const cdOffset = r32(buf, eocdPos + 16);
1835
+ // Bounds check
1836
+ if (cdOffset + cdSize > buf.length) {
1837
+ matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
1838
+ return matches;
1839
+ }
1840
+ // Iterate central directory entries
1841
+ let ptr = cdOffset;
1842
+ let seen = 0;
1843
+ let sumComp = 0;
1844
+ let sumUnc = 0;
1845
+ while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
1846
+ const sig = r32(buf, ptr);
1847
+ if (sig !== SIG_CEN)
1848
+ break; // stop if structure breaks
1849
+ const compSize = r32(buf, ptr + 20);
1850
+ const uncSize = r32(buf, ptr + 24);
1851
+ const fnLen = r16(buf, ptr + 28);
1852
+ const exLen = r16(buf, ptr + 30);
1853
+ const cmLen = r16(buf, ptr + 32);
1854
+ const nameStart = ptr + 46;
1855
+ const nameEnd = nameStart + fnLen;
1856
+ if (nameEnd > buf.length)
1857
+ break;
1858
+ const name = buf.toString("utf8", nameStart, nameEnd);
1859
+ sumComp += compSize;
1860
+ sumUnc += uncSize;
1861
+ seen++;
1862
+ if (name.length > cfg.maxEntryNameLength) {
1863
+ matches.push({
1864
+ rule: "zip_entry_name_too_long",
1865
+ severity: "medium",
1866
+ meta: { name, length: name.length },
1867
+ });
1868
+ }
1869
+ if (hasTraversal(name)) {
1870
+ matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
1871
+ }
1872
+ // move to next entry
1873
+ ptr = nameEnd + exLen + cmLen;
1874
+ }
1875
+ if (seen !== totalEntries) {
1876
+ // central dir truncated/odd, still report what we found
1877
+ matches.push({
1878
+ rule: "zip_cd_truncated",
1879
+ severity: "medium",
1880
+ meta: { seen, totalEntries },
1881
+ });
1882
+ }
1883
+ // Heuristics thresholds
1884
+ if (seen > cfg.maxEntries) {
1885
+ matches.push({
1886
+ rule: "zip_too_many_entries",
1887
+ severity: "medium",
1888
+ meta: { seen, limit: cfg.maxEntries },
1889
+ });
1890
+ }
1891
+ if (sumUnc > cfg.maxTotalUncompressedBytes) {
1892
+ matches.push({
1893
+ rule: "zip_total_uncompressed_too_large",
1894
+ severity: "medium",
1895
+ meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
1896
+ });
1897
+ }
1898
+ if (sumComp === 0 && sumUnc > 0) {
1899
+ matches.push({
1900
+ rule: "zip_suspicious_ratio",
1901
+ severity: "medium",
1902
+ meta: { ratio: Infinity },
1903
+ });
1904
+ }
1905
+ else if (sumComp > 0) {
1906
+ const ratio = sumUnc / Math.max(1, sumComp);
1907
+ if (ratio >= cfg.maxCompressionRatio) {
1908
+ matches.push({
1909
+ rule: "zip_suspicious_ratio",
1910
+ severity: "medium",
1911
+ meta: { ratio, limit: cfg.maxCompressionRatio },
1912
+ });
1913
+ }
1914
+ }
1915
+ return matches;
1930
1916
  },
1917
+ };
1918
+ }
1919
+
1920
+ /** Decompilation-specific types for Pompelmi */
1921
+ const SUSPICIOUS_PATTERNS = [
1922
+ {
1923
+ name: "syscall_direct",
1924
+ description: "Direct system call without library wrapper",
1925
+ severity: "medium",
1926
+ pattern: /syscall|sysenter|int\s+0x80/i,
1931
1927
  },
1932
- /** Production-ready configuration */
1933
- production: {
1934
- defaultPreset: 'advanced',
1935
- performance: {
1936
- enableCache: true,
1937
- enablePerformanceTracking: true,
1938
- maxConcurrency: 5,
1939
- cacheOptions: {
1940
- maxSize: 5000,
1941
- ttl: 7200000, // 2 hours
1942
- enableLRU: true,
1943
- enableStats: true,
1944
- },
1945
- },
1946
- security: {
1947
- maxFileSize: 200 * 1024 * 1024, // 200MB
1948
- enableThreatIntel: true,
1949
- scanTimeout: 45000,
1950
- strictMode: false,
1951
- },
1952
- advanced: {
1953
- enablePolyglotDetection: true,
1954
- enableObfuscationDetection: true,
1955
- enableNestedArchiveAnalysis: true,
1956
- maxArchiveDepth: 7,
1957
- },
1958
- logging: {
1959
- verbose: false,
1960
- level: 'warn',
1961
- enableStats: true,
1962
- },
1928
+ {
1929
+ name: "process_injection",
1930
+ description: "Process injection techniques",
1931
+ severity: "high",
1932
+ pattern: /CreateRemoteThread|WriteProcessMemory|VirtualAllocEx/i,
1963
1933
  },
1964
- /** Development configuration */
1965
- development: {
1966
- defaultPreset: 'basic',
1967
- performance: {
1968
- enableCache: false,
1969
- enablePerformanceTracking: true,
1970
- maxConcurrency: 3,
1971
- },
1972
- security: {
1973
- maxFileSize: 50 * 1024 * 1024, // 50MB
1974
- scanTimeout: 15000,
1975
- strictMode: false,
1976
- },
1977
- logging: {
1978
- verbose: true,
1979
- level: 'debug',
1980
- enableStats: true,
1981
- },
1934
+ {
1935
+ name: "anti_debug",
1936
+ description: "Anti-debugging techniques",
1937
+ severity: "medium",
1938
+ pattern: /IsDebuggerPresent|CheckRemoteDebuggerPresent|OutputDebugString/i,
1982
1939
  },
1983
- };
1940
+ {
1941
+ name: "obfuscation_xor",
1942
+ description: "XOR-based obfuscation pattern",
1943
+ severity: "medium",
1944
+ pattern: /xor.*0x[0-9a-f]+.*xor/i,
1945
+ },
1946
+ {
1947
+ name: "crypto_constants",
1948
+ description: "Cryptographic constants",
1949
+ severity: "low",
1950
+ pattern: /0x67452301|0xefcdab89|0x98badcfe|0x10325476/i,
1951
+ },
1952
+ ];
1953
+
1984
1954
  /**
1985
- * Configuration manager
1955
+ * Batch scanning with concurrency control
1956
+ * @module utils/batch-scanner
1986
1957
  */
1987
- class ConfigManager {
1988
- constructor(initialConfig) {
1989
- this.config = this.mergeConfig(DEFAULT_CONFIG, initialConfig || {});
1990
- }
1991
- /**
1992
- * Get current configuration
1993
- */
1994
- getConfig() {
1995
- return { ...this.config };
1996
- }
1997
- /**
1998
- * Update configuration
1999
- */
2000
- updateConfig(updates) {
2001
- this.config = this.mergeConfig(this.config, updates);
2002
- }
2003
- /**
2004
- * Load a preset configuration
2005
- */
2006
- loadPreset(preset) {
2007
- const presetConfig = CONFIG_PRESETS[preset];
2008
- this.config = this.mergeConfig(DEFAULT_CONFIG, presetConfig);
2009
- }
2010
- /**
2011
- * Reset to default configuration
2012
- */
2013
- reset() {
2014
- this.config = { ...DEFAULT_CONFIG };
2015
- }
2016
- /**
2017
- * Get a specific configuration value
2018
- */
2019
- get(key) {
2020
- return this.config[key];
2021
- }
2022
- /**
2023
- * Set a specific configuration value
2024
- */
2025
- set(key, value) {
2026
- this.config[key] = value;
1958
+ /**
1959
+ * Batch file scanner with concurrency control and progress tracking
1960
+ */
1961
+ class BatchScanner {
1962
+ constructor(options = {}) {
1963
+ this.options = {
1964
+ concurrency: 5,
1965
+ continueOnError: true,
1966
+ ...options,
1967
+ };
2027
1968
  }
2028
1969
  /**
2029
- * Validate configuration
1970
+ * Scan multiple files with controlled concurrency
2030
1971
  */
2031
- validate() {
1972
+ async scanBatch(tasks) {
1973
+ const startTime = Date.now();
1974
+ const results = new Array(tasks.length);
2032
1975
  const errors = [];
2033
- // Validate performance settings
2034
- if (this.config.performance?.maxConcurrency !== undefined) {
2035
- if (this.config.performance.maxConcurrency < 1) {
2036
- errors.push('maxConcurrency must be at least 1');
2037
- }
2038
- if (this.config.performance.maxConcurrency > 50) {
2039
- errors.push('maxConcurrency should not exceed 50');
2040
- }
2041
- }
2042
- // Validate security settings
2043
- if (this.config.security?.maxFileSize !== undefined) {
2044
- if (this.config.security.maxFileSize < 1024) {
2045
- errors.push('maxFileSize must be at least 1KB');
1976
+ let successCount = 0;
1977
+ let errorCount = 0;
1978
+ let completedCount = 0;
1979
+ const concurrency = this.options.concurrency ?? 5;
1980
+ // Process tasks in chunks with controlled concurrency
1981
+ const processingQueue = [];
1982
+ let currentIndex = 0;
1983
+ const processTask = async (index) => {
1984
+ try {
1985
+ const task = tasks[index];
1986
+ const report = await scanBytes(task.content, {
1987
+ ...this.options,
1988
+ ctx: task.context,
1989
+ });
1990
+ results[index] = report;
1991
+ successCount++;
1992
+ completedCount++;
1993
+ if (this.options.onProgress) {
1994
+ this.options.onProgress(completedCount, tasks.length, report);
1995
+ }
2046
1996
  }
2047
- }
2048
- if (this.config.security?.scanTimeout !== undefined) {
2049
- if (this.config.security.scanTimeout < 1000) {
2050
- errors.push('scanTimeout must be at least 1000ms');
1997
+ catch (error) {
1998
+ errorCount++;
1999
+ completedCount++;
2000
+ const err = error instanceof Error ? error : new Error(String(error));
2001
+ if (this.options.onError) {
2002
+ this.options.onError(err, index);
2003
+ }
2004
+ errors.push({ index, error: err });
2005
+ if (!this.options.continueOnError) {
2006
+ throw err;
2007
+ }
2008
+ results[index] = null;
2051
2009
  }
2052
- }
2053
- // Validate advanced settings
2054
- if (this.config.advanced?.maxArchiveDepth !== undefined) {
2055
- if (this.config.advanced.maxArchiveDepth < 1) {
2056
- errors.push('maxArchiveDepth must be at least 1');
2010
+ };
2011
+ // Start initial batch of concurrent tasks
2012
+ while (currentIndex < tasks.length) {
2013
+ while (processingQueue.length < concurrency && currentIndex < tasks.length) {
2014
+ const promise = processTask(currentIndex);
2015
+ processingQueue.push(promise);
2016
+ currentIndex++;
2017
+ // Remove completed promises from queue
2018
+ promise.finally(() => {
2019
+ const idx = processingQueue.indexOf(promise);
2020
+ if (idx > -1)
2021
+ processingQueue.splice(idx, 1);
2022
+ });
2057
2023
  }
2058
- if (this.config.advanced.maxArchiveDepth > 20) {
2059
- errors.push('maxArchiveDepth should not exceed 20');
2024
+ // Wait for at least one task to complete before continuing
2025
+ if (processingQueue.length >= concurrency) {
2026
+ await Promise.race(processingQueue);
2060
2027
  }
2061
2028
  }
2029
+ // Wait for all remaining tasks
2030
+ await Promise.all(processingQueue);
2031
+ const totalDurationMs = Date.now() - startTime;
2062
2032
  return {
2063
- valid: errors.length === 0,
2033
+ reports: results,
2034
+ successCount,
2035
+ errorCount,
2036
+ totalDurationMs,
2064
2037
  errors,
2065
2038
  };
2066
2039
  }
2067
2040
  /**
2068
- * Deep merge configuration objects
2041
+ * Scan files from File objects (browser environment)
2069
2042
  */
2070
- mergeConfig(base, updates) {
2071
- return {
2072
- ...base,
2073
- ...updates,
2074
- performance: {
2075
- ...base.performance,
2076
- ...updates.performance,
2077
- cacheOptions: {
2078
- ...base.performance?.cacheOptions,
2079
- ...updates.performance?.cacheOptions,
2080
- },
2081
- },
2082
- security: {
2083
- ...base.security,
2084
- ...updates.security,
2085
- },
2086
- advanced: {
2087
- ...base.advanced,
2088
- ...updates.advanced,
2089
- },
2090
- logging: {
2091
- ...base.logging,
2092
- ...updates.logging,
2093
- },
2094
- callbacks: {
2095
- ...base.callbacks,
2096
- ...updates.callbacks,
2097
- },
2098
- presetOptions: {
2099
- ...base.presetOptions,
2100
- ...updates.presetOptions,
2043
+ async scanFiles(files) {
2044
+ const tasks = await Promise.all(files.map(async (file) => ({
2045
+ content: new Uint8Array(await file.arrayBuffer()),
2046
+ context: {
2047
+ filename: file.name,
2048
+ mimeType: file.type,
2049
+ size: file.size,
2101
2050
  },
2102
- };
2103
- }
2104
- /**
2105
- * Export configuration as JSON
2106
- */
2107
- toJSON() {
2108
- return JSON.stringify(this.config, null, 2);
2051
+ })));
2052
+ return this.scanBatch(tasks);
2109
2053
  }
2110
2054
  /**
2111
- * Load configuration from JSON
2055
+ * Scan files from file paths (Node.js environment)
2112
2056
  */
2113
- fromJSON(json) {
2114
- try {
2115
- const parsed = JSON.parse(json);
2116
- this.config = this.mergeConfig(DEFAULT_CONFIG, parsed);
2117
- }
2118
- catch (error) {
2119
- throw new Error(`Failed to parse configuration JSON: ${error}`);
2120
- }
2057
+ async scanFilePaths(filePaths) {
2058
+ const fs = await import('fs/promises');
2059
+ const path = await import('path');
2060
+ const tasks = await Promise.all(filePaths.map(async (filePath) => {
2061
+ const [content, stats] = await Promise.all([fs.readFile(filePath), fs.stat(filePath)]);
2062
+ return {
2063
+ content: new Uint8Array(content),
2064
+ context: {
2065
+ filename: path.basename(filePath),
2066
+ size: stats.size,
2067
+ },
2068
+ };
2069
+ }));
2070
+ return this.scanBatch(tasks);
2121
2071
  }
2122
2072
  }
2123
2073
  /**
2124
- * Create a new configuration manager
2074
+ * Quick helper for batch scanning with default options
2125
2075
  */
2126
- function createConfig(config) {
2127
- return new ConfigManager(config);
2076
+ async function batchScan(tasks, options) {
2077
+ const scanner = new BatchScanner(options);
2078
+ return scanner.scanBatch(tasks);
2128
2079
  }
2080
+
2129
2081
  /**
2130
- * Get a preset configuration
2082
+ * Export utilities for scan results
2083
+ * @module utils/export
2131
2084
  */
2132
- function getPresetConfig(preset) {
2133
- return { ...DEFAULT_CONFIG, ...CONFIG_PRESETS[preset] };
2134
- }
2135
-
2136
2085
  /**
2137
- * HIPAA Compliance Module for Pompelmi
2138
- *
2139
- * This module provides comprehensive HIPAA compliance features for healthcare environments
2140
- * where Pompelmi is used to analyze potentially compromised systems containing PHI.
2141
- *
2142
- * Key protections:
2143
- * - Data sanitization and redaction
2144
- * - Secure temporary file handling
2145
- * - Audit logging
2146
- * - Memory protection
2147
- * - Error message sanitization
2086
+ * Export scan results to various formats
2148
2087
  */
2149
- class HipaaComplianceManager {
2150
- constructor(config) {
2151
- this.auditEvents = [];
2152
- this.config = {
2153
- sanitizeErrors: true,
2154
- sanitizeFilenames: true,
2155
- encryptTempFiles: true,
2156
- memoryProtection: true,
2157
- requireSecureTransport: true,
2158
- ...config,
2159
- enabled: config.enabled !== undefined ? config.enabled : true
2160
- };
2161
- this.sessionId = this.generateSessionId();
2162
- }
2088
+ class ScanResultExporter {
2163
2089
  /**
2164
- * Sanitize filename to prevent PHI leakage in logs
2090
+ * Export to JSON format
2165
2091
  */
2166
- sanitizeFilename(filename) {
2167
- if (!this.config.enabled || !this.config.sanitizeFilenames || !filename) {
2168
- return filename || 'unknown';
2092
+ toJSON(reports, options = {}) {
2093
+ const data = Array.isArray(reports) ? reports : [reports];
2094
+ if (!options.includeDetails) {
2095
+ // Simplified output
2096
+ const simplified = data.map((r) => ({
2097
+ verdict: r.verdict,
2098
+ file: r.file?.name,
2099
+ matches: r.matches.length,
2100
+ durationMs: r.durationMs,
2101
+ }));
2102
+ return options.prettyPrint ? JSON.stringify(simplified, null, 2) : JSON.stringify(simplified);
2169
2103
  }
2170
- // Remove potentially sensitive path information
2171
- const basename = path__namespace.basename(filename);
2172
- // Hash the filename to create a consistent but non-revealing identifier
2173
- const hash = crypto__namespace.createHash('sha256').update(basename).digest('hex').substring(0, 8);
2174
- // Preserve file extension for analysis purposes
2175
- const ext = path__namespace.extname(basename);
2176
- return `file_${hash}${ext}`;
2104
+ return options.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
2177
2105
  }
2178
2106
  /**
2179
- * Sanitize error messages to prevent PHI exposure
2107
+ * Export to CSV format
2180
2108
  */
2181
- sanitizeError(error) {
2182
- if (!this.config.enabled || !this.config.sanitizeErrors) {
2183
- return typeof error === 'string' ? error : error.message;
2109
+ toCSV(reports, options = {}) {
2110
+ const data = Array.isArray(reports) ? reports : [reports];
2111
+ const headers = [
2112
+ "filename",
2113
+ "verdict",
2114
+ "matches_count",
2115
+ "file_size",
2116
+ "mime_type",
2117
+ "duration_ms",
2118
+ "engine",
2119
+ ];
2120
+ if (options.includeDetails) {
2121
+ headers.push("reasons", "match_rules");
2184
2122
  }
2185
- const message = typeof error === 'string' ? error : error.message;
2186
- // Remove common patterns that might contain PHI
2187
- let sanitized = message
2188
- // Remove file paths
2189
- .replace(/[A-Za-z]:\\\\[^\\s]+/g, '[REDACTED_PATH]')
2190
- .replace(/\/[^\\s]+/g, '[REDACTED_PATH]')
2191
- // Remove potential patient identifiers (numbers that could be MRNs, SSNs)
2192
- .replace(/\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g, '[REDACTED_ID]')
2193
- .replace(/\\b\\d{6,}\\b/g, '[REDACTED_ID]')
2194
- // Remove email addresses
2195
- .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g, '[REDACTED_EMAIL]')
2196
- // Remove potential names (capitalize words in error messages)
2197
- .replace(/\\b[A-Z][a-z]+\\s+[A-Z][a-z]+\\b/g, '[REDACTED_NAME]')
2198
- // Remove IP addresses
2199
- .replace(/\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g, '[REDACTED_IP]');
2200
- return sanitized;
2123
+ const rows = data.map((report) => {
2124
+ const row = [
2125
+ this.escapeCsv(report.file?.name || "unknown"),
2126
+ report.verdict,
2127
+ report.matches.length.toString(),
2128
+ (report.file?.size || 0).toString(),
2129
+ this.escapeCsv(report.file?.mimeType || "unknown"),
2130
+ (report.durationMs || 0).toString(),
2131
+ report.engine || "unknown",
2132
+ ];
2133
+ if (options.includeDetails) {
2134
+ row.push(this.escapeCsv((report.reasons || []).join("; ")), this.escapeCsv(report.matches.map((m) => m.rule).join("; ")));
2135
+ }
2136
+ return row.join(",");
2137
+ });
2138
+ return [headers.join(","), ...rows].join("\n");
2201
2139
  }
2202
2140
  /**
2203
- * Create secure temporary file path with encryption if enabled
2141
+ * Export to Markdown format
2204
2142
  */
2205
- createSecureTempPath(prefix = 'pompelmi') {
2206
- if (!this.config.enabled) {
2207
- return path__namespace.join(os__namespace.tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
2143
+ toMarkdown(reports, options = {}) {
2144
+ const data = Array.isArray(reports) ? reports : [reports];
2145
+ let md = "# Scan Results\n\n";
2146
+ md += `**Total Scans:** ${data.length}\n\n`;
2147
+ const clean = data.filter((r) => r.verdict === "clean").length;
2148
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
2149
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
2150
+ md += "## Summary\n\n";
2151
+ md += `- ✅ Clean: ${clean}\n`;
2152
+ md += `- ⚠️ Suspicious: ${suspicious}\n`;
2153
+ md += `- ❌ Malicious: ${malicious}\n\n`;
2154
+ md += "## Detailed Results\n\n";
2155
+ for (const report of data) {
2156
+ const icon = report.verdict === "clean" ? "✅" : report.verdict === "suspicious" ? "⚠️" : "❌";
2157
+ md += `### ${icon} ${report.file?.name || "Unknown"}\n\n`;
2158
+ md += `- **Verdict:** ${report.verdict}\n`;
2159
+ md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
2160
+ md += `- **MIME Type:** ${report.file?.mimeType || "unknown"}\n`;
2161
+ md += `- **Duration:** ${report.durationMs || 0}ms\n`;
2162
+ md += `- **Matches:** ${report.matches.length}\n`;
2163
+ if (options.includeDetails && report.matches.length > 0) {
2164
+ md += "\n**Match Details:**\n";
2165
+ for (const match of report.matches) {
2166
+ md += `- ${match.rule}`;
2167
+ if (match.tags && match.tags.length > 0) {
2168
+ md += ` (${match.tags.join(", ")})`;
2169
+ }
2170
+ md += "\n";
2171
+ }
2172
+ }
2173
+ md += "\n";
2208
2174
  }
2209
- // Use cryptographically secure random names
2210
- const randomId = crypto__namespace.randomBytes(16).toString('hex');
2211
- const timestamp = Date.now();
2212
- // Create path in secure temp directory
2213
- const secureTempDir = this.getSecureTempDir();
2214
- const tempPath = path__namespace.join(secureTempDir, `${prefix}-${timestamp}-${randomId}`);
2215
- this.auditLog('temp_file_created', {
2216
- action: 'create_temp_file',
2217
- success: true,
2218
- metadata: { path: this.sanitizeFilename(tempPath) }
2219
- });
2220
- return tempPath;
2175
+ return md;
2221
2176
  }
2222
2177
  /**
2223
- * Get or create secure temporary directory with restricted permissions
2178
+ * Export to SARIF format (Static Analysis Results Interchange Format)
2179
+ * Useful for CI/CD integration
2224
2180
  */
2225
- getSecureTempDir() {
2226
- const secureTempPath = path__namespace.join(os__namespace.tmpdir(), 'pompelmi-secure');
2227
- try {
2228
- const fs = require('fs');
2229
- if (!fs.existsSync(secureTempPath)) {
2230
- fs.mkdirSync(secureTempPath, { mode: 0o700 }); // Owner read/write/execute only
2231
- }
2232
- }
2233
- catch (error) {
2234
- // Fallback to system temp
2235
- return os__namespace.tmpdir();
2236
- }
2237
- return secureTempPath;
2181
+ toSARIF(reports, options = {}) {
2182
+ const data = Array.isArray(reports) ? reports : [reports];
2183
+ const results = data.flatMap((report) => {
2184
+ if (report.verdict === "clean")
2185
+ return [];
2186
+ return report.matches.map((match) => ({
2187
+ ruleId: match.rule,
2188
+ level: report.verdict === "malicious" ? "error" : "warning",
2189
+ message: {
2190
+ text: `${match.rule} detected in ${report.file?.name || "unknown file"}`,
2191
+ },
2192
+ locations: [
2193
+ {
2194
+ physicalLocation: {
2195
+ artifactLocation: {
2196
+ uri: report.file?.name || "unknown",
2197
+ },
2198
+ },
2199
+ },
2200
+ ],
2201
+ properties: {
2202
+ tags: match.tags,
2203
+ metadata: match.meta,
2204
+ },
2205
+ }));
2206
+ });
2207
+ const sarif = {
2208
+ version: "2.1.0",
2209
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
2210
+ runs: [
2211
+ {
2212
+ tool: {
2213
+ driver: {
2214
+ name: "Pompelmi",
2215
+ version: "0.29.0",
2216
+ informationUri: "https://pompelmi.github.io/pompelmi/",
2217
+ },
2218
+ },
2219
+ results,
2220
+ },
2221
+ ],
2222
+ };
2223
+ return options.prettyPrint ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
2238
2224
  }
2239
2225
  /**
2240
- * Secure file cleanup with multiple overwrite passes
2226
+ * Export to HTML format
2241
2227
  */
2242
- async secureFileCleanup(filePath) {
2243
- if (!this.config.enabled) {
2244
- try {
2245
- const fs = await import('fs/promises');
2246
- await fs.unlink(filePath);
2247
- }
2248
- catch {
2249
- // Ignore cleanup errors
2250
- }
2251
- return;
2252
- }
2253
- try {
2254
- const fs = await import('fs/promises');
2255
- const stats = await fs.stat(filePath);
2256
- if (this.config.memoryProtection) {
2257
- // Overwrite file with random data multiple times (DoD 5220.22-M standard)
2258
- const fileSize = stats.size;
2259
- const buffer = crypto__namespace.randomBytes(Math.min(fileSize, 64 * 1024)); // 64KB chunks
2260
- for (let pass = 0; pass < 3; pass++) {
2261
- const handle = await fs.open(filePath, 'r+');
2262
- try {
2263
- for (let offset = 0; offset < fileSize; offset += buffer.length) {
2264
- const chunk = offset + buffer.length > fileSize
2265
- ? buffer.subarray(0, fileSize - offset)
2266
- : buffer;
2267
- await handle.write(chunk, 0, chunk.length, offset);
2268
- }
2269
- await handle.sync();
2270
- }
2271
- finally {
2272
- await handle.close();
2228
+ toHTML(reports, options = {}) {
2229
+ const data = Array.isArray(reports) ? reports : [reports];
2230
+ const clean = data.filter((r) => r.verdict === "clean").length;
2231
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
2232
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
2233
+ let html = `<!DOCTYPE html>
2234
+ <html lang="en">
2235
+ <head>
2236
+ <meta charset="UTF-8">
2237
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2238
+ <title>Pompelmi Scan Results</title>
2239
+ <style>
2240
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
2241
+ .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
2242
+ .card { padding: 20px; border-radius: 8px; text-align: center; }
2243
+ .clean { background: #d4edda; color: #155724; }
2244
+ .suspicious { background: #fff3cd; color: #856404; }
2245
+ .malicious { background: #f8d7da; color: #721c24; }
2246
+ .result { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 10px 0; }
2247
+ .result h3 { margin-top: 0; }
2248
+ .badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; margin: 2px; }
2249
+ table { width: 100%; border-collapse: collapse; }
2250
+ th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
2251
+ </style>
2252
+ </head>
2253
+ <body>
2254
+ <h1>🛡️ Pompelmi Scan Results</h1>
2255
+ <div class="summary">
2256
+ <div class="card clean"><h2>${clean}</h2><p>Clean Files</p></div>
2257
+ <div class="card suspicious"><h2>${suspicious}</h2><p>Suspicious Files</p></div>
2258
+ <div class="card malicious"><h2>${malicious}</h2><p>Malicious Files</p></div>
2259
+ </div>
2260
+ <h2>Detailed Results</h2>`;
2261
+ for (const report of data) {
2262
+ const statusClass = report.verdict;
2263
+ html += `<div class="result ${statusClass}">`;
2264
+ html += `<h3>${this.escapeHtml(report.file?.name || "Unknown")}</h3>`;
2265
+ html += `<table>`;
2266
+ html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
2267
+ html += `<tr><th>Size</th><td>${this.formatBytes(report.file?.size || 0)}</td></tr>`;
2268
+ html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || "unknown")}</td></tr>`;
2269
+ html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
2270
+ html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
2271
+ html += `</table>`;
2272
+ if (options.includeDetails && report.matches.length > 0) {
2273
+ html += `<h4>Match Details:</h4><ul>`;
2274
+ for (const match of report.matches) {
2275
+ html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
2276
+ if (match.tags && match.tags.length > 0) {
2277
+ html += ` ${match.tags.map((tag) => `<span class="badge">${this.escapeHtml(tag)}</span>`).join("")}`;
2273
2278
  }
2279
+ html += `</li>`;
2274
2280
  }
2281
+ html += `</ul>`;
2275
2282
  }
2276
- // Final deletion
2277
- await fs.unlink(filePath);
2278
- this.auditLog('temp_file_deleted', {
2279
- action: 'secure_delete',
2280
- success: true,
2281
- metadata: {
2282
- path: this.sanitizeFilename(filePath),
2283
- overwritePasses: this.config.memoryProtection ? 3 : 0
2284
- }
2285
- });
2283
+ html += `</div>`;
2286
2284
  }
2287
- catch (error) {
2288
- this.auditLog('temp_file_deleted', {
2289
- action: 'secure_delete',
2290
- success: false,
2291
- sanitizedError: this.sanitizeError(error),
2292
- metadata: { path: this.sanitizeFilename(filePath) }
2293
- });
2285
+ html += `</body></html>`;
2286
+ return html;
2287
+ }
2288
+ /**
2289
+ * Export to specified format
2290
+ */
2291
+ export(reports, format, options = {}) {
2292
+ switch (format) {
2293
+ case "json":
2294
+ return this.toJSON(reports, options);
2295
+ case "csv":
2296
+ return this.toCSV(reports, options);
2297
+ case "markdown":
2298
+ return this.toMarkdown(reports, options);
2299
+ case "html":
2300
+ return this.toHTML(reports, options);
2301
+ case "sarif":
2302
+ return this.toSARIF(reports, options);
2303
+ default:
2304
+ throw new Error(`Unsupported export format: ${format}`);
2305
+ }
2306
+ }
2307
+ escapeCsv(value) {
2308
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
2309
+ return `"${value.replace(/"/g, '""')}"`;
2294
2310
  }
2311
+ return value;
2312
+ }
2313
+ escapeHtml(value) {
2314
+ return value
2315
+ .replace(/&/g, "&amp;")
2316
+ .replace(/</g, "&lt;")
2317
+ .replace(/>/g, "&gt;")
2318
+ .replace(/"/g, "&quot;")
2319
+ .replace(/'/g, "&#039;");
2320
+ }
2321
+ formatBytes(bytes) {
2322
+ if (bytes === 0)
2323
+ return "0 Bytes";
2324
+ const k = 1024;
2325
+ const sizes = ["Bytes", "KB", "MB", "GB"];
2326
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2327
+ return Math.round((bytes / k ** i) * 100) / 100 + " " + sizes[i];
2328
+ }
2329
+ }
2330
+ /**
2331
+ * Quick export helper
2332
+ */
2333
+ function exportScanResults(reports, format, options) {
2334
+ const exporter = new ScanResultExporter();
2335
+ return exporter.export(reports, format, options);
2336
+ }
2337
+
2338
+ /**
2339
+ * Threat intelligence integration and enhanced detection
2340
+ * @module utils/threat-intelligence
2341
+ */
2342
+ /**
2343
+ * Built-in threat intelligence - known malware hashes
2344
+ * In production, this would connect to real threat intel APIs
2345
+ */
2346
+ class LocalThreatIntelligence {
2347
+ constructor() {
2348
+ this.name = "Local Database";
2349
+ this.knownThreats = new Map();
2350
+ // Initialize with some example known threats (in production, load from database)
2351
+ this.initializeKnownThreats();
2352
+ }
2353
+ initializeKnownThreats() {
2354
+ // Example: EICAR test file hash
2355
+ this.knownThreats.set("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", {
2356
+ threatLevel: 100,
2357
+ category: "test-malware",
2358
+ source: "local",
2359
+ metadata: { name: "EICAR Test File" },
2360
+ });
2361
+ }
2362
+ async checkHash(hash) {
2363
+ return this.knownThreats.get(hash.toLowerCase()) || null;
2295
2364
  }
2296
2365
  /**
2297
- * Calculate secure file hash for audit purposes
2366
+ * Add a known threat to the local database
2298
2367
  */
2299
- calculateFileHash(data) {
2300
- return crypto__namespace.createHash('sha256').update(data).digest('hex');
2368
+ addThreat(hash, info) {
2369
+ this.knownThreats.set(hash.toLowerCase(), info);
2301
2370
  }
2302
2371
  /**
2303
- * Log audit event
2372
+ * Remove a threat from the local database
2304
2373
  */
2305
- auditLog(eventType, details) {
2306
- if (!this.config.enabled)
2307
- return;
2308
- const event = {
2309
- timestamp: new Date().toISOString(),
2310
- eventType,
2311
- sessionId: this.sessionId,
2312
- details: {
2313
- action: details.action || 'unknown',
2314
- success: details.success ?? true,
2315
- ...details
2316
- }
2317
- };
2318
- this.auditEvents.push(event);
2319
- // Write to audit log file if configured
2320
- if (this.config.auditLogPath) {
2321
- this.writeAuditLog(event).catch(() => {
2322
- // Silent failure to prevent error loops
2323
- });
2324
- }
2374
+ removeThreat(hash) {
2375
+ return this.knownThreats.delete(hash.toLowerCase());
2325
2376
  }
2326
2377
  /**
2327
- * Write audit event to file
2378
+ * Get all known threats
2328
2379
  */
2329
- async writeAuditLog(event) {
2330
- if (!this.config.auditLogPath)
2331
- return;
2332
- try {
2333
- const fs = await import('fs/promises');
2334
- const logLine = JSON.stringify(event) + '\\n';
2335
- await fs.appendFile(this.config.auditLogPath, logLine, { flag: 'a' });
2380
+ getAllThreats() {
2381
+ return new Map(this.knownThreats);
2382
+ }
2383
+ }
2384
+ /**
2385
+ * Threat intelligence aggregator
2386
+ */
2387
+ class ThreatIntelligenceAggregator {
2388
+ constructor(sources) {
2389
+ this.sources = [];
2390
+ if (sources) {
2391
+ this.sources = sources;
2336
2392
  }
2337
- catch {
2338
- // Silent failure
2393
+ else {
2394
+ // Default to local intelligence
2395
+ this.sources = [new LocalThreatIntelligence()];
2339
2396
  }
2340
2397
  }
2341
2398
  /**
2342
- * Generate cryptographically secure session ID
2399
+ * Add a threat intelligence source
2343
2400
  */
2344
- generateSessionId() {
2345
- return crypto__namespace.randomBytes(16).toString('hex');
2401
+ addSource(source) {
2402
+ this.sources.push(source);
2346
2403
  }
2347
2404
  /**
2348
- * Get current audit events for this session
2405
+ * Check file hash against all sources
2349
2406
  */
2350
- getAuditEvents() {
2351
- return [...this.auditEvents];
2407
+ async checkHash(hash) {
2408
+ const results = await Promise.allSettled(this.sources.map((source) => source.checkHash(hash)));
2409
+ const threats = [];
2410
+ for (const result of results) {
2411
+ if (result.status === "fulfilled" && result.value) {
2412
+ threats.push(result.value);
2413
+ }
2414
+ }
2415
+ return threats;
2352
2416
  }
2353
2417
  /**
2354
- * Clear sensitive data from memory
2418
+ * Enhance scan report with threat intelligence
2355
2419
  */
2356
- clearSensitiveData() {
2357
- if (!this.config.enabled || !this.config.memoryProtection)
2358
- return;
2359
- // Clear audit events
2360
- this.auditEvents.length = 0;
2361
- // Force garbage collection if available
2362
- if (global.gc) {
2363
- global.gc();
2364
- }
2420
+ async enhanceScanReport(content, report) {
2421
+ // Calculate file hash
2422
+ const hash = crypto.createHash("sha256").update(content).digest("hex");
2423
+ // Check threat intelligence
2424
+ const threatIntel = await this.checkHash(hash);
2425
+ // Calculate risk score
2426
+ const riskScore = this.calculateRiskScore(report, threatIntel);
2427
+ return {
2428
+ ...report,
2429
+ fileHash: hash,
2430
+ threatIntel: threatIntel.length > 0 ? threatIntel : undefined,
2431
+ riskScore,
2432
+ };
2365
2433
  }
2366
2434
  /**
2367
- * Validate transport security
2435
+ * Calculate overall risk score based on scan results and threat intel
2368
2436
  */
2369
- validateTransportSecurity(url) {
2370
- if (!this.config.enabled || !this.config.requireSecureTransport) {
2371
- return true;
2372
- }
2373
- if (!url)
2374
- return true;
2375
- try {
2376
- const urlObj = new URL(url);
2377
- const isSecure = urlObj.protocol === 'https:' || urlObj.hostname === 'localhost' || urlObj.hostname === '127.0.0.1';
2378
- if (!isSecure) {
2379
- this.auditLog('security_violation', {
2380
- action: 'insecure_transport',
2381
- success: false,
2382
- metadata: { protocol: urlObj.protocol, hostname: urlObj.hostname }
2383
- });
2384
- }
2385
- return isSecure;
2437
+ calculateRiskScore(report, threats) {
2438
+ let score = 0;
2439
+ // Base score from verdict
2440
+ switch (report.verdict) {
2441
+ case "malicious":
2442
+ score += 70;
2443
+ break;
2444
+ case "suspicious":
2445
+ score += 40;
2446
+ break;
2447
+ case "clean":
2448
+ score += 0;
2449
+ break;
2386
2450
  }
2387
- catch {
2388
- return false;
2451
+ // Add points for number of matches
2452
+ score += Math.min(report.matches.length * 5, 20);
2453
+ // Add points from threat intelligence
2454
+ if (threats.length > 0) {
2455
+ const maxThreat = Math.max(...threats.map((t) => t.threatLevel));
2456
+ score = Math.max(score, maxThreat);
2389
2457
  }
2458
+ return Math.min(score, 100);
2390
2459
  }
2391
2460
  }
2392
- // Global HIPAA compliance instance
2393
- let hipaaManager = null;
2394
2461
  /**
2395
- * Initialize HIPAA compliance
2462
+ * Create default threat intelligence aggregator
2396
2463
  */
2397
- function initializeHipaaCompliance(config) {
2398
- hipaaManager = new HipaaComplianceManager(config);
2399
- return hipaaManager;
2464
+ function createThreatIntelligence() {
2465
+ return new ThreatIntelligenceAggregator();
2400
2466
  }
2401
2467
  /**
2402
- * Get current HIPAA compliance manager
2468
+ * Helper to get file hash
2403
2469
  */
2404
- function getHipaaManager() {
2405
- return hipaaManager;
2470
+ function getFileHash(content) {
2471
+ return crypto.createHash("sha256").update(content).digest("hex");
2406
2472
  }
2473
+
2407
2474
  /**
2408
- * HIPAA-compliant error wrapper
2475
+ * Validates a File by MIME type and size (max 5 MB).
2409
2476
  */
2410
- function createHipaaError(error, context) {
2411
- const manager = getHipaaManager();
2412
- if (!manager) {
2413
- return typeof error === 'string' ? new Error(error) : error;
2477
+ function validateFile(file) {
2478
+ const maxSize = 5 * 1024 * 1024;
2479
+ const allowedTypes = ["text/plain", "application/json", "text/csv"];
2480
+ if (!allowedTypes.includes(file.type)) {
2481
+ return { valid: false, error: "Unsupported file type" };
2414
2482
  }
2415
- const sanitizedMessage = manager.sanitizeError(error);
2416
- const hipaaError = new Error(sanitizedMessage);
2417
- manager.auditLog('error_occurred', {
2418
- action: context || 'error',
2419
- success: false,
2420
- sanitizedError: sanitizedMessage
2421
- });
2422
- return hipaaError;
2423
- }
2424
- /**
2425
- * HIPAA-compliant temporary file utilities
2426
- */
2427
- const HipaaTemp = {
2428
- createPath: (prefix) => {
2429
- const manager = getHipaaManager();
2430
- return manager ? manager.createSecureTempPath(prefix) : path__namespace.join(os__namespace.tmpdir(), `${prefix || 'pompelmi'}-${Date.now()}`);
2431
- },
2432
- cleanup: async (filePath) => {
2433
- const manager = getHipaaManager();
2434
- if (manager) {
2435
- await manager.secureFileCleanup(filePath);
2436
- }
2437
- else {
2438
- try {
2439
- const fs = await import('fs/promises');
2440
- await fs.unlink(filePath);
2441
- }
2442
- catch {
2443
- // Ignore errors
2444
- }
2445
- }
2483
+ if (file.size > maxSize) {
2484
+ return { valid: false, error: "File too large (max 5 MB)" };
2446
2485
  }
2447
- };
2486
+ return { valid: true };
2487
+ }
2488
+
2489
+ function mapMatchesToVerdict(matches = []) {
2490
+ if (!matches.length)
2491
+ return "clean";
2492
+ const malHints = ["trojan", "ransom", "worm", "spy", "rootkit", "keylog", "botnet"];
2493
+ const tagSet = new Set(matches.flatMap((m) => (m.tags ?? []).map((t) => t.toLowerCase())));
2494
+ const nameHit = (r) => malHints.some((h) => r.toLowerCase().includes(h));
2495
+ const isMal = matches.some((m) => nameHit(m.rule)) || tagSet.has("malware") || tagSet.has("critical");
2496
+ return isMal ? "malicious" : "suspicious";
2497
+ }
2448
2498
 
2449
2499
  exports.ARCHIVES = ARCHIVES;
2450
2500
  exports.BatchScanner = BatchScanner;