pompelmi 0.34.10 → 0.35.1

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 +595 -557
  7. package/dist/pompelmi.browser.cjs.map +1 -1
  8. package/dist/pompelmi.browser.esm.js +595 -557
  9. package/dist/pompelmi.browser.esm.js.map +1 -1
  10. package/dist/pompelmi.cjs +2056 -2015
  11. package/dist/pompelmi.cjs.map +1 -1
  12. package/dist/pompelmi.esm.js +2056 -2015
  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 +595 -557
  27. package/dist/pompelmi.react.cjs.map +1 -1
  28. package/dist/pompelmi.react.esm.js +595 -557
  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 +7 -7
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
- /**
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
28
  /**
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
368
- */
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
- /**
398
- * Detect obfuscated JavaScript/VBScript
29
+ * Advanced configuration system for pompelmi
30
+ * @module config
399
31
  */
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
32
  /**
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;
313
+ function getPresetConfig(preset) {
314
+ return { ...DEFAULT_CONFIG, ...CONFIG_PRESETS[preset] };
683
315
  }
684
316
 
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;
317
+ /**
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
329
+ */
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();
698
343
  }
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;
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";
725
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}`;
726
358
  }
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
- }
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;
772
365
  }
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);
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;
798
382
  }
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 },
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) },
826
400
  });
827
- out.push(rep);
828
- }
829
- return out;
830
- }
831
-
832
- /**
833
- * Validates a File by MIME type and size (max 5 MB).
834
- */
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' };
401
+ return tempPath;
840
402
  }
841
- if (file.size > maxSize) {
842
- return { valid: false, error: 'File too large (max 5 MB)' };
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");
408
+ try {
409
+ const fs = require("fs");
410
+ if (!fs.existsSync(secureTempPath)) {
411
+ fs.mkdirSync(secureTempPath, { mode: 0o700 }); // Owner read/write/execute only
412
+ }
413
+ }
414
+ catch (error) {
415
+ // Fallback to system temp
416
+ return os__namespace.tmpdir();
417
+ }
418
+ return secureTempPath;
843
419
  }
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;
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);
428
+ }
429
+ catch {
430
+ // Ignore cleanup errors
431
+ }
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);
876
447
  }
877
- res = await fetchFn(endpoint, {
878
- method: 'POST',
879
- headers: { 'Content-Type': 'application/json', ...headers },
880
- body: JSON.stringify(payload),
881
- });
448
+ await handle.sync();
882
449
  }
883
- if (!res.ok) {
884
- throw new Error(`[remote-yara] HTTP ${res.status} ${res.statusText}`);
450
+ finally {
451
+ await handle.close();
885
452
  }
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
- }));
453
+ }
454
+ }
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,
892
463
  },
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) {
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;
922
511
  try {
923
- const bytes = new Uint8Array(await file.arrayBuffer());
924
- const matches = await compiled.scan(bytes);
925
- results.push({ file, matches });
512
+ const fs = await import('fs/promises');
513
+ const logLine = JSON.stringify(event) + "\\n";
514
+ await fs.appendFile(this.config.auditLogPath, logLine, { flag: "a" });
926
515
  }
927
- catch (err) {
928
- console.warn('[remote-yara] scan error for', file.name, err);
929
- results.push({ file, matches: [], error: String(err?.message ?? err) });
516
+ catch {
517
+ // Silent failure
930
518
  }
931
519
  }
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;
976
- }
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;
984
- }
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 } });
1012
- }
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
- }
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 }
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,1637 @@ 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,
1331
- };
1332
- }
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) => {
1348
- try {
1349
- const task = tasks[index];
1350
- const report = await scanBytes(task.content, {
1351
- ...this.options,
1352
- ctx: task.context,
1353
- });
1354
- results[index] = report;
1355
- successCount++;
1356
- completedCount++;
1357
- if (this.options.onProgress) {
1358
- this.options.onProgress(completedCount, tasks.length, report);
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
+ }
1359
1016
  }
1360
1017
  }
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;
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
+ }
1371
1035
  }
1372
- results[index] = null;
1373
1036
  }
1037
+ return all;
1374
1038
  };
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);
1386
- });
1039
+ }
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);
1387
1049
  }
1388
- // Wait for at least one task to complete before continuing
1389
- if (processingQueue.length >= concurrency) {
1390
- await Promise.race(processingQueue);
1050
+ catch {
1051
+ // ignore individual scanner failures
1391
1052
  }
1392
1053
  }
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,
1402
- };
1054
+ return all;
1055
+ };
1056
+ }
1057
+ function createPresetScanner(preset, opts = {}) {
1058
+ const baseScanners = [CommonHeuristicsScanner];
1059
+ const dynamicScannerPromises = [];
1060
+ // Add decompilation scanners based on preset
1061
+ if (preset === "decompilation-basic" ||
1062
+ preset === "decompilation-deep" ||
1063
+ preset === "malware-analysis" ||
1064
+ opts.enableDecompilation) {
1065
+ const depth = preset === "decompilation-deep" || preset === "malware-analysis"
1066
+ ? "deep"
1067
+ : preset === "decompilation-basic"
1068
+ ? "basic"
1069
+ : opts.decompilationDepth || "basic";
1070
+ let importModule;
1071
+ try {
1072
+ // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
1073
+ importModule = new Function("specifier", "return import(specifier)");
1074
+ }
1075
+ catch {
1076
+ importModule = undefined;
1077
+ }
1078
+ if (importModule &&
1079
+ (!opts.decompilationEngine ||
1080
+ opts.decompilationEngine === "binaryninja-hlil" ||
1081
+ opts.decompilationEngine === "both")) {
1082
+ dynamicScannerPromises.push(importModule("@pompelmi/engine-binaryninja")
1083
+ .then((mod) => mod.createBinaryNinjaScanner({
1084
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
1085
+ depth,
1086
+ pythonPath: opts.pythonPath,
1087
+ binaryNinjaPath: opts.binaryNinjaPath,
1088
+ }))
1089
+ .catch(() => null));
1090
+ }
1091
+ if (importModule &&
1092
+ (!opts.decompilationEngine ||
1093
+ opts.decompilationEngine === "ghidra-pcode" ||
1094
+ opts.decompilationEngine === "both")) {
1095
+ dynamicScannerPromises.push(importModule("@pompelmi/engine-ghidra")
1096
+ .then((mod) => mod.createGhidraScanner({
1097
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
1098
+ depth,
1099
+ ghidraPath: opts.ghidraPath,
1100
+ analyzeHeadless: opts.analyzeHeadless,
1101
+ }))
1102
+ .catch(() => null));
1103
+ }
1104
+ }
1105
+ let composedScannerPromise;
1106
+ const getComposedScanner = async () => {
1107
+ composedScannerPromise ?? (composedScannerPromise = Promise.all(dynamicScannerPromises).then((dynamicScanners) => composeScanners(...baseScanners, ...dynamicScanners.filter((scanner) => scanner !== null))));
1108
+ return composedScannerPromise;
1109
+ };
1110
+ return async (input, ctx) => {
1111
+ const scanner = await getComposedScanner();
1112
+ return scanner(input, ctx);
1113
+ };
1114
+ }
1115
+
1116
+ /**
1117
+ * Advanced threat detection utilities
1118
+ * @module utils/advanced-detection
1119
+ */
1120
+ /**
1121
+ * Enhanced polyglot file detection
1122
+ * Detects files that can be interpreted as multiple formats
1123
+ */
1124
+ function detectPolyglot(bytes) {
1125
+ const matches = [];
1126
+ // Check for PDF/ZIP polyglot
1127
+ if (isPDFZipPolyglot(bytes)) {
1128
+ matches.push({
1129
+ rule: "polyglot_pdf_zip",
1130
+ severity: "high",
1131
+ meta: { description: "File can be interpreted as both PDF and ZIP" },
1132
+ });
1403
1133
  }
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);
1134
+ // Check for image/script polyglot
1135
+ if (isImageScriptPolyglot(bytes)) {
1136
+ matches.push({
1137
+ rule: "polyglot_image_script",
1138
+ severity: "high",
1139
+ meta: { description: "Image file contains executable script content" },
1140
+ });
1417
1141
  }
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,
1142
+ // Check for GIFAR (GIF/JAR polyglot)
1143
+ if (isGIFAR(bytes)) {
1144
+ matches.push({
1145
+ rule: "polyglot_gifar",
1146
+ severity: "critical",
1147
+ meta: { description: "GIF file contains Java archive" },
1148
+ });
1149
+ }
1150
+ return matches;
1151
+ }
1152
+ /**
1153
+ * Detect obfuscated JavaScript/VBScript
1154
+ */
1155
+ function detectObfuscatedScripts(bytes) {
1156
+ const matches = [];
1157
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
1158
+ // Check for common obfuscation patterns
1159
+ const obfuscationPatterns = [
1160
+ /eval\s*\(\s*unescape\s*\(/gi,
1161
+ /eval\s*\(\s*atob\s*\(/gi,
1162
+ /String\.fromCharCode\s*\(\s*\d+(?:\s*,\s*\d+){10,}/gi,
1163
+ /[a-z0-9]{100,}/gi, // Long encoded strings
1164
+ /\\x[0-9a-f]{2}/gi, // Hex escapes
1165
+ ];
1166
+ for (const pattern of obfuscationPatterns) {
1167
+ if (pattern.test(text)) {
1168
+ matches.push({
1169
+ rule: "obfuscated_script",
1170
+ severity: "medium",
1171
+ meta: {
1172
+ description: "Detected obfuscated script content",
1173
+ pattern: pattern.source,
1434
1174
  },
1435
- };
1436
- }));
1437
- return this.scanBatch(tasks);
1175
+ });
1176
+ break;
1177
+ }
1438
1178
  }
1179
+ return matches;
1439
1180
  }
1440
1181
  /**
1441
- * Quick helper for batch scanning with default options
1182
+ * Enhanced nested archive detection with depth limits
1442
1183
  */
1443
- async function batchScan(tasks, options) {
1444
- const scanner = new BatchScanner(options);
1445
- return scanner.scanBatch(tasks);
1184
+ function analyzeNestedArchives(bytes, maxDepth = 10) {
1185
+ let depth = 0;
1186
+ let currentBytes = bytes;
1187
+ while (depth < maxDepth) {
1188
+ if (isArchive(currentBytes)) {
1189
+ depth++;
1190
+ {
1191
+ break;
1192
+ }
1193
+ }
1194
+ else {
1195
+ break;
1196
+ }
1197
+ }
1198
+ return {
1199
+ depth,
1200
+ hasExcessiveNesting: depth >= 5,
1201
+ };
1202
+ }
1203
+ // Helper functions
1204
+ function isPDFZipPolyglot(bytes) {
1205
+ if (bytes.length < 8)
1206
+ return false;
1207
+ // Check for PDF signature
1208
+ const hasPDF = bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46;
1209
+ // Check for ZIP signature anywhere in the file
1210
+ let hasZIP = false;
1211
+ for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
1212
+ if (bytes[i] === 0x50 &&
1213
+ bytes[i + 1] === 0x4b &&
1214
+ bytes[i + 2] === 0x03 &&
1215
+ bytes[i + 3] === 0x04) {
1216
+ hasZIP = true;
1217
+ break;
1218
+ }
1219
+ }
1220
+ return hasPDF && hasZIP;
1221
+ }
1222
+ function isImageScriptPolyglot(bytes) {
1223
+ if (bytes.length < 100)
1224
+ return false;
1225
+ // Check for image signatures
1226
+ const isImage = (bytes[0] === 0xff && bytes[1] === 0xd8) || // JPEG
1227
+ (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) || // PNG
1228
+ (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46); // GIF
1229
+ if (!isImage)
1230
+ return false;
1231
+ // Check for script content
1232
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1233
+ return /<script|javascript:|eval\(|function\s*\(/i.test(text);
1234
+ }
1235
+ function isGIFAR(bytes) {
1236
+ if (bytes.length < 100)
1237
+ return false;
1238
+ // Check for GIF signature
1239
+ const isGIF = bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46;
1240
+ // Check for ZIP/JAR signature
1241
+ let hasZIP = false;
1242
+ for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
1243
+ if (bytes[i] === 0x50 &&
1244
+ bytes[i + 1] === 0x4b &&
1245
+ bytes[i + 2] === 0x03 &&
1246
+ bytes[i + 3] === 0x04) {
1247
+ hasZIP = true;
1248
+ break;
1249
+ }
1250
+ }
1251
+ return isGIF && hasZIP;
1252
+ }
1253
+ function isArchive(bytes) {
1254
+ if (bytes.length < 4)
1255
+ return false;
1256
+ return (
1257
+ // ZIP
1258
+ (bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) ||
1259
+ // RAR
1260
+ (bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
1261
+ // 7z
1262
+ (bytes[0] === 0x37 && bytes[1] === 0x7a && bytes[2] === 0xbc && bytes[3] === 0xaf) ||
1263
+ // tar.gz
1264
+ (bytes[0] === 0x1f && bytes[1] === 0x8b));
1446
1265
  }
1447
1266
 
1448
1267
  /**
1449
- * Threat intelligence integration and enhanced detection
1450
- * @module utils/threat-intelligence
1268
+ * Cache management system for scan results
1269
+ * @module utils/cache-manager
1451
1270
  */
1452
1271
  /**
1453
- * Built-in threat intelligence - known malware hashes
1454
- * In production, this would connect to real threat intel APIs
1272
+ * LRU cache for scan results with TTL support
1455
1273
  */
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
- });
1274
+ class ScanCacheManager {
1275
+ constructor(options = {}) {
1276
+ this.cache = new Map();
1277
+ // Statistics
1278
+ this.stats = {
1279
+ hits: 0,
1280
+ misses: 0,
1281
+ evictions: 0,
1282
+ };
1283
+ this.maxSize = options.maxSize ?? 1000;
1284
+ this.ttl = options.ttl ?? 3600000; // 1 hour default
1285
+ this.enableLRU = options.enableLRU ?? true;
1286
+ this.enableStats = options.enableStats ?? false;
1471
1287
  }
1472
- async checkHash(hash) {
1473
- return this.knownThreats.get(hash.toLowerCase()) || null;
1288
+ /**
1289
+ * Generate cache key from file content
1290
+ */
1291
+ generateKey(content, preset) {
1292
+ const hash = crypto.createHash("sha256")
1293
+ .update(content)
1294
+ .update(preset || "default")
1295
+ .digest("hex");
1296
+ return hash;
1474
1297
  }
1475
1298
  /**
1476
- * Add a known threat to the local database
1299
+ * Check if cache entry is still valid
1477
1300
  */
1478
- addThreat(hash, info) {
1479
- this.knownThreats.set(hash.toLowerCase(), info);
1301
+ isValid(entry) {
1302
+ return Date.now() - entry.timestamp < this.ttl;
1480
1303
  }
1481
1304
  /**
1482
- * Remove a threat from the local database
1305
+ * Evict oldest or least-used entry when cache is full
1483
1306
  */
1484
- removeThreat(hash) {
1485
- return this.knownThreats.delete(hash.toLowerCase());
1307
+ evict() {
1308
+ if (this.cache.size === 0)
1309
+ return;
1310
+ let targetKey = null;
1311
+ let oldestTime = Infinity;
1312
+ let lowestAccess = Infinity;
1313
+ for (const [key, entry] of this.cache.entries()) {
1314
+ if (this.enableLRU) {
1315
+ // LRU: evict least recently used
1316
+ if (entry.timestamp < oldestTime) {
1317
+ oldestTime = entry.timestamp;
1318
+ targetKey = key;
1319
+ }
1320
+ }
1321
+ else {
1322
+ // LFU: evict least frequently used
1323
+ if (entry.accessCount < lowestAccess) {
1324
+ lowestAccess = entry.accessCount;
1325
+ targetKey = key;
1326
+ }
1327
+ }
1328
+ }
1329
+ if (targetKey) {
1330
+ this.cache.delete(targetKey);
1331
+ if (this.enableStats)
1332
+ this.stats.evictions++;
1333
+ }
1486
1334
  }
1487
1335
  /**
1488
- * Get all known threats
1336
+ * Store scan result in cache
1489
1337
  */
1490
- getAllThreats() {
1491
- return new Map(this.knownThreats);
1338
+ set(content, report, preset) {
1339
+ const key = this.generateKey(content, preset);
1340
+ // Evict if necessary
1341
+ if (this.cache.size >= this.maxSize) {
1342
+ this.evict();
1343
+ }
1344
+ this.cache.set(key, {
1345
+ report,
1346
+ timestamp: Date.now(),
1347
+ accessCount: 0,
1348
+ });
1492
1349
  }
1493
- }
1494
- /**
1495
- * Threat intelligence aggregator
1496
- */
1497
- class ThreatIntelligenceAggregator {
1498
- constructor(sources) {
1499
- this.sources = [];
1500
- if (sources) {
1501
- this.sources = sources;
1350
+ /**
1351
+ * Retrieve scan result from cache
1352
+ */
1353
+ get(content, preset) {
1354
+ const key = this.generateKey(content, preset);
1355
+ const entry = this.cache.get(key);
1356
+ if (!entry) {
1357
+ if (this.enableStats)
1358
+ this.stats.misses++;
1359
+ return null;
1502
1360
  }
1503
- else {
1504
- // Default to local intelligence
1505
- this.sources = [new LocalThreatIntelligence()];
1361
+ if (!this.isValid(entry)) {
1362
+ this.cache.delete(key);
1363
+ if (this.enableStats)
1364
+ this.stats.misses++;
1365
+ return null;
1506
1366
  }
1367
+ // Update access tracking
1368
+ entry.accessCount++;
1369
+ entry.timestamp = Date.now(); // Update for LRU
1370
+ if (this.enableStats)
1371
+ this.stats.hits++;
1372
+ return entry.report;
1507
1373
  }
1508
1374
  /**
1509
- * Add a threat intelligence source
1375
+ * Check if result exists in cache
1510
1376
  */
1511
- addSource(source) {
1512
- this.sources.push(source);
1377
+ has(content, preset) {
1378
+ const key = this.generateKey(content, preset);
1379
+ const entry = this.cache.get(key);
1380
+ return entry !== undefined && this.isValid(entry);
1513
1381
  }
1514
1382
  /**
1515
- * Check file hash against all sources
1383
+ * Clear entire cache
1516
1384
  */
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);
1385
+ clear() {
1386
+ this.cache.clear();
1387
+ if (this.enableStats) {
1388
+ this.stats.hits = 0;
1389
+ this.stats.misses = 0;
1390
+ this.stats.evictions = 0;
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Remove expired entries
1395
+ */
1396
+ prune() {
1397
+ let removed = 0;
1398
+ for (const [key, entry] of this.cache.entries()) {
1399
+ if (!this.isValid(entry)) {
1400
+ this.cache.delete(key);
1401
+ removed++;
1523
1402
  }
1524
1403
  }
1525
- return threats;
1404
+ return removed;
1526
1405
  }
1527
1406
  /**
1528
- * Enhance scan report with threat intelligence
1407
+ * Get cache statistics
1529
1408
  */
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);
1409
+ getStats() {
1410
+ const total = this.stats.hits + this.stats.misses;
1411
+ const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
1537
1412
  return {
1538
- ...report,
1539
- fileHash: hash,
1540
- threatIntel: threatIntel.length > 0 ? threatIntel : undefined,
1541
- riskScore,
1413
+ hits: this.stats.hits,
1414
+ misses: this.stats.misses,
1415
+ size: this.cache.size,
1416
+ hitRate,
1417
+ evictions: this.stats.evictions,
1542
1418
  };
1543
1419
  }
1544
1420
  /**
1545
- * Calculate overall risk score based on scan results and threat intel
1421
+ * Get current cache size
1546
1422
  */
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);
1423
+ get size() {
1424
+ return this.cache.size;
1569
1425
  }
1570
1426
  }
1427
+ // Export singleton instance for convenience
1428
+ let defaultCache = null;
1571
1429
  /**
1572
- * Create default threat intelligence aggregator
1430
+ * Get or create the default cache instance
1573
1431
  */
1574
- function createThreatIntelligence() {
1575
- return new ThreatIntelligenceAggregator();
1432
+ function getDefaultCache(options) {
1433
+ if (!defaultCache) {
1434
+ defaultCache = new ScanCacheManager(options);
1435
+ }
1436
+ return defaultCache;
1576
1437
  }
1577
1438
  /**
1578
- * Helper to get file hash
1439
+ * Reset the default cache instance
1579
1440
  */
1580
- function getFileHash(content) {
1581
- return crypto.createHash('sha256').update(content).digest('hex');
1441
+ function resetDefaultCache() {
1442
+ defaultCache = null;
1582
1443
  }
1583
1444
 
1584
1445
  /**
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;
1446
+ * Performance monitoring utilities for pompelmi scans
1447
+ * @module utils/performance-metrics
1448
+ */
1449
+ /**
1450
+ * Track performance metrics for a scan operation
1451
+ */
1452
+ class PerformanceTracker {
1453
+ constructor() {
1454
+ this.checkpoints = new Map();
1455
+ this.startTime = Date.now();
1683
1456
  }
1684
1457
  /**
1685
- * Export to SARIF format (Static Analysis Results Interchange Format)
1686
- * Useful for CI/CD integration
1458
+ * Mark a checkpoint in the scan process
1687
1459
  */
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);
1460
+ checkpoint(name) {
1461
+ this.checkpoints.set(name, Date.now());
1733
1462
  }
1734
1463
  /**
1735
- * Export to HTML format
1464
+ * Get duration since start or since a specific checkpoint
1736
1465
  */
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>`;
1466
+ getDuration(since) {
1467
+ const now = Date.now();
1468
+ if (since && this.checkpoints.has(since)) {
1469
+ return now - (this.checkpoints.get(since) ?? now);
1793
1470
  }
1794
- html += `</body></html>`;
1795
- return html;
1471
+ return now - this.startTime;
1796
1472
  }
