pompelmi 0.34.9 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +26 -14
  2. package/dist/pompelmi.audit.cjs +13 -15
  3. package/dist/pompelmi.audit.cjs.map +1 -1
  4. package/dist/pompelmi.audit.esm.js +13 -15
  5. package/dist/pompelmi.audit.esm.js.map +1 -1
  6. package/dist/pompelmi.browser.cjs +585 -534
  7. package/dist/pompelmi.browser.cjs.map +1 -1
  8. package/dist/pompelmi.browser.esm.js +585 -534
  9. package/dist/pompelmi.browser.esm.js.map +1 -1
  10. package/dist/pompelmi.cjs +2066 -2016
  11. package/dist/pompelmi.cjs.map +1 -1
  12. package/dist/pompelmi.esm.js +2066 -2016
  13. package/dist/pompelmi.esm.js.map +1 -1
  14. package/dist/pompelmi.hooks.cjs +2 -2
  15. package/dist/pompelmi.hooks.cjs.map +1 -1
  16. package/dist/pompelmi.hooks.esm.js +2 -2
  17. package/dist/pompelmi.hooks.esm.js.map +1 -1
  18. package/dist/pompelmi.policy-packs.cjs +74 -73
  19. package/dist/pompelmi.policy-packs.cjs.map +1 -1
  20. package/dist/pompelmi.policy-packs.esm.js +74 -73
  21. package/dist/pompelmi.policy-packs.esm.js.map +1 -1
  22. package/dist/pompelmi.quarantine.cjs +135 -133
  23. package/dist/pompelmi.quarantine.cjs.map +1 -1
  24. package/dist/pompelmi.quarantine.esm.js +135 -133
  25. package/dist/pompelmi.quarantine.esm.js.map +1 -1
  26. package/dist/pompelmi.react.cjs +585 -534
  27. package/dist/pompelmi.react.cjs.map +1 -1
  28. package/dist/pompelmi.react.esm.js +585 -534
  29. package/dist/pompelmi.react.esm.js.map +1 -1
  30. package/dist/types/audit.d.ts +12 -12
  31. package/dist/types/browser-index.d.ts +12 -12
  32. package/dist/types/config.d.ts +4 -4
  33. package/dist/types/engines/dynamic-taint.d.ts +1 -1
  34. package/dist/types/engines/hybrid-orchestrator.d.ts +1 -1
  35. package/dist/types/engines/hybrid-taint-integration.d.ts +6 -6
  36. package/dist/types/engines/taint-policies.d.ts +4 -4
  37. package/dist/types/hipaa-compliance.d.ts +2 -2
  38. package/dist/types/hooks.d.ts +2 -2
  39. package/dist/types/index.d.ts +20 -20
  40. package/dist/types/node/scanDir.d.ts +5 -5
  41. package/dist/types/policy-packs.d.ts +2 -2
  42. package/dist/types/presets.d.ts +3 -3
  43. package/dist/types/quarantine/index.d.ts +3 -3
  44. package/dist/types/quarantine/storage.d.ts +1 -1
  45. package/dist/types/quarantine/types.d.ts +3 -3
  46. package/dist/types/quarantine/workflow.d.ts +4 -4
  47. package/dist/types/react-index.d.ts +2 -2
  48. package/dist/types/risk.d.ts +1 -1
  49. package/dist/types/scan/remote.d.ts +2 -2
  50. package/dist/types/scan.d.ts +5 -5
  51. package/dist/types/scanners/common-heuristics.d.ts +1 -1
  52. package/dist/types/scanners/zip-bomb-guard.d.ts +1 -1
  53. package/dist/types/src/audit.d.ts +84 -0
  54. package/dist/types/src/browser-index.d.ts +29 -0
  55. package/dist/types/src/config.d.ts +143 -0
  56. package/dist/types/src/engines/dynamic-taint.d.ts +102 -0
  57. package/dist/types/src/engines/hybrid-orchestrator.d.ts +65 -0
  58. package/dist/types/src/engines/hybrid-taint-integration.d.ts +129 -0
  59. package/dist/types/src/engines/taint-policies.d.ts +84 -0
  60. package/dist/types/src/hipaa-compliance.d.ts +110 -0
  61. package/dist/types/src/hooks.d.ts +89 -0
  62. package/dist/types/src/index.d.ts +29 -0
  63. package/dist/types/src/magic.d.ts +7 -0
  64. package/dist/types/src/node/scanDir.d.ts +30 -0
  65. package/dist/types/src/policy-packs.d.ts +98 -0
  66. package/dist/types/src/policy.d.ts +12 -0
  67. package/dist/types/src/presets.d.ts +72 -0
  68. package/dist/types/src/quarantine/index.d.ts +18 -0
  69. package/dist/types/src/quarantine/storage.d.ts +77 -0
  70. package/dist/types/src/quarantine/types.d.ts +78 -0
  71. package/dist/types/src/quarantine/workflow.d.ts +97 -0
  72. package/dist/types/src/react-index.d.ts +13 -0
  73. package/dist/types/src/risk.d.ts +18 -0
  74. package/dist/types/src/scan/remote.d.ts +12 -0
  75. package/dist/types/src/scan.d.ts +17 -0
  76. package/dist/types/src/scanners/common-heuristics.d.ts +14 -0
  77. package/dist/types/src/scanners/zip-bomb-guard.d.ts +9 -0
  78. package/dist/types/src/scanners/zipTraversalGuard.d.ts +19 -0
  79. package/dist/types/src/stream.d.ts +10 -0
  80. package/dist/types/src/types/decompilation.d.ts +96 -0
  81. package/dist/types/src/types/taint-tracking.d.ts +495 -0
  82. package/dist/types/src/types.d.ts +48 -0
  83. package/dist/types/src/useFileScanner.d.ts +15 -0
  84. package/dist/types/src/utils/advanced-detection.d.ts +21 -0
  85. package/dist/types/src/utils/batch-scanner.d.ts +62 -0
  86. package/dist/types/src/utils/cache-manager.d.ts +95 -0
  87. package/dist/types/src/utils/export.d.ts +51 -0
  88. package/dist/types/src/utils/performance-metrics.d.ts +68 -0
  89. package/dist/types/src/utils/threat-intelligence.d.ts +96 -0
  90. package/dist/types/src/validate.d.ts +7 -0
  91. package/dist/types/src/verdict.d.ts +2 -0
  92. package/dist/types/src/yara/browser.d.ts +7 -0
  93. package/dist/types/src/yara/index.d.ts +17 -0
  94. package/dist/types/src/yara/node.d.ts +2 -0
  95. package/dist/types/src/yara/remote.d.ts +10 -0
  96. package/dist/types/src/yara-bridge.d.ts +3 -0
  97. package/dist/types/src/zip.d.ts +13 -0
  98. package/dist/types/types/decompilation.d.ts +4 -4
  99. package/dist/types/types/taint-tracking.d.ts +19 -19
  100. package/dist/types/types.d.ts +3 -3
  101. package/dist/types/useFileScanner.d.ts +1 -1
  102. package/dist/types/utils/advanced-detection.d.ts +1 -1
  103. package/dist/types/utils/batch-scanner.d.ts +3 -3
  104. package/dist/types/utils/cache-manager.d.ts +1 -1
  105. package/dist/types/utils/export.d.ts +2 -2
  106. package/dist/types/utils/threat-intelligence.d.ts +4 -4
  107. package/dist/types/verdict.d.ts +1 -1
  108. package/dist/types/yara/browser.d.ts +1 -1
  109. package/dist/types/yara/index.d.ts +1 -1
  110. package/dist/types/yara/node.d.ts +1 -1
  111. package/dist/types/yara/remote.d.ts +2 -2
  112. package/package.json +6 -6
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  <p>
6
6
  <a href="https://www.npmjs.com/package/pompelmi"><img alt="npm version" src="https://img.shields.io/npm/v/pompelmi"></a>