1797
1473
  /**
1798
- * Export to specified format
1474
+ * Generate final metrics report
1799
1475
  */
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}`);
1476
+ getMetrics(bytesScanned) {
1477
+ const totalDuration = this.getDuration();
1478
+ const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
1479
+ return {
1480
+ totalDurationMs: totalDuration,
1481
+ heuristicsDurationMs: this.checkpoints.has("heuristics_end")
1482
+ ? (this.checkpoints.get("heuristics_end") ?? 0) -
1483
+ (this.checkpoints.get("heuristics_start") ?? 0)
1484
+ : undefined,
1485
+ yaraDurationMs: this.checkpoints.has("yara_end")
1486
+ ? (this.checkpoints.get("yara_end") ?? 0) - (this.checkpoints.get("yara_start") ?? 0)
1487
+ : undefined,
1488
+ prepDurationMs: this.checkpoints.has("prep_end")
1489
+ ? (this.checkpoints.get("prep_end") ?? 0) - this.startTime
1490
+ : undefined,
1491
+ throughputBps: throughput,
1492
+ bytesScanned,
1493
+ startedAt: this.startTime,
1494
+ completedAt: Date.now(),
1495
+ };
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Aggregate statistics from multiple scan reports
1500
+ */
1501
+ function aggregateScanStats(reports) {
1502
+ let cleanCount = 0;
1503
+ let suspiciousCount = 0;
1504
+ let maliciousCount = 0;
1505
+ let totalDuration = 0;
1506
+ let totalBytes = 0;
1507
+ let validDurationCount = 0;
1508
+ for (const report of reports) {
1509
+ if (report.verdict === "clean")
1510
+ cleanCount++;
1511
+ else if (report.verdict === "suspicious")
1512
+ suspiciousCount++;
1513
+ else if (report.verdict === "malicious")
1514
+ maliciousCount++;
1515
+ if (report.durationMs !== undefined) {
1516
+ totalDuration += report.durationMs;
1517
+ validDurationCount++;
1518
+ }
1519
+ if (report.file?.size !== undefined) {
1520
+ totalBytes += report.file.size;
1814
1521
  }
1815
1522
  }
1816
- escapeCsv(value) {
1817
- if (value.includes(',') || value.includes('"') || value.includes('\n')) {
1818
- return `"${value.replace(/"/g, '""')}"`;
1523
+ const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
1524
+ const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
1525
+ return {
1526
+ totalScans: reports.length,
1527
+ cleanCount,
1528
+ suspiciousCount,
1529
+ maliciousCount,
1530
+ avgDurationMs: avgDuration,
1531
+ avgThroughputBps: avgThroughput,
1532
+ totalBytesScanned: totalBytes,
1533
+ };
1534
+ }
1535
+
1536
+ /** Mappa veloce estensione -> mime (basic) */
1537
+ function guessMimeByExt(name) {
1538
+ if (!name)
1539
+ return;
1540
+ const ext = name.toLowerCase().split(".").pop();
1541
+ switch (ext) {
1542
+ case "zip":
1543
+ return "application/zip";
1544
+ case "png":
1545
+ return "image/png";
1546
+ case "jpg":
1547
+ case "jpeg":
1548
+ return "image/jpeg";
1549
+ case "pdf":
1550
+ return "application/pdf";
1551
+ case "txt":
1552
+ return "text/plain";
1553
+ default:
1554
+ return;
1555
+ }
1556
+ }
1557
+ /** Heuristica semplice per verdetto */
1558
+ function computeVerdict(matches) {
1559
+ if (!matches.length)
1560
+ return "clean";
1561
+ // se la regola contiene 'zip_' lo marchiamo "suspicious"
1562
+ const anyHigh = matches.some((m) => (m.tags ?? []).includes("critical") || (m.tags ?? []).includes("high"));
1563
+ return anyHigh ? "malicious" : "suspicious";
1564
+ }
1565
+ /** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
1566
+ function toYaraMatches(ms) {
1567
+ return ms.map((m) => ({
1568
+ rule: m.rule,
1569
+ namespace: "heuristics",
1570
+ tags: ["heuristics"].concat(m.severity ? [m.severity] : []),
1571
+ meta: m.meta,
1572
+ }));
1573
+ }
1574
+ /** Scan di bytes (browser/node) usando preset (default: zip-basic) */
1575
+ async function scanBytes(input, opts = {}) {
1576
+ // Check cache first if enabled
1577
+ if (opts.enableCache || opts.config?.performance?.enableCache) {
1578
+ const cache = getDefaultCache(opts.config?.performance?.cacheOptions);
1579
+ const cached = cache.get(input, opts.preset);
1580
+ if (cached) {
1581
+ return cached;
1819
1582
  }
1820
- return value;
1821
1583
  }
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;');
1584
+ const perfTracker = opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking
1585
+ ? new PerformanceTracker()
1586
+ : null;
1587
+ perfTracker?.checkpoint("prep_start");
1588
+ const preset = opts.preset ?? opts.config?.defaultPreset ?? "zip-basic";
1589
+ const ctx = {
1590
+ ...opts.ctx,
1591
+ mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
1592
+ size: opts.ctx?.size ?? input.byteLength,
1593
+ };
1594
+ perfTracker?.checkpoint("prep_end");
1595
+ perfTracker?.checkpoint("heuristics_start");
1596
+ const scanFn = createPresetScanner(preset);
1597
+ const matchesH = await (typeof scanFn === "function"
1598
+ ? scanFn
1599
+ : scanFn.scan)(input, ctx);
1600
+ const allMatches = [...matchesH];
1601
+ perfTracker?.checkpoint("heuristics_end");
1602
+ // Advanced detection (enabled by default, can be overridden by config)
1603
+ const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
1604
+ if (advancedEnabled) {
1605
+ perfTracker?.checkpoint("advanced_start");
1606
+ // Detect polyglot files
1607
+ if (opts.config?.advanced?.enablePolyglotDetection !== false) {
1608
+ const polyglotMatches = detectPolyglot(input);
1609
+ allMatches.push(...polyglotMatches);
1610
+ }
1611
+ // Detect obfuscated scripts
1612
+ if (opts.config?.advanced?.enableObfuscationDetection !== false) {
1613
+ const obfuscatedMatches = detectObfuscatedScripts(input);
1614
+ allMatches.push(...obfuscatedMatches);
1615
+ }
1616
+ // Check for excessive nesting in archives
1617
+ if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
1618
+ const nestingAnalysis = analyzeNestedArchives(input);
1619
+ const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
1620
+ if (nestingAnalysis.hasExcessiveNesting || nestingAnalysis.depth > maxDepth) {
1621
+ allMatches.push({
1622
+ rule: "excessive_archive_nesting",
1623
+ severity: "high",
1624
+ meta: {
1625
+ description: "Excessive archive nesting detected",
1626
+ depth: nestingAnalysis.depth,
1627
+ maxAllowed: maxDepth,
1628
+ },
1629
+ });
1630
+ }
1631
+ }
1632
+ perfTracker?.checkpoint("advanced_end");
1829
1633
  }
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];
1634
+ const matches = toYaraMatches(allMatches);
1635
+ const verdict = computeVerdict(matches);
1636
+ perfTracker ? perfTracker.getDuration() : Date.now();
1637
+ const durationMs = perfTracker ? perfTracker.getDuration() : 0;
1638
+ const report = {
1639
+ ok: verdict === "clean",
1640
+ verdict,
1641
+ matches,
1642
+ reasons: matches.map((m) => m.rule),
1643
+ file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
1644
+ durationMs,
1645
+ engine: "heuristics",
1646
+ truncated: false,
1647
+ timedOut: false,
1648
+ };
1649
+ // Add performance metrics if tracking enabled
1650
+ if (perfTracker &&
1651
+ (opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
1652
+ report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
1653
+ }
1654
+ // Cache result if enabled
1655
+ if (opts.enableCache || opts.config?.performance?.enableCache) {
1656
+ const cache = getDefaultCache(opts.config?.performance?.cacheOptions);
1657
+ cache.set(input, report, opts.preset);
1658
+ }
1659
+ // Invoke callbacks if configured
1660
+ opts.config?.callbacks?.onScanComplete?.(report);
1661
+ return report;
1662
+ }
1663
+ /** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
1664
+ async function scanFile(filePath, opts = {}) {
1665
+ const [{ readFile, stat }, path] = await Promise.all([import('fs/promises'), import('path')]);
1666
+ const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
1667
+ const ctx = {
1668
+ filename: path.basename(filePath),
1669
+ mimeType: guessMimeByExt(filePath),
1670
+ size: st.size,
1671
+ };
1672
+ return scanBytes(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), { ...opts, ctx });
1673
+ }
1674
+ /** Scan multipli File (browser) usando scanBytes + preset di default */
1675
+ async function scanFiles(files, opts = {}) {
1676
+ const list = Array.from(files);
1677
+ const out = [];
1678
+ for (const f of list) {
1679
+ const buf = new Uint8Array(await f.arrayBuffer());
1680
+ const rep = await scanBytes(buf, {
1681
+ ...opts,
1682
+ ctx: { filename: f.name, mimeType: f.type || guessMimeByExt(f.name), size: f.size },
1683
+ });
1684
+ out.push(rep);
1837
1685
  }
1686
+ return out;
1838
1687
  }