7
7
  <a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/pompelmi/pompelmi/ci.yml?label=ci"></a>
8
+ <a href="https://codecov.io/gh/pompelmi/pompelmi"><img alt="Codecov" src="https://codecov.io/gh/pompelmi/pompelmi/branch/main/graph/badge.svg?flag=core"></a>
8
9
  <a href="https://github.com/pompelmi/pompelmi/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/pompelmi/pompelmi"></a>
9
10
  <a href="https://www.npmjs.com/package/pompelmi"><img alt="npm downloads" src="https://img.shields.io/npm/dm/pompelmi"></a>
10
11
  </p>
@@ -93,16 +94,31 @@ if (report.verdict !== "clean") {
93
94
  }
94
95
  ```
95
96
 
96
- ## Next steps
97
+ ## Start Here
97
98
 
98
- - [Documentation](https://pompelmi.github.io/pompelmi/)
99
+ - Express: [Docs](https://pompelmi.github.io/pompelmi/how-to/express/) · [Examples](./examples/express-minimal)
100
+ - Next.js: [Docs](https://pompelmi.github.io/pompelmi/how-to/nextjs/) · [Examples](./examples/next-app-router)
101
+ - NestJS: [Docs](https://pompelmi.github.io/pompelmi/how-to/nestjs/) · [Example app](./examples/nestjs-app)
102
+ - Fastify: [Docs](https://pompelmi.github.io/pompelmi/how-to/fastify/) · [Package](./packages/fastify-plugin)
103
+ - Koa: [Docs](https://pompelmi.github.io/pompelmi/how-to/koa/) · [Package](./packages/koa-middleware)
104
+ - CI/CD: [Use case](https://pompelmi.github.io/pompelmi/use-cases/cicd-artifact-scanning/) · [Blog](https://pompelmi.github.io/pompelmi/blog/cicd-scan-build-artifacts/)
105
+ - S3 / object storage: [Tutorial](https://pompelmi.github.io/pompelmi/tutorials/secure-s3-presigned-uploads-with-malware-scanning/) · [Use case](https://pompelmi.github.io/pompelmi/use-cases/s3-presigned-upload-security/)
106
+
107
+ ## Go Deeper
108
+
109
+ - [Docs home](https://pompelmi.github.io/pompelmi/)
110
+ - [Use cases](https://pompelmi.github.io/pompelmi/use-cases/)
111
+ - [Comparisons](https://pompelmi.github.io/pompelmi/comparisons/)
112
+ - [Tutorials](https://pompelmi.github.io/pompelmi/tutorials/)
113
+ - [Featured in](https://pompelmi.github.io/pompelmi/featured-in/)
114
+ - [Translations](https://pompelmi.github.io/pompelmi/translations/)
99
115
  - [Examples index](./examples/README.md)
100
116
  - [Demo example](./examples/demo)
101
117
  - [Contributing](./CONTRIBUTING.md)
102
118
  - [Security](./SECURITY.md)
103
119
  - [Roadmap](./ROADMAP.md)
104
120
 
105
- ## What Problem It Solves
121
+ ## What It Checks
106
122
 
107
123
  Upload endpoints are part of your attack surface. A renamed executable, a risky PDF, or a hostile archive can look harmless until it is stored, unpacked, served, or parsed by another system.
108
124
 
@@ -115,12 +131,6 @@ Pompelmi adds checks at the upload boundary for:
115
131
 
116
132
  The goal is simple: inspect first, store later.
117
133
 
118
- ## Why This Shape
119
-
120
- - Plain Markdown, readable in GitHub and in a terminal
121
- - Fast path first: install, example, then deeper links
122
- - Minimal top-level detail, with docs and examples for everything else
123
-
124
134
  ## Ecosystem
125
135
 
126
136
  - `pompelmi`
@@ -138,7 +148,7 @@ The goal is simple: inspect first, store later.
138
148
  - `packages/` framework adapters and supporting packages
139
149
  - `examples/` runnable examples
140
150
  - `tests/` test coverage
141
- - `website/` documentation site
151
+ - `website/` public docs, blog, and discovery site
142
152
 
143
153
  ## Development
144
154
 
@@ -150,16 +160,18 @@ pnpm build
150
160
 
151
161
  <!-- MENTIONS:START -->
152
162
 
153
- ## 🌟 Featured In
163
+ ## Featured In
164
+
165
+ Full page: [pompelmi.github.io/pompelmi/featured-in](https://pompelmi.github.io/pompelmi/featured-in/)
154
166
 
155
167
  *Last updated: March 20, 2026*
156
168
 
157
- ### 📋 Awesome Lists & Curated Collections
169
+ ### Awesome Lists & Curated Collections
158
170
 
159
171
  - [Awesome JavaScript](https://github.com/sorrycc/awesome-javascript) — sorrycc
160
172
  - [Awesome TypeScript](https://github.com/dzharii/awesome-typescript) — dzharii
161
173
 
162
- ### 📰 Newsletters & Roundups
174
+ ### Newsletters & Roundups
163
175
 
164
176
  - [The Overflow Issue 319: Dogfooding your SDLC](https://stackoverflow.blog/newsletter/issue-319-dogfooding-your-sdlc/) — Stack Overflow (2026-03-04)
165
177
  - [Hottest cybersecurity open-source tools of the month: February 2026](https://www.helpnetsecurity.com/2026/02/26/hottest-cybersecurity-open-source-tools-of-the-month-february-2026/) — Help Net Security (2026-02-26)
@@ -167,7 +179,7 @@ pnpm build
167
179
  - [Node Weekly Issue 594](https://nodeweekly.com/issues/594) — Node Weekly (2025-09-30)
168
180
  - [Det. Eng. Weekly Issue #124 - The DEFCON hangover is real](https://www.detectionengineering.net/p/det-eng-weekly-issue-124-the-defcon) — Detection Engineering (2025-08-13)
169
181
 
170
- ### 🔗 Other Mentions
182
+ ### Other Mentions
171
183
 
172
184
  - [Defense against uploads: Q&A with OSS file scanner, pompelmi](https://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/) — Stack Overflow (2026-02-23)
173
185
  - [Pompelmi: Open-source secure file upload scanning for Node.js](https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/) — Help Net Security (2026-02-02)
@@ -45,7 +45,7 @@ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
45
45
  class AuditTrail {
46
46
  constructor(options = {}) {
47
47
  this.options = {
48
- output: options.output ?? { dest: 'console' },
48
+ output: options.output ?? { dest: "console" },
49
49
  pretty: options.pretty ?? false,
50
50
  };
51
51
  }
@@ -53,7 +53,7 @@ class AuditTrail {
53
53
  logScanComplete(report, extra) {
54
54
  const record = {
55
55
  timestamp: new Date().toISOString(),
56
- event: report.verdict !== 'clean' ? 'threat.detected' : 'scan.complete',
56
+ event: report.verdict !== "clean" ? "threat.detected" : "scan.complete",
57
57
  verdict: report.verdict,
58
58
  matchCount: report.matches?.length ?? 0,
59
59
  durationMs: report.durationMs,
@@ -67,8 +67,8 @@ class AuditTrail {
67
67
  logScanError(error, extra) {
68
68
  const record = {
69
69
  timestamp: new Date().toISOString(),
70
- event: 'scan.error',
71
- verdict: 'clean', // unknown at this point
70
+ event: "scan.error",
71
+ verdict: "clean", // unknown at this point
72
72
  matchCount: 0,
73
73
  error: error instanceof Error ? error.message : String(error),
74
74
  ...extra,
@@ -79,7 +79,7 @@ class AuditTrail {
79
79
  logQuarantine(entry, correlationId) {
80
80
  const record = {
81
81
  timestamp: new Date().toISOString(),
82
- event: 'quarantine.created',
82
+ event: "quarantine.created",
83
83
  quarantineId: entry.id,
84
84
  filename: entry.file.originalName,
85
85
  sha256: entry.file.sha256,
@@ -92,11 +92,11 @@ class AuditTrail {
92
92
  logQuarantineResolved(entry, correlationId) {
93
93
  const record = {
94
94
  timestamp: new Date().toISOString(),
95
- event: entry.status === 'deleted' ? 'quarantine.deleted' : 'quarantine.resolved',
95
+ event: entry.status === "deleted" ? "quarantine.deleted" : "quarantine.resolved",
96
96
  quarantineId: entry.id,
97
97
  filename: entry.file.originalName,
98
98
  sha256: entry.file.sha256,
99
- decision: entry.status === 'promoted' ? 'promote' : 'delete',
99
+ decision: entry.status === "promoted" ? "promote" : "delete",
100
100
  reviewedBy: entry.reviewedBy,
101
101
  reviewNote: entry.reviewNote,
102
102
  correlationId,
@@ -104,19 +104,17 @@ class AuditTrail {
104
104
  void this.write(record);
105
105
  }
106
106
  async write(record) {
107
- const line = this.options.pretty
108
- ? JSON.stringify(record, null, 2)
109
- : JSON.stringify(record);
107
+ const line = this.options.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);
110
108
  const { output } = this.options;
111
109
  try {
112
- if (output.dest === 'console') {
113
- process.stdout.write(line + '\n');
110
+ if (output.dest === "console") {
111
+ process.stdout.write(line + "\n");
114
112
  }
115
- else if (output.dest === 'file') {
113
+ else if (output.dest === "file") {
116
114
  // Append a newline-delimited JSON (NDJSON) record.
117
- await fs__namespace.promises.appendFile(output.path, line + '\n', 'utf8');
115
+ await fs__namespace.promises.appendFile(output.path, line + "\n", "utf8");
118
116
  }
119
- else if (output.dest === 'custom') {
117
+ else if (output.dest === "custom") {
120
118
  await output.write(record);
121
119
  }
122
120
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pompelmi.audit.cjs","sources":["../src/audit.ts"],"sourcesContent":["/**\n * Audit trail for Pompelmi scan and quarantine events.\n *\n * Produces structured, append-only audit records suitable for:\n * - compliance logging (HIPAA, SOC 2, ISO 27001)\n * - SIEM ingestion\n * - operational dashboards\n * - incident response\n *\n * Usage:\n * ```ts\n * import { AuditTrail } from 'pompelmi/audit';\n *\n * const audit = new AuditTrail({ dest: 'file', path: './audit.jsonl' });\n * audit.logScanComplete({ filename: 'upload.zip', verdict: 'suspicious', ... });\n * audit.logQuarantine({ entryId: '...', sha256: '...', ... });\n * ```\n *\n * @module audit\n */\n\nimport * as fs from 'fs';\nimport type { ScanReport } from './types';\nimport type { QuarantineEntry } from './quarantine/types';\n\n// ── Record types ──────────────────────────────────────────────────────────────\n\nexport type AuditEventType =\n | 'scan.complete'\n | 'scan.error'\n | 'threat.detected'\n | 'quarantine.created'\n | 'quarantine.resolved'\n | 'quarantine.deleted';\n\ninterface BaseAuditRecord {\n /** ISO-8601 timestamp. */\n timestamp: string;\n /** Event type for structured log routing. */\n event: AuditEventType;\n /** Application-assigned session or request id for correlation. */\n correlationId?: string;\n /** Uploader identity. */\n uploadedBy?: string;\n}\n\nexport interface ScanAuditRecord extends BaseAuditRecord {\n event: 'scan.complete' | 'scan.error' | 'threat.detected';\n filename?: string;\n mimeType?: string;\n sizeBytes?: number;\n sha256?: string;\n verdict: ScanReport['verdict'];\n matchCount: number;\n durationMs?: number;\n engine?: string;\n error?: string;\n}\n\nexport interface QuarantineAuditRecord extends BaseAuditRecord {\n event: 'quarantine.created' | 'quarantine.resolved' | 'quarantine.deleted';\n quarantineId: string;\n filename?: string;\n sha256: string;\n decision?: 'promote' | 'delete';\n reviewedBy?: string;\n reviewNote?: string;\n}\n\nexport type AuditRecord = ScanAuditRecord | QuarantineAuditRecord;\n\n// ── Destination ───────────────────────────────────────────────────────────────\n\nexport type AuditDest =\n | { dest: 'console' }\n | { dest: 'file'; path: string }\n | { dest: 'custom'; write: (record: AuditRecord) => void | Promise<void> };\n\nexport interface AuditTrailOptions {\n /** Where to write audit records. Default: 'console'. */\n output?: AuditDest;\n /** If true, pretty-print JSON. Useful for debugging. Default: false. */\n pretty?: boolean;\n}\n\n// ── AuditTrail ────────────────────────────────────────────────────────────────\n\nexport class AuditTrail {\n private readonly options: Required<AuditTrailOptions>;\n\n constructor(options: AuditTrailOptions = {}) {\n this.options = {\n output: options.output ?? { dest: 'console' },\n pretty: options.pretty ?? false,\n };\n }\n\n /** Log a completed scan. */\n logScanComplete(\n report: ScanReport,\n extra?: Pick<ScanAuditRecord, 'filename' | 'sizeBytes' | 'sha256' | 'correlationId' | 'uploadedBy'>,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: report.verdict !== 'clean' ? 'threat.detected' : 'scan.complete',\n verdict: report.verdict,\n matchCount: report.matches?.length ?? 0,\n durationMs: report.durationMs,\n engine: report.engine,\n mimeType: report.file?.mimeType,\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a scan error. */\n logScanError(\n error: unknown,\n extra?: Pick<ScanAuditRecord, 'filename' | 'correlationId' | 'uploadedBy'>,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: 'scan.error',\n verdict: 'clean', // unknown at this point\n matchCount: 0,\n error: error instanceof Error ? error.message : String(error),\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a new quarantine entry. */\n logQuarantine(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: 'quarantine.created',\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n uploadedBy: entry.file.uploadedBy,\n correlationId,\n };\n void this.write(record);\n }\n\n /** Log a quarantine resolution (promote or delete). */\n logQuarantineResolved(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: entry.status === 'deleted' ? 'quarantine.deleted' : 'quarantine.resolved',\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n decision: entry.status === 'promoted' ? 'promote' : 'delete',\n reviewedBy: entry.reviewedBy,\n reviewNote: entry.reviewNote,\n correlationId,\n };\n void this.write(record);\n }\n\n private async write(record: AuditRecord): Promise<void> {\n const line = this.options.pretty\n ? JSON.stringify(record, null, 2)\n : JSON.stringify(record);\n\n const { output } = this.options;\n\n try {\n if (output.dest === 'console') {\n process.stdout.write(line + '\\n');\n } else if (output.dest === 'file') {\n // Append a newline-delimited JSON (NDJSON) record.\n await fs.promises.appendFile(output.path, line + '\\n', 'utf8');\n } else if (output.dest === 'custom') {\n await output.write(record);\n }\n } catch {\n // Audit failures must never interrupt the upload pipeline.\n }\n }\n}\n"],"names":["fs"],"mappings":"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"}
1
+ {"version":3,"file":"pompelmi.audit.cjs","sources":["../../src/audit.ts"],"sourcesContent":["/**\n * Audit trail for Pompelmi scan and quarantine events.\n *\n * Produces structured, append-only audit records suitable for:\n * - compliance logging (HIPAA, SOC 2, ISO 27001)\n * - SIEM ingestion\n * - operational dashboards\n * - incident response\n *\n * Usage:\n * ```ts\n * import { AuditTrail } from 'pompelmi/audit';\n *\n * const audit = new AuditTrail({ dest: 'file', path: './audit.jsonl' });\n * audit.logScanComplete({ filename: 'upload.zip', verdict: 'suspicious', ... });\n * audit.logQuarantine({ entryId: '...', sha256: '...', ... });\n * ```\n *\n * @module audit\n */\n\nimport * as fs from \"fs\";\nimport type { QuarantineEntry } from \"./quarantine/types\";\nimport type { ScanReport } from \"./types\";\n\n// ── Record types ──────────────────────────────────────────────────────────────\n\nexport type AuditEventType =\n | \"scan.complete\"\n | \"scan.error\"\n | \"threat.detected\"\n | \"quarantine.created\"\n | \"quarantine.resolved\"\n | \"quarantine.deleted\";\n\ninterface BaseAuditRecord {\n /** ISO-8601 timestamp. */\n timestamp: string;\n /** Event type for structured log routing. */\n event: AuditEventType;\n /** Application-assigned session or request id for correlation. */\n correlationId?: string;\n /** Uploader identity. */\n uploadedBy?: string;\n}\n\nexport interface ScanAuditRecord extends BaseAuditRecord {\n event: \"scan.complete\" | \"scan.error\" | \"threat.detected\";\n filename?: string;\n mimeType?: string;\n sizeBytes?: number;\n sha256?: string;\n verdict: ScanReport[\"verdict\"];\n matchCount: number;\n durationMs?: number;\n engine?: string;\n error?: string;\n}\n\nexport interface QuarantineAuditRecord extends BaseAuditRecord {\n event: \"quarantine.created\" | \"quarantine.resolved\" | \"quarantine.deleted\";\n quarantineId: string;\n filename?: string;\n sha256: string;\n decision?: \"promote\" | \"delete\";\n reviewedBy?: string;\n reviewNote?: string;\n}\n\nexport type AuditRecord = ScanAuditRecord | QuarantineAuditRecord;\n\n// ── Destination ───────────────────────────────────────────────────────────────\n\nexport type AuditDest =\n | { dest: \"console\" }\n | { dest: \"file\"; path: string }\n | { dest: \"custom\"; write: (record: AuditRecord) => void | Promise<void> };\n\nexport interface AuditTrailOptions {\n /** Where to write audit records. Default: 'console'. */\n output?: AuditDest;\n /** If true, pretty-print JSON. Useful for debugging. Default: false. */\n pretty?: boolean;\n}\n\n// ── AuditTrail ────────────────────────────────────────────────────────────────\n\nexport class AuditTrail {\n private readonly options: Required<AuditTrailOptions>;\n\n constructor(options: AuditTrailOptions = {}) {\n this.options = {\n output: options.output ?? { dest: \"console\" },\n pretty: options.pretty ?? false,\n };\n }\n\n /** Log a completed scan. */\n logScanComplete(\n report: ScanReport,\n extra?: Pick<\n ScanAuditRecord,\n \"filename\" | \"sizeBytes\" | \"sha256\" | \"correlationId\" | \"uploadedBy\"\n >,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: report.verdict !== \"clean\" ? \"threat.detected\" : \"scan.complete\",\n verdict: report.verdict,\n matchCount: report.matches?.length ?? 0,\n durationMs: report.durationMs,\n engine: report.engine,\n mimeType: report.file?.mimeType,\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a scan error. */\n logScanError(\n error: unknown,\n extra?: Pick<ScanAuditRecord, \"filename\" | \"correlationId\" | \"uploadedBy\">,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: \"scan.error\",\n verdict: \"clean\", // unknown at this point\n matchCount: 0,\n error: error instanceof Error ? error.message : String(error),\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a new quarantine entry. */\n logQuarantine(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: \"quarantine.created\",\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n uploadedBy: entry.file.uploadedBy,\n correlationId,\n };\n void this.write(record);\n }\n\n /** Log a quarantine resolution (promote or delete). */\n logQuarantineResolved(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: entry.status === \"deleted\" ? \"quarantine.deleted\" : \"quarantine.resolved\",\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n decision: entry.status === \"promoted\" ? \"promote\" : \"delete\",\n reviewedBy: entry.reviewedBy,\n reviewNote: entry.reviewNote,\n correlationId,\n };\n void this.write(record);\n }\n\n private async write(record: AuditRecord): Promise<void> {\n const line = this.options.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);\n\n const { output } = this.options;\n\n try {\n if (output.dest === \"console\") {\n process.stdout.write(line + \"\\n\");\n } else if (output.dest === \"file\") {\n // Append a newline-delimited JSON (NDJSON) record.\n await fs.promises.appendFile(output.path, line + \"\\n\", \"utf8\");\n } else if (output.dest === \"custom\") {\n await output.write(record);\n }\n } catch {\n // Audit failures must never interrupt the upload pipeline.\n }\n }\n}\n"],"names":["fs"],"mappings":"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"}
@@ -24,7 +24,7 @@ import * as fs from 'fs';
24
24
  class AuditTrail {
25
25
  constructor(options = {}) {
26
26
  this.options = {
27
- output: options.output ?? { dest: 'console' },
27
+ output: options.output ?? { dest: "console" },
28
28
  pretty: options.pretty ?? false,
29
29
  };
30
30
  }
@@ -32,7 +32,7 @@ class AuditTrail {
32
32
  logScanComplete(report, extra) {
33
33
  const record = {
34
34
  timestamp: new Date().toISOString(),
35
- event: report.verdict !== 'clean' ? 'threat.detected' : 'scan.complete',
35
+ event: report.verdict !== "clean" ? "threat.detected" : "scan.complete",
36
36
  verdict: report.verdict,
37
37
  matchCount: report.matches?.length ?? 0,
38
38
  durationMs: report.durationMs,
@@ -46,8 +46,8 @@ class AuditTrail {
46
46
  logScanError(error, extra) {
47
47
  const record = {
48
48
  timestamp: new Date().toISOString(),
49
- event: 'scan.error',
50
- verdict: 'clean', // unknown at this point
49
+ event: "scan.error",
50
+ verdict: "clean", // unknown at this point
51
51
  matchCount: 0,
52
52
  error: error instanceof Error ? error.message : String(error),
53
53
  ...extra,
@@ -58,7 +58,7 @@ class AuditTrail {
58
58
  logQuarantine(entry, correlationId) {
59
59
  const record = {
60
60
  timestamp: new Date().toISOString(),
61
- event: 'quarantine.created',
61
+ event: "quarantine.created",
62
62
  quarantineId: entry.id,
63
63
  filename: entry.file.originalName,
64
64
  sha256: entry.file.sha256,
@@ -71,11 +71,11 @@ class AuditTrail {
71
71
  logQuarantineResolved(entry, correlationId) {
72
72
  const record = {
73
73
  timestamp: new Date().toISOString(),
74
- event: entry.status === 'deleted' ? 'quarantine.deleted' : 'quarantine.resolved',
74
+ event: entry.status === "deleted" ? "quarantine.deleted" : "quarantine.resolved",
75
75
  quarantineId: entry.id,
76
76
  filename: entry.file.originalName,
77
77
  sha256: entry.file.sha256,
78
- decision: entry.status === 'promoted' ? 'promote' : 'delete',
78
+ decision: entry.status === "promoted" ? "promote" : "delete",
79
79
  reviewedBy: entry.reviewedBy,
80
80
  reviewNote: entry.reviewNote,
81
81
  correlationId,
@@ -83,19 +83,17 @@ class AuditTrail {
83
83
  void this.write(record);
84
84
  }
85
85
  async write(record) {
86
- const line = this.options.pretty
87
- ? JSON.stringify(record, null, 2)
88
- : JSON.stringify(record);
86
+ const line = this.options.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);
89
87
  const { output } = this.options;
90
88
  try {
91
- if (output.dest === 'console') {
92
- process.stdout.write(line + '\n');
89
+ if (output.dest === "console") {
90
+ process.stdout.write(line + "\n");
93
91
  }
94
- else if (output.dest === 'file') {
92
+ else if (output.dest === "file") {
95
93
  // Append a newline-delimited JSON (NDJSON) record.
96
- await fs.promises.appendFile(output.path, line + '\n', 'utf8');
94
+ await fs.promises.appendFile(output.path, line + "\n", "utf8");
97
95
  }
98
- else if (output.dest === 'custom') {
96
+ else if (output.dest === "custom") {
99
97
  await output.write(record);
100
98
  }
101
99
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pompelmi.audit.esm.js","sources":["../src/audit.ts"],"sourcesContent":["/**\n * Audit trail for Pompelmi scan and quarantine events.\n *\n * Produces structured, append-only audit records suitable for:\n * - compliance logging (HIPAA, SOC 2, ISO 27001)\n * - SIEM ingestion\n * - operational dashboards\n * - incident response\n *\n * Usage:\n * ```ts\n * import { AuditTrail } from 'pompelmi/audit';\n *\n * const audit = new AuditTrail({ dest: 'file', path: './audit.jsonl' });\n * audit.logScanComplete({ filename: 'upload.zip', verdict: 'suspicious', ... });\n * audit.logQuarantine({ entryId: '...', sha256: '...', ... });\n * ```\n *\n * @module audit\n */\n\nimport * as fs from 'fs';\nimport type { ScanReport } from './types';\nimport type { QuarantineEntry } from './quarantine/types';\n\n// ── Record types ──────────────────────────────────────────────────────────────\n\nexport type AuditEventType =\n | 'scan.complete'\n | 'scan.error'\n | 'threat.detected'\n | 'quarantine.created'\n | 'quarantine.resolved'\n | 'quarantine.deleted';\n\ninterface BaseAuditRecord {\n /** ISO-8601 timestamp. */\n timestamp: string;\n /** Event type for structured log routing. */\n event: AuditEventType;\n /** Application-assigned session or request id for correlation. */\n correlationId?: string;\n /** Uploader identity. */\n uploadedBy?: string;\n}\n\nexport interface ScanAuditRecord extends BaseAuditRecord {\n event: 'scan.complete' | 'scan.error' | 'threat.detected';\n filename?: string;\n mimeType?: string;\n sizeBytes?: number;\n sha256?: string;\n verdict: ScanReport['verdict'];\n matchCount: number;\n durationMs?: number;\n engine?: string;\n error?: string;\n}\n\nexport interface QuarantineAuditRecord extends BaseAuditRecord {\n event: 'quarantine.created' | 'quarantine.resolved' | 'quarantine.deleted';\n quarantineId: string;\n filename?: string;\n sha256: string;\n decision?: 'promote' | 'delete';\n reviewedBy?: string;\n reviewNote?: string;\n}\n\nexport type AuditRecord = ScanAuditRecord | QuarantineAuditRecord;\n\n// ── Destination ───────────────────────────────────────────────────────────────\n\nexport type AuditDest =\n | { dest: 'console' }\n | { dest: 'file'; path: string }\n | { dest: 'custom'; write: (record: AuditRecord) => void | Promise<void> };\n\nexport interface AuditTrailOptions {\n /** Where to write audit records. Default: 'console'. */\n output?: AuditDest;\n /** If true, pretty-print JSON. Useful for debugging. Default: false. */\n pretty?: boolean;\n}\n\n// ── AuditTrail ────────────────────────────────────────────────────────────────\n\nexport class AuditTrail {\n private readonly options: Required<AuditTrailOptions>;\n\n constructor(options: AuditTrailOptions = {}) {\n this.options = {\n output: options.output ?? { dest: 'console' },\n pretty: options.pretty ?? false,\n };\n }\n\n /** Log a completed scan. */\n logScanComplete(\n report: ScanReport,\n extra?: Pick<ScanAuditRecord, 'filename' | 'sizeBytes' | 'sha256' | 'correlationId' | 'uploadedBy'>,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: report.verdict !== 'clean' ? 'threat.detected' : 'scan.complete',\n verdict: report.verdict,\n matchCount: report.matches?.length ?? 0,\n durationMs: report.durationMs,\n engine: report.engine,\n mimeType: report.file?.mimeType,\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a scan error. */\n logScanError(\n error: unknown,\n extra?: Pick<ScanAuditRecord, 'filename' | 'correlationId' | 'uploadedBy'>,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: 'scan.error',\n verdict: 'clean', // unknown at this point\n matchCount: 0,\n error: error instanceof Error ? error.message : String(error),\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a new quarantine entry. */\n logQuarantine(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: 'quarantine.created',\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n uploadedBy: entry.file.uploadedBy,\n correlationId,\n };\n void this.write(record);\n }\n\n /** Log a quarantine resolution (promote or delete). */\n logQuarantineResolved(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: entry.status === 'deleted' ? 'quarantine.deleted' : 'quarantine.resolved',\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n decision: entry.status === 'promoted' ? 'promote' : 'delete',\n reviewedBy: entry.reviewedBy,\n reviewNote: entry.reviewNote,\n correlationId,\n };\n void this.write(record);\n }\n\n private async write(record: AuditRecord): Promise<void> {\n const line = this.options.pretty\n ? JSON.stringify(record, null, 2)\n : JSON.stringify(record);\n\n const { output } = this.options;\n\n try {\n if (output.dest === 'console') {\n process.stdout.write(line + '\\n');\n } else if (output.dest === 'file') {\n // Append a newline-delimited JSON (NDJSON) record.\n await fs.promises.appendFile(output.path, line + '\\n', 'utf8');\n } else if (output.dest === 'custom') {\n await output.write(record);\n }\n } catch {\n // Audit failures must never interrupt the upload pipeline.\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pompelmi.audit.esm.js","sources":["../../src/audit.ts"],"sourcesContent":["/**\n * Audit trail for Pompelmi scan and quarantine events.\n *\n * Produces structured, append-only audit records suitable for:\n * - compliance logging (HIPAA, SOC 2, ISO 27001)\n * - SIEM ingestion\n * - operational dashboards\n * - incident response\n *\n * Usage:\n * ```ts\n * import { AuditTrail } from 'pompelmi/audit';\n *\n * const audit = new AuditTrail({ dest: 'file', path: './audit.jsonl' });\n * audit.logScanComplete({ filename: 'upload.zip', verdict: 'suspicious', ... });\n * audit.logQuarantine({ entryId: '...', sha256: '...', ... });\n * ```\n *\n * @module audit\n */\n\nimport * as fs from \"fs\";\nimport type { QuarantineEntry } from \"./quarantine/types\";\nimport type { ScanReport } from \"./types\";\n\n// ── Record types ──────────────────────────────────────────────────────────────\n\nexport type AuditEventType =\n | \"scan.complete\"\n | \"scan.error\"\n | \"threat.detected\"\n | \"quarantine.created\"\n | \"quarantine.resolved\"\n | \"quarantine.deleted\";\n\ninterface BaseAuditRecord {\n /** ISO-8601 timestamp. */\n timestamp: string;\n /** Event type for structured log routing. */\n event: AuditEventType;\n /** Application-assigned session or request id for correlation. */\n correlationId?: string;\n /** Uploader identity. */\n uploadedBy?: string;\n}\n\nexport interface ScanAuditRecord extends BaseAuditRecord {\n event: \"scan.complete\" | \"scan.error\" | \"threat.detected\";\n filename?: string;\n mimeType?: string;\n sizeBytes?: number;\n sha256?: string;\n verdict: ScanReport[\"verdict\"];\n matchCount: number;\n durationMs?: number;\n engine?: string;\n error?: string;\n}\n\nexport interface QuarantineAuditRecord extends BaseAuditRecord {\n event: \"quarantine.created\" | \"quarantine.resolved\" | \"quarantine.deleted\";\n quarantineId: string;\n filename?: string;\n sha256: string;\n decision?: \"promote\" | \"delete\";\n reviewedBy?: string;\n reviewNote?: string;\n}\n\nexport type AuditRecord = ScanAuditRecord | QuarantineAuditRecord;\n\n// ── Destination ───────────────────────────────────────────────────────────────\n\nexport type AuditDest =\n | { dest: \"console\" }\n | { dest: \"file\"; path: string }\n | { dest: \"custom\"; write: (record: AuditRecord) => void | Promise<void> };\n\nexport interface AuditTrailOptions {\n /** Where to write audit records. Default: 'console'. */\n output?: AuditDest;\n /** If true, pretty-print JSON. Useful for debugging. Default: false. */\n pretty?: boolean;\n}\n\n// ── AuditTrail ────────────────────────────────────────────────────────────────\n\nexport class AuditTrail {\n private readonly options: Required<AuditTrailOptions>;\n\n constructor(options: AuditTrailOptions = {}) {\n this.options = {\n output: options.output ?? { dest: \"console\" },\n pretty: options.pretty ?? false,\n };\n }\n\n /** Log a completed scan. */\n logScanComplete(\n report: ScanReport,\n extra?: Pick<\n ScanAuditRecord,\n \"filename\" | \"sizeBytes\" | \"sha256\" | \"correlationId\" | \"uploadedBy\"\n >,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: report.verdict !== \"clean\" ? \"threat.detected\" : \"scan.complete\",\n verdict: report.verdict,\n matchCount: report.matches?.length ?? 0,\n durationMs: report.durationMs,\n engine: report.engine,\n mimeType: report.file?.mimeType,\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a scan error. */\n logScanError(\n error: unknown,\n extra?: Pick<ScanAuditRecord, \"filename\" | \"correlationId\" | \"uploadedBy\">,\n ): void {\n const record: ScanAuditRecord = {\n timestamp: new Date().toISOString(),\n event: \"scan.error\",\n verdict: \"clean\", // unknown at this point\n matchCount: 0,\n error: error instanceof Error ? error.message : String(error),\n ...extra,\n };\n void this.write(record);\n }\n\n /** Log a new quarantine entry. */\n logQuarantine(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: \"quarantine.created\",\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n uploadedBy: entry.file.uploadedBy,\n correlationId,\n };\n void this.write(record);\n }\n\n /** Log a quarantine resolution (promote or delete). */\n logQuarantineResolved(entry: QuarantineEntry, correlationId?: string): void {\n const record: QuarantineAuditRecord = {\n timestamp: new Date().toISOString(),\n event: entry.status === \"deleted\" ? \"quarantine.deleted\" : \"quarantine.resolved\",\n quarantineId: entry.id,\n filename: entry.file.originalName,\n sha256: entry.file.sha256,\n decision: entry.status === \"promoted\" ? \"promote\" : \"delete\",\n reviewedBy: entry.reviewedBy,\n reviewNote: entry.reviewNote,\n correlationId,\n };\n void this.write(record);\n }\n\n private async write(record: AuditRecord): Promise<void> {\n const line = this.options.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);\n\n const { output } = this.options;\n\n try {\n if (output.dest === \"console\") {\n process.stdout.write(line + \"\\n\");\n } else if (output.dest === \"file\") {\n // Append a newline-delimited JSON (NDJSON) record.\n await fs.promises.appendFile(output.path, line + \"\\n\", \"utf8\");\n } else if (output.dest === \"custom\") {\n await output.write(record);\n }\n } catch {\n // Audit failures must never interrupt the upload pipeline.\n }\n }\n}\n"],"names":[],"mappings":"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"}