1839
- /**
1840
- * Quick export helper
1841
- */
1842
- function exportScanResults(reports, format, options) {
1843
- const exporter = new ScanResultExporter();
1844
- return exporter.export(reports, format, options);
1688
+
1689
+ async function createRemoteEngine(opts) {
1690
+ const { endpoint, headers = {}, rulesField = "rules", fileField = "file", mode = "multipart", rulesAsBase64 = false, } = opts;
1691
+ const engine = {
1692
+ async compile(rulesSource) {
1693
+ return {
1694
+ async scan(data) {
1695
+ const fetchFn = globalThis.fetch;
1696
+ if (!fetchFn)
1697
+ throw new Error("[remote-yara] fetch non disponibile in questo ambiente");
1698
+ let res;
1699
+ if (mode === "multipart") {
1700
+ const FormDataCtor = globalThis.FormData;
1701
+ const BlobCtor = globalThis.Blob;
1702
+ if (!FormDataCtor || !BlobCtor) {
1703
+ throw new Error("[remote-yara] FormData/Blob non disponibili (usa json-base64 oppure esegui in browser)");
1704
+ }
1705
+ const form = new FormDataCtor();
1706
+ form.set(rulesField, new BlobCtor([rulesSource], { type: "text/plain" }), "rules.yar");
1707
+ form.set(fileField, new BlobCtor([data], { type: "application/octet-stream" }), "sample.bin");
1708
+ res = await fetchFn(endpoint, { method: "POST", body: form, headers });
1709
+ }
1710
+ else {
1711
+ const b64 = base64FromBytes(data);
1712
+ const payload = { [fileField]: b64 };
1713
+ if (rulesAsBase64) {
1714
+ payload["rulesB64"] = base64FromString(rulesSource);
1715
+ }
1716
+ else {
1717
+ payload[rulesField] = rulesSource;
1718
+ }
1719
+ res = await fetchFn(endpoint, {
1720
+ method: "POST",
1721
+ headers: { "Content-Type": "application/json", ...headers },
1722
+ body: JSON.stringify(payload),
1723
+ });
1724
+ }
1725
+ if (!res.ok) {
1726
+ throw new Error(`[remote-yara] HTTP ${res.status} ${res.statusText}`);
1727
+ }
1728
+ const json = await res.json().catch(() => null);
1729
+ const arr = Array.isArray(json) ? json : (json?.matches ?? []);
1730
+ return (arr ?? []).map((m) => ({
1731
+ rule: m.rule ?? m.ruleIdentifier ?? "unknown",
1732
+ tags: m.tags ?? [],
1733
+ }));
1734
+ },
1735
+ };
1736
+ },
1737
+ };
1738
+ return engine;
1739
+ }
1740
+ // Helpers
1741
+ function base64FromBytes(bytes) {
1742
+ // usa btoa se disponibile (browser); altrimenti fallback manuale
1743
+ const btoaFn = globalThis.btoa;
1744
+ let bin = "";
1745
+ for (let i = 0; i < bytes.byteLength; i++)
1746
+ bin += String.fromCharCode(bytes[i]);
1747
+ return btoaFn ? btoaFn(bin) : Buffer.from(bin, "binary").toString("base64");
1748
+ }
1749
+ function base64FromString(s) {
1750
+ const btoaFn = globalThis.btoa;
1751
+ return btoaFn ? btoaFn(s) : Buffer.from(s, "utf8").toString("base64");
1845
1752
  }
1846
1753
 
1754
+ // src/scan/remote.ts
1847
1755
  /**
1848
- * Advanced configuration system for pompelmi
1849
- * @module config
1850
- */
1851
- /**
1852
- * Default configuration
1756
+ * Scansiona una lista di File nel browser usando il motore remoto via HTTP.
1757
+ * Non richiede WASM né dipendenze native sul client.
1853
1758
  */
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
- },
1759
+ async function scanFilesWithRemoteYara(files, rulesSource, remote) {
1760
+ const engine = await createRemoteEngine(remote);
1761
+ const compiled = await engine.compile(rulesSource);
1762
+ const results = [];
1763
+ for (const file of files) {
1764
+ try {
1765
+ const bytes = new Uint8Array(await file.arrayBuffer());
1766
+ const matches = await compiled.scan(bytes);
1767
+ results.push({ file, matches });
1768
+ }
1769
+ catch (err) {
1770
+ console.warn("[remote-yara] scan error for", file.name, err);
1771
+ results.push({ file, matches: [], error: String(err?.message ?? err) });
1772
+ }
1773
+ }
1774
+ return results;
1775
+ }
1776
+
1777
+ const SIG_CEN = 0x02014b50;
1778
+ const DEFAULTS = {
1779
+ maxEntries: 1000,
1780
+ maxTotalUncompressedBytes: 500 * 1024 * 1024,
1781
+ maxEntryNameLength: 255,
1782
+ maxCompressionRatio: 1000,
1783
+ eocdSearchWindow: 70000,
1885
1784
  };
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,
1785
+ function r16(buf, off) {
1786
+ return buf.readUInt16LE(off);
1787
+ }
1788
+ function r32(buf, off) {
1789
+ return buf.readUInt32LE(off);
1790
+ }
1791
+ function isZipLike(buf) {
1792
+ // local file header at start is common
1793
+ return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
1794
+ }
1795
+ function lastIndexOfEOCD(buf, window) {
1796
+ const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
1797
+ const start = Math.max(0, buf.length - window);
1798
+ const idx = buf.lastIndexOf(sig, Math.min(buf.length - sig.length, buf.length - 1));
1799
+ return idx >= start ? idx : -1;
1800
+ }
1801
+ function hasTraversal(name) {
1802
+ return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
1803
+ }
1804
+ function createZipBombGuard(opts = {}) {
1805
+ const cfg = { ...DEFAULTS, ...opts };
1806
+ return {
1807
+ async scan(input) {
1808
+ const buf = Buffer.from(input);
1809
+ const matches = [];
1810
+ if (!isZipLike(buf))
1811
+ return matches;
1812
+ // Find EOCD near the end
1813
+ const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
1814
+ if (eocdPos < 0 || eocdPos + 22 > buf.length) {
1815
+ // ZIP but no EOCD — malformed or polyglot → suspicious
1816
+ matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
1817
+ return matches;
1818
+ }
1819
+ const totalEntries = r16(buf, eocdPos + 10);
1820
+ const cdSize = r32(buf, eocdPos + 12);
1821
+ const cdOffset = r32(buf, eocdPos + 16);
1822
+ // Bounds check
1823
+ if (cdOffset + cdSize > buf.length) {
1824
+ matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
1825
+ return matches;
1826
+ }
1827
+ // Iterate central directory entries
1828
+ let ptr = cdOffset;
1829
+ let seen = 0;
1830
+ let sumComp = 0;
1831
+ let sumUnc = 0;
1832
+ while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
1833
+ const sig = r32(buf, ptr);
1834
+ if (sig !== SIG_CEN)
1835
+ break; // stop if structure breaks
1836
+ const compSize = r32(buf, ptr + 20);
1837
+ const uncSize = r32(buf, ptr + 24);
1838
+ const fnLen = r16(buf, ptr + 28);
1839
+ const exLen = r16(buf, ptr + 30);
1840
+ const cmLen = r16(buf, ptr + 32);
1841
+ const nameStart = ptr + 46;
1842
+ const nameEnd = nameStart + fnLen;
1843
+ if (nameEnd > buf.length)
1844
+ break;
1845
+ const name = buf.toString("utf8", nameStart, nameEnd);
1846
+ sumComp += compSize;
1847
+ sumUnc += uncSize;
1848
+ seen++;
1849
+ if (name.length > cfg.maxEntryNameLength) {
1850
+ matches.push({
1851
+ rule: "zip_entry_name_too_long",
1852
+ severity: "medium",
1853
+ meta: { name, length: name.length },
1854
+ });
1855
+ }
1856
+ if (hasTraversal(name)) {
1857
+ matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
1858
+ }
1859
+ // move to next entry
1860
+ ptr = nameEnd + exLen + cmLen;
1861
+ }
1862
+ if (seen !== totalEntries) {
1863
+ // central dir truncated/odd, still report what we found
1864
+ matches.push({
1865
+ rule: "zip_cd_truncated",
1866
+ severity: "medium",
1867
+ meta: { seen, totalEntries },
1868
+ });
1869
+ }
1870
+ // Heuristics thresholds
1871
+ if (seen > cfg.maxEntries) {
1872
+ matches.push({
1873
+ rule: "zip_too_many_entries",
1874
+ severity: "medium",
1875
+ meta: { seen, limit: cfg.maxEntries },
1876
+ });
1877
+ }
1878
+ if (sumUnc > cfg.maxTotalUncompressedBytes) {
1879
+ matches.push({
1880
+ rule: "zip_total_uncompressed_too_large",
1881
+ severity: "medium",
1882
+ meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
1883
+ });
1884
+ }
1885
+ if (sumComp === 0 && sumUnc > 0) {
1886
+ matches.push({
1887
+ rule: "zip_suspicious_ratio",
1888
+ severity: "medium",
1889
+ meta: { ratio: Infinity },
1890
+ });
1891
+ }
1892
+ else if (sumComp > 0) {
1893
+ const ratio = sumUnc / Math.max(1, sumComp);
1894
+ if (ratio >= cfg.maxCompressionRatio) {
1895
+ matches.push({
1896
+ rule: "zip_suspicious_ratio",
1897
+ severity: "medium",
1898
+ meta: { ratio, limit: cfg.maxCompressionRatio },
1899
+ });
1900
+ }
1901
+ }
1902
+ return matches;
1930
1903
  },
1904
+ };
1905
+ }
1906
+
1907
+ /** Decompilation-specific types for Pompelmi */
1908
+ const SUSPICIOUS_PATTERNS = [
1909
+ {
1910
+ name: "syscall_direct",
1911
+ description: "Direct system call without library wrapper",
1912
+ severity: "medium",
1913
+ pattern: /syscall|sysenter|int\s+0x80/i,
1931
1914
  },
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
- },
1915
+ {
1916
+ name: "process_injection",
1917
+ description: "Process injection techniques",
1918
+ severity: "high",
1919
+ pattern: /CreateRemoteThread|WriteProcessMemory|VirtualAllocEx/i,
1963
1920
  },
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
- },
1921
+ {
1922
+ name: "anti_debug",
1923
+ description: "Anti-debugging techniques",
1924
+ severity: "medium",
1925
+ pattern: /IsDebuggerPresent|CheckRemoteDebuggerPresent|OutputDebugString/i,
1982
1926
  },
1983
- };
1927
+ {
1928
+ name: "obfuscation_xor",
1929
+ description: "XOR-based obfuscation pattern",
1930
+ severity: "medium",
1931
+ pattern: /xor.*0x[0-9a-f]+.*xor/i,
1932
+ },
1933
+ {
1934
+ name: "crypto_constants",
1935
+ description: "Cryptographic constants",
1936
+ severity: "low",
1937
+ pattern: /0x67452301|0xefcdab89|0x98badcfe|0x10325476/i,
1938
+ },
1939
+ ];
1940
+
1984
1941
  /**
1985
- * Configuration manager
1942
+ * Batch scanning with concurrency control
1943
+ * @module utils/batch-scanner
1986
1944
  */
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;
1945
+ /**
1946
+ * Batch file scanner with concurrency control and progress tracking
1947
+ */
1948
+ class BatchScanner {
1949
+ constructor(options = {}) {
1950
+ this.options = {
1951
+ concurrency: 5,
1952
+ continueOnError: true,
1953
+ ...options,
1954
+ };
2027
1955
  }
2028
1956
  /**
2029
- * Validate configuration
1957
+ * Scan multiple files with controlled concurrency
2030
1958
  */
2031
- validate() {
1959
+ async scanBatch(tasks) {
1960
+ const startTime = Date.now();
1961
+ const results = new Array(tasks.length);
2032
1962
  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');
1963
+ let successCount = 0;
1964
+ let errorCount = 0;
1965
+ let completedCount = 0;
1966
+ const concurrency = this.options.concurrency ?? 5;
1967
+ // Process tasks in chunks with controlled concurrency
1968
+ const processingQueue = [];
1969
+ let currentIndex = 0;
1970
+ const processTask = async (index) => {
1971
+ try {
1972
+ const task = tasks[index];
1973
+ const report = await scanBytes(task.content, {
1974
+ ...this.options,
1975
+ ctx: task.context,
1976
+ });
1977
+ results[index] = report;
1978
+ successCount++;
1979
+ completedCount++;
1980
+ if (this.options.onProgress) {
1981
+ this.options.onProgress(completedCount, tasks.length, report);
1982
+ }
2046
1983
  }
2047
- }
2048
- if (this.config.security?.scanTimeout !== undefined) {
2049
- if (this.config.security.scanTimeout < 1000) {
2050
- errors.push('scanTimeout must be at least 1000ms');
1984
+ catch (error) {
1985
+ errorCount++;
1986
+ completedCount++;
1987
+ const err = error instanceof Error ? error : new Error(String(error));
1988
+ if (this.options.onError) {
1989
+ this.options.onError(err, index);
1990
+ }
1991
+ errors.push({ index, error: err });
1992
+ if (!this.options.continueOnError) {
1993
+ throw err;
1994
+ }
1995
+ results[index] = null;
2051
1996
  }
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');
1997
+ };
1998
+ // Start initial batch of concurrent tasks
1999
+ while (currentIndex < tasks.length) {
2000
+ while (processingQueue.length < concurrency && currentIndex < tasks.length) {
2001
+ const promise = processTask(currentIndex);
2002
+ processingQueue.push(promise);
2003
+ currentIndex++;
2004
+ // Remove completed promises from queue
2005
+ promise
2006
+ .finally(() => {
2007
+ const idx = processingQueue.indexOf(promise);
2008
+ if (idx > -1)
2009
+ processingQueue.splice(idx, 1);
2010
+ })
2011
+ .catch(() => {
2012
+ // Rejections are handled by the main queue waits; swallow the cleanup chain.
2013
+ });
2057
2014
  }
2058
- if (this.config.advanced.maxArchiveDepth > 20) {
2059
- errors.push('maxArchiveDepth should not exceed 20');
2015
+ // Wait for at least one task to complete before continuing
2016
+ if (processingQueue.length >= concurrency) {
2017
+ await Promise.race(processingQueue);
2060
2018
  }
2061
2019
  }
2020
+ // Wait for all remaining tasks
2021
+ await Promise.all(processingQueue);
2022
+ const totalDurationMs = Date.now() - startTime;
2062
2023
  return {
2063
- valid: errors.length === 0,
2024
+ reports: results,
2025
+ successCount,
2026
+ errorCount,
2027
+ totalDurationMs,
2064
2028
  errors,
2065
2029
  };
2066
2030
  }
2067
2031
  /**
2068
- * Deep merge configuration objects
2032
+ * Scan files from File objects (browser environment)
2069
2033
  */
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,
2034
+ async scanFiles(files) {
2035
+ const tasks = await Promise.all(files.map(async (file) => ({
2036
+ content: new Uint8Array(await file.arrayBuffer()),
2037
+ context: {
2038
+ filename: file.name,
2039
+ mimeType: file.type,
2040
+ size: file.size,
2101
2041
  },
2102
- };
2103
- }
2104
- /**
2105
- * Export configuration as JSON
2106
- */
2107
- toJSON() {
2108
- return JSON.stringify(this.config, null, 2);
2042
+ })));
2043
+ return this.scanBatch(tasks);
2109
2044
  }
2110
2045
  /**
2111
- * Load configuration from JSON
2046
+ * Scan files from file paths (Node.js environment)
2112
2047
  */
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
- }
2048
+ async scanFilePaths(filePaths) {
2049
+ const fs = await import('fs/promises');
2050
+ const path = await import('path');
2051
+ const tasks = await Promise.all(filePaths.map(async (filePath) => {
2052
+ const [content, stats] = await Promise.all([fs.readFile(filePath), fs.stat(filePath)]);
2053
+ return {
2054
+ content: new Uint8Array(content),
2055
+ context: {
2056
+ filename: path.basename(filePath),
2057
+ size: stats.size,
2058
+ },
2059
+ };
2060
+ }));
2061
+ return this.scanBatch(tasks);
2121
2062
  }
2122
2063
  }
2123
2064
  /**
2124
- * Create a new configuration manager
2065
+ * Quick helper for batch scanning with default options
2125
2066
  */
2126
- function createConfig(config) {
2127
- return new ConfigManager(config);
2067
+ async function batchScan(tasks, options) {
2068
+ const scanner = new BatchScanner(options);
2069
+ return scanner.scanBatch(tasks);
2128
2070
  }
2071
+
2129
2072
  /**
2130
- * Get a preset configuration
2073
+ * Export utilities for scan results
2074
+ * @module utils/export
2131
2075
  */
2132
- function getPresetConfig(preset) {
2133
- return { ...DEFAULT_CONFIG, ...CONFIG_PRESETS[preset] };
2134
- }
2135
-
2136
2076
  /**
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
2077
+ * Export scan results to various formats
2148
2078
  */
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
- }
2079
+ class ScanResultExporter {
2163
2080
  /**
2164
- * Sanitize filename to prevent PHI leakage in logs
2081
+ * Export to JSON format
2165
2082
  */
2166
- sanitizeFilename(filename) {
2167
- if (!this.config.enabled || !this.config.sanitizeFilenames || !filename) {
2168
- return filename || 'unknown';
2083
+ toJSON(reports, options = {}) {
2084
+ const data = Array.isArray(reports) ? reports : [reports];
2085
+ if (!options.includeDetails) {
2086
+ // Simplified output
2087
+ const simplified = data.map((r) => ({
2088
+ verdict: r.verdict,
2089
+ file: r.file?.name,
2090
+ matches: r.matches.length,
2091
+ durationMs: r.durationMs,
2092
+ }));
2093
+ return options.prettyPrint ? JSON.stringify(simplified, null, 2) : JSON.stringify(simplified);
2169
2094
  }
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}`;
2095
+ return options.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
2177
2096
  }
2178
2097
  /**
2179
- * Sanitize error messages to prevent PHI exposure
2098
+ * Export to CSV format
2180
2099
  */
2181
- sanitizeError(error) {
2182
- if (!this.config.enabled || !this.config.sanitizeErrors) {
2183
- return typeof error === 'string' ? error : error.message;
2100
+ toCSV(reports, options = {}) {
2101
+ const data = Array.isArray(reports) ? reports : [reports];
2102
+ const headers = [
2103
+ "filename",
2104
+ "verdict",
2105
+ "matches_count",
2106
+ "file_size",
2107
+ "mime_type",
2108
+ "duration_ms",
2109
+ "engine",
2110
+ ];
2111
+ if (options.includeDetails) {
2112
+ headers.push("reasons", "match_rules");
2184
2113
  }
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;
2114
+ const rows = data.map((report) => {
2115
+ const row = [
2116
+ this.escapeCsv(report.file?.name || "unknown"),
2117
+ report.verdict,
2118
+ report.matches.length.toString(),
2119
+ (report.file?.size || 0).toString(),
2120
+ this.escapeCsv(report.file?.mimeType || "unknown"),
2121
+ (report.durationMs || 0).toString(),
2122
+ report.engine || "unknown",
2123
+ ];
2124
+ if (options.includeDetails) {
2125
+ row.push(this.escapeCsv((report.reasons || []).join("; ")), this.escapeCsv(report.matches.map((m) => m.rule).join("; ")));
2126
+ }
2127
+ return row.join(",");
2128
+ });
2129
+ return [headers.join(","), ...rows].join("\n");
2201
2130
  }
2202
2131
  /**
2203
- * Create secure temporary file path with encryption if enabled
2132
+ * Export to Markdown format
2204
2133
  */
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)}`);
2134
+ toMarkdown(reports, options = {}) {
2135
+ const data = Array.isArray(reports) ? reports : [reports];
2136
+ let md = "# Scan Results\n\n";
2137
+ md += `**Total Scans:** ${data.length}\n\n`;
2138
+ const clean = data.filter((r) => r.verdict === "clean").length;
2139
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
2140
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
2141
+ md += "## Summary\n\n";
2142
+ md += `- ✅ Clean: ${clean}\n`;
2143
+ md += `- ⚠️ Suspicious: ${suspicious}\n`;
2144
+ md += `- ❌ Malicious: ${malicious}\n\n`;
2145
+ md += "## Detailed Results\n\n";
2146
+ for (const report of data) {
2147
+ const icon = report.verdict === "clean" ? "✅" : report.verdict === "suspicious" ? "⚠️" : "❌";
2148
+ md += `### ${icon} ${report.file?.name || "Unknown"}\n\n`;
2149
+ md += `- **Verdict:** ${report.verdict}\n`;
2150
+ md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
2151
+ md += `- **MIME Type:** ${report.file?.mimeType || "unknown"}\n`;
2152
+ md += `- **Duration:** ${report.durationMs || 0}ms\n`;
2153
+ md += `- **Matches:** ${report.matches.length}\n`;
2154
+ if (options.includeDetails && report.matches.length > 0) {
2155
+ md += "\n**Match Details:**\n";
2156
+ for (const match of report.matches) {
2157
+ md += `- ${match.rule}`;
2158
+ if (match.tags && match.tags.length > 0) {
2159
+ md += ` (${match.tags.join(", ")})`;
2160
+ }
2161
+ md += "\n";
2162
+ }
2163
+ }
2164
+ md += "\n";
2208
2165
  }
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;
2166
+ return md;
2221
2167
  }
2222
2168
  /**
2223
- * Get or create secure temporary directory with restricted permissions
2169
+ * Export to SARIF format (Static Analysis Results Interchange Format)
2170
+ * Useful for CI/CD integration
2224
2171
  */
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;
2172
+ toSARIF(reports, options = {}) {
2173
+ const data = Array.isArray(reports) ? reports : [reports];
2174
+ const results = data.flatMap((report) => {
2175
+ if (report.verdict === "clean")
2176
+ return [];
2177
+ return report.matches.map((match) => ({
2178
+ ruleId: match.rule,
2179
+ level: report.verdict === "malicious" ? "error" : "warning",
2180
+ message: {
2181
+ text: `${match.rule} detected in ${report.file?.name || "unknown file"}`,
2182
+ },
2183
+ locations: [
2184
+ {
2185
+ physicalLocation: {
2186
+ artifactLocation: {
2187
+ uri: report.file?.name || "unknown",
2188
+ },
2189
+ },
2190
+ },
2191
+ ],
2192
+ properties: {
2193
+ tags: match.tags,
2194
+ metadata: match.meta,
2195
+ },
2196
+ }));
2197
+ });
2198
+ const sarif = {
2199
+ version: "2.1.0",
2200
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
2201
+ runs: [
2202
+ {
2203
+ tool: {
2204
+ driver: {
2205
+ name: "Pompelmi",
2206
+ version: "0.29.0",
2207
+ informationUri: "https://pompelmi.github.io/pompelmi/",
2208
+ },
2209
+ },
2210
+ results,
2211
+ },
2212
+ ],
2213
+ };
2214
+ return options.prettyPrint ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
2238
2215
  }
2239
2216
  /**
2240
- * Secure file cleanup with multiple overwrite passes
2217
+ * Export to HTML format
2241
2218
  */
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();
2219
+ toHTML(reports, options = {}) {
2220
+ const data = Array.isArray(reports) ? reports : [reports];
2221
+ const clean = data.filter((r) => r.verdict === "clean").length;
2222
+ const suspicious = data.filter((r) => r.verdict === "suspicious").length;
2223
+ const malicious = data.filter((r) => r.verdict === "malicious").length;
2224
+ let html = `<!DOCTYPE html>
2225
+ <html lang="en">
2226
+ <head>
2227
+ <meta charset="UTF-8">
2228
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2229
+ <title>Pompelmi Scan Results</title>
2230
+ <style>
2231
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
2232
+ .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
2233
+ .card { padding: 20px; border-radius: 8px; text-align: center; }
2234
+ .clean { background: #d4edda; color: #155724; }
2235
+ .suspicious { background: #fff3cd; color: #856404; }
2236
+ .malicious { background: #f8d7da; color: #721c24; }
2237
+ .result { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 10px 0; }
2238
+ .result h3 { margin-top: 0; }
2239
+ .badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; margin: 2px; }
2240
+ table { width: 100%; border-collapse: collapse; }
2241
+ th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
2242
+ </style>
2243
+ </head>
2244
+ <body>
2245
+ <h1>🛡️ Pompelmi Scan Results</h1>
2246
+ <div class="summary">
2247
+ <div class="card clean"><h2>${clean}</h2><p>Clean Files</p></div>
2248
+ <div class="card suspicious"><h2>${suspicious}</h2><p>Suspicious Files</p></div>
2249
+ <div class="card malicious"><h2>${malicious}</h2><p>Malicious Files</p></div>
2250
+ </div>
2251
+ <h2>Detailed Results</h2>`;
2252
+ for (const report of data) {
2253
+ const statusClass = report.verdict;
2254
+ html += `<div class="result ${statusClass}">`;
2255
+ html += `<h3>${this.escapeHtml(report.file?.name || "Unknown")}</h3>`;
2256
+ html += `<table>`;
2257
+ html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
2258
+ html += `<tr><th>Size</th><td>${this.formatBytes(report.file?.size || 0)}</td></tr>`;
2259
+ html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || "unknown")}</td></tr>`;
2260
+ html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
2261
+ html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
2262
+ html += `</table>`;
2263
+ if (options.includeDetails && report.matches.length > 0) {
2264
+ html += `<h4>Match Details:</h4><ul>`;
2265
+ for (const match of report.matches) {
2266
+ html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
2267
+ if (match.tags && match.tags.length > 0) {
2268
+ html += ` ${match.tags.map((tag) => `<span class="badge">${this.escapeHtml(tag)}</span>`).join("")}`;
2273
2269
  }
2270
+ html += `</li>`;
2274
2271
  }
2272
+ html += `</ul>`;
2275
2273
  }
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
- });
2274
+ html += `</div>`;
2286
2275
  }
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
- });
2276
+ html += `</body></html>`;
2277
+ return html;
2278
+ }
2279
+ /**
2280
+ * Export to specified format
2281
+ */
2282
+ export(reports, format, options = {}) {
2283
+ switch (format) {
2284
+ case "json":
2285
+ return this.toJSON(reports, options);
2286
+ case "csv":
2287
+ return this.toCSV(reports, options);
2288
+ case "markdown":
2289
+ return this.toMarkdown(reports, options);
2290
+ case "html":
2291
+ return this.toHTML(reports, options);
2292
+ case "sarif":
2293
+ return this.toSARIF(reports, options);
2294
+ default:
2295
+ throw new Error(`Unsupported export format: ${format}`);
2296
+ }
2297
+ }
2298
+ escapeCsv(value) {
2299
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
2300
+ return `"${value.replace(/"/g, '""')}"`;
2294
2301
  }
2302
+ return value;
2303
+ }
2304
+ escapeHtml(value) {
2305
+ return value
2306
+ .replace(/&/g, "&amp;")
2307
+ .replace(/</g, "&lt;")
2308
+ .replace(/>/g, "&gt;")
2309
+ .replace(/"/g, "&quot;")
2310
+ .replace(/'/g, "&#039;");
2311
+ }
2312
+ formatBytes(bytes) {
2313
+ if (bytes === 0)
2314
+ return "0 Bytes";
2315
+ const k = 1024;
2316
+ const sizes = ["Bytes", "KB", "MB", "GB"];
2317
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2318
+ return Math.round((bytes / k ** i) * 100) / 100 + " " + sizes[i];
2319
+ }
2320
+ }
2321
+ /**
2322
+ * Quick export helper
2323
+ */
2324
+ function exportScanResults(reports, format, options) {
2325
+ const exporter = new ScanResultExporter();
2326
+ return exporter.export(reports, format, options);
2327
+ }
2328
+
2329
+ /**
2330
+ * Threat intelligence integration and enhanced detection
2331
+ * @module utils/threat-intelligence
2332
+ */
2333
+ /**
2334
+ * Built-in threat intelligence - known malware hashes
2335
+ * In production, this would connect to real threat intel APIs
2336
+ */
2337
+ class LocalThreatIntelligence {
2338
+ constructor() {
2339
+ this.name = "Local Database";
2340
+ this.knownThreats = new Map();
2341
+ // Initialize with some example known threats (in production, load from database)
2342
+ this.initializeKnownThreats();
2343
+ }
2344
+ initializeKnownThreats() {
2345
+ // Example: EICAR test file hash
2346
+ this.knownThreats.set("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", {
2347
+ threatLevel: 100,
2348
+ category: "test-malware",
2349
+ source: "local",
2350
+ metadata: { name: "EICAR Test File" },
2351
+ });
2352
+ }
2353
+ async checkHash(hash) {
2354
+ return this.knownThreats.get(hash.toLowerCase()) || null;
2295
2355
  }
2296
2356
  /**
2297
- * Calculate secure file hash for audit purposes
2357
+ * Add a known threat to the local database
2298
2358
  */
2299
- calculateFileHash(data) {
2300
- return crypto__namespace.createHash('sha256').update(data).digest('hex');
2359
+ addThreat(hash, info) {
2360
+ this.knownThreats.set(hash.toLowerCase(), info);
2301
2361
  }
2302
2362
  /**
2303
- * Log audit event
2363
+ * Remove a threat from the local database
2304
2364
  */
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
- }
2365
+ removeThreat(hash) {
2366
+ return this.knownThreats.delete(hash.toLowerCase());
2325
2367
  }
2326
2368
  /**
2327
- * Write audit event to file
2369
+ * Get all known threats
2328
2370
  */
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' });
2371
+ getAllThreats() {
2372
+ return new Map(this.knownThreats);
2373
+ }
2374
+ }
2375
+ /**
2376
+ * Threat intelligence aggregator
2377
+ */
2378
+ class ThreatIntelligenceAggregator {
2379
+ constructor(sources) {
2380
+ this.sources = [];
2381
+ if (sources) {
2382
+ this.sources = sources;
2336
2383
  }
2337
- catch {
2338
- // Silent failure
2384
+ else {
2385
+ // Default to local intelligence
2386
+ this.sources = [new LocalThreatIntelligence()];
2339
2387
  }
2340
2388
  }
2341
2389
  /**
2342
- * Generate cryptographically secure session ID
2390
+ * Add a threat intelligence source
2343
2391
  */
2344
- generateSessionId() {
2345
- return crypto__namespace.randomBytes(16).toString('hex');
2392
+ addSource(source) {
2393
+ this.sources.push(source);
2346
2394
  }
2347
2395
  /**
2348
- * Get current audit events for this session
2396
+ * Check file hash against all sources
2349
2397
  */
2350
- getAuditEvents() {
2351
- return [...this.auditEvents];
2398
+ async checkHash(hash) {
2399
+ const results = await Promise.allSettled(this.sources.map((source) => source.checkHash(hash)));
2400
+ const threats = [];
2401
+ for (const result of results) {
2402
+ if (result.status === "fulfilled" && result.value) {
2403
+ threats.push(result.value);
2404
+ }
2405
+ }
2406
+ return threats;
2352
2407
  }
2353
2408
  /**
2354
- * Clear sensitive data from memory
2409
+ * Enhance scan report with threat intelligence
2355
2410
  */
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
- }
2411
+ async enhanceScanReport(content, report) {
2412
+ // Calculate file hash
2413
+ const hash = crypto.createHash("sha256").update(content).digest("hex");
2414
+ // Check threat intelligence
2415
+ const threatIntel = await this.checkHash(hash);
2416
+ // Calculate risk score
2417
+ const riskScore = this.calculateRiskScore(report, threatIntel);
2418
+ return {
2419
+ ...report,
2420
+ fileHash: hash,
2421
+ threatIntel: threatIntel.length > 0 ? threatIntel : undefined,
2422
+ riskScore,
2423
+ };
2365
2424
  }
2366
2425
  /**
2367
- * Validate transport security
2426
+ * Calculate overall risk score based on scan results and threat intel
2368
2427
  */
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;
2428
+ calculateRiskScore(report, threats) {
2429
+ let score = 0;
2430
+ // Base score from verdict
2431
+ switch (report.verdict) {
2432
+ case "malicious":
2433
+ score += 70;
2434
+ break;
2435
+ case "suspicious":
2436
+ score += 40;
2437
+ break;
2438
+ case "clean":
2439
+ score += 0;
2440
+ break;
2386
2441
  }
2387
- catch {
2388
- return false;
2442
+ // Add points for number of matches
2443
+ score += Math.min(report.matches.length * 5, 20);
2444
+ // Add points from threat intelligence
2445
+ if (threats.length > 0) {
2446
+ const maxThreat = Math.max(...threats.map((t) => t.threatLevel));
2447
+ score = Math.max(score, maxThreat);
2389
2448
  }
2449
+ return Math.min(score, 100);
2390
2450
  }
2391
2451
  }
2392
- // Global HIPAA compliance instance
2393
- let hipaaManager = null;
2394
2452
  /**
2395
- * Initialize HIPAA compliance
2453
+ * Create default threat intelligence aggregator
2396
2454
  */
2397
- function initializeHipaaCompliance(config) {
2398
- hipaaManager = new HipaaComplianceManager(config);
2399
- return hipaaManager;
2455
+ function createThreatIntelligence() {
2456
+ return new ThreatIntelligenceAggregator();
2400
2457
  }
2401
2458
  /**
2402
- * Get current HIPAA compliance manager
2459
+ * Helper to get file hash
2403
2460
  */
2404
- function getHipaaManager() {
2405
- return hipaaManager;
2461
+ function getFileHash(content) {
2462
+ return crypto.createHash("sha256").update(content).digest("hex");
2406
2463
  }
2464
+
2407
2465
  /**
2408
- * HIPAA-compliant error wrapper
2466
+ * Validates a File by MIME type and size (max 5 MB).
2409
2467
  */
2410
- function createHipaaError(error, context) {
2411
- const manager = getHipaaManager();
2412
- if (!manager) {
2413
- return typeof error === 'string' ? new Error(error) : error;
2468
+ function validateFile(file) {
2469
+ const maxSize = 5 * 1024 * 1024;
2470
+ const allowedTypes = ["text/plain", "application/json", "text/csv"];
2471
+ if (!allowedTypes.includes(file.type)) {
2472
+ return { valid: false, error: "Unsupported file type" };
2414
2473
  }
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
- }
2474
+ if (file.size > maxSize) {
2475
+ return { valid: false, error: "File too large (max 5 MB)" };
2446
2476
  }
2447
- };
2477
+ return { valid: true };
2478
+ }
2479
+
2480
+ function mapMatchesToVerdict(matches = []) {
2481
+ if (!matches.length)
2482
+ return "clean";
2483
+ const malHints = ["trojan", "ransom", "worm", "spy", "rootkit", "keylog", "botnet"];
2484
+ const tagSet = new Set(matches.flatMap((m) => (m.tags ?? []).map((t) => t.toLowerCase())));
2485
+ const nameHit = (r) => malHints.some((h) => r.toLowerCase().includes(h));
2486
+ const isMal = matches.some((m) => nameHit(m.rule)) || tagSet.has("malware") || tagSet.has("critical");
2487
+ return isMal ? "malicious" : "suspicious";
2488
+ }
2448
2489
 
2449
2490
  exports.ARCHIVES = ARCHIVES;
2450
2491
  exports.BatchScanner = BatchScanner;