pompelmi 0.32.1 → 0.34.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 (43) hide show
  1. package/README.md +355 -957
  2. package/dist/pompelmi.audit.cjs +130 -0
  3. package/dist/pompelmi.audit.cjs.map +1 -0
  4. package/dist/pompelmi.audit.esm.js +109 -0
  5. package/dist/pompelmi.audit.esm.js.map +1 -0
  6. package/dist/pompelmi.browser.cjs +1455 -0
  7. package/dist/pompelmi.browser.cjs.map +1 -0
  8. package/dist/pompelmi.browser.esm.js +1429 -0
  9. package/dist/pompelmi.browser.esm.js.map +1 -0
  10. package/dist/pompelmi.cjs +1403 -3118
  11. package/dist/pompelmi.cjs.map +1 -1
  12. package/dist/pompelmi.esm.js +1397 -3116
  13. package/dist/pompelmi.esm.js.map +1 -1
  14. package/dist/pompelmi.hooks.cjs +75 -0
  15. package/dist/pompelmi.hooks.cjs.map +1 -0
  16. package/dist/pompelmi.hooks.esm.js +72 -0
  17. package/dist/pompelmi.hooks.esm.js.map +1 -0
  18. package/dist/pompelmi.policy-packs.cjs +239 -0
  19. package/dist/pompelmi.policy-packs.cjs.map +1 -0
  20. package/dist/pompelmi.policy-packs.esm.js +231 -0
  21. package/dist/pompelmi.policy-packs.esm.js.map +1 -0
  22. package/dist/pompelmi.quarantine.cjs +315 -0
  23. package/dist/pompelmi.quarantine.cjs.map +1 -0
  24. package/dist/pompelmi.quarantine.esm.js +291 -0
  25. package/dist/pompelmi.quarantine.esm.js.map +1 -0
  26. package/dist/pompelmi.react.cjs +1486 -0
  27. package/dist/pompelmi.react.cjs.map +1 -0
  28. package/dist/pompelmi.react.esm.js +1459 -0
  29. package/dist/pompelmi.react.esm.js.map +1 -0
  30. package/dist/types/audit.d.ts +84 -0
  31. package/dist/types/browser-index.d.ts +28 -2
  32. package/dist/types/config.d.ts +3 -2
  33. package/dist/types/hooks.d.ts +89 -0
  34. package/dist/types/index.d.ts +17 -9
  35. package/dist/types/policy-packs.d.ts +98 -0
  36. package/dist/types/quarantine/index.d.ts +18 -0
  37. package/dist/types/quarantine/storage.d.ts +77 -0
  38. package/dist/types/quarantine/types.d.ts +78 -0
  39. package/dist/types/quarantine/workflow.d.ts +97 -0
  40. package/dist/types/react-index.d.ts +13 -0
  41. package/dist/types/scanners/common-heuristics.d.ts +2 -2
  42. package/dist/types/types.d.ts +0 -1
  43. package/package.json +55 -4
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+
5
+ function _interopNamespaceDefault(e) {
6
+ var n = Object.create(null);
7
+ if (e) {
8
+ Object.keys(e).forEach(function (k) {
9
+ if (k !== 'default') {
10
+ var d = Object.getOwnPropertyDescriptor(e, k);
11
+ Object.defineProperty(n, k, d.get ? d : {
12
+ enumerable: true,
13
+ get: function () { return e[k]; }
14
+ });
15
+ }
16
+ });
17
+ }
18
+ n.default = e;
19
+ return Object.freeze(n);
20
+ }
21
+
22
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
23
+
24
+ /**
25
+ * Audit trail for Pompelmi scan and quarantine events.
26
+ *
27
+ * Produces structured, append-only audit records suitable for:
28
+ * - compliance logging (HIPAA, SOC 2, ISO 27001)
29
+ * - SIEM ingestion
30
+ * - operational dashboards
31
+ * - incident response
32
+ *
33
+ * Usage:
34
+ * ```ts
35
+ * import { AuditTrail } from 'pompelmi/audit';
36
+ *
37
+ * const audit = new AuditTrail({ dest: 'file', path: './audit.jsonl' });
38
+ * audit.logScanComplete({ filename: 'upload.zip', verdict: 'suspicious', ... });
39
+ * audit.logQuarantine({ entryId: '...', sha256: '...', ... });
40
+ * ```
41
+ *
42
+ * @module audit
43
+ */
44
+ // ── AuditTrail ────────────────────────────────────────────────────────────────
45
+ class AuditTrail {
46
+ constructor(options = {}) {
47
+ this.options = {
48
+ output: options.output ?? { dest: 'console' },
49
+ pretty: options.pretty ?? false,
50
+ };
51
+ }
52
+ /** Log a completed scan. */
53
+ logScanComplete(report, extra) {
54
+ const record = {
55
+ timestamp: new Date().toISOString(),
56
+ event: report.verdict !== 'clean' ? 'threat.detected' : 'scan.complete',
57
+ verdict: report.verdict,
58
+ matchCount: report.matches?.length ?? 0,
59
+ durationMs: report.durationMs,
60
+ engine: report.engine,
61
+ mimeType: report.file?.mimeType,
62
+ ...extra,
63
+ };
64
+ void this.write(record);
65
+ }
66
+ /** Log a scan error. */
67
+ logScanError(error, extra) {
68
+ const record = {
69
+ timestamp: new Date().toISOString(),
70
+ event: 'scan.error',
71
+ verdict: 'clean', // unknown at this point
72
+ matchCount: 0,
73
+ error: error instanceof Error ? error.message : String(error),
74
+ ...extra,
75
+ };
76
+ void this.write(record);
77
+ }
78
+ /** Log a new quarantine entry. */
79
+ logQuarantine(entry, correlationId) {
80
+ const record = {
81
+ timestamp: new Date().toISOString(),
82
+ event: 'quarantine.created',
83
+ quarantineId: entry.id,
84
+ filename: entry.file.originalName,
85
+ sha256: entry.file.sha256,
86
+ uploadedBy: entry.file.uploadedBy,
87
+ correlationId,
88
+ };
89
+ void this.write(record);
90
+ }
91
+ /** Log a quarantine resolution (promote or delete). */
92
+ logQuarantineResolved(entry, correlationId) {
93
+ const record = {
94
+ timestamp: new Date().toISOString(),
95
+ event: entry.status === 'deleted' ? 'quarantine.deleted' : 'quarantine.resolved',
96
+ quarantineId: entry.id,
97
+ filename: entry.file.originalName,
98
+ sha256: entry.file.sha256,
99
+ decision: entry.status === 'promoted' ? 'promote' : 'delete',
100
+ reviewedBy: entry.reviewedBy,
101
+ reviewNote: entry.reviewNote,
102
+ correlationId,
103
+ };
104
+ void this.write(record);
105
+ }
106
+ async write(record) {
107
+ const line = this.options.pretty
108
+ ? JSON.stringify(record, null, 2)
109
+ : JSON.stringify(record);
110
+ const { output } = this.options;
111
+ try {
112
+ if (output.dest === 'console') {
113
+ process.stdout.write(line + '\n');
114
+ }
115
+ else if (output.dest === 'file') {
116
+ // Append a newline-delimited JSON (NDJSON) record.
117
+ await fs__namespace.promises.appendFile(output.path, line + '\n', 'utf8');
118
+ }
119
+ else if (output.dest === 'custom') {
120
+ await output.write(record);
121
+ }
122
+ }
123
+ catch {
124
+ // Audit failures must never interrupt the upload pipeline.
125
+ }
126
+ }
127
+ }
128
+
129
+ exports.AuditTrail = AuditTrail;
130
+ //# sourceMappingURL=pompelmi.audit.cjs.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;;;;;AAmBG;AAkEH;MAEa,UAAU,CAAA;AAGrB,IAAA,WAAA,CAAY,UAA6B,EAAE,EAAA;QACzC,IAAI,CAAC,OAAO,GAAG;YACb,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;AAC7C,YAAA,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK;SAChC;IACH;;IAGA,eAAe,CACb,MAAkB,EAClB,KAAmG,EAAA;AAEnG,QAAA,MAAM,MAAM,GAAoB;AAC9B,YAAA,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;AACnC,YAAA,KAAK,EAAE,MAAM,CAAC,OAAO,KAAK,OAAO,GAAG,iBAAiB,GAAG,eAAe;YACvE,OAAO,EAAE,MAAM,CAAC,OAAO;AACvB,YAAA,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;YACvC,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;AACrB,YAAA,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ;AAC/B,YAAA,GAAG,KAAK;SACT;AACD,QAAA,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACzB;;IAGA,YAAY,CACV,KAAc,EACd,KAA0E,EAAA;AAE1E,QAAA,MAAM,MAAM,GAAoB;AAC9B,YAAA,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;AACnC,YAAA,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,OAAO;AAChB,YAAA,UAAU,EAAE,CAAC;AACb,YAAA,KAAK,EAAE,KAAK,YAAY,KAAK,GAAG,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC;AAC7D,YAAA,GAAG,KAAK;SACT;AACD,QAAA,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACzB;;IAGA,aAAa,CAAC,KAAsB,EAAE,aAAsB,EAAA;AAC1D,QAAA,MAAM,MAAM,GAA0B;AACpC,YAAA,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;AACnC,YAAA,KAAK,EAAE,oBAAoB;YAC3B,YAAY,EAAE,KAAK,CAAC,EAAE;AACtB,YAAA,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY;AACjC,YAAA,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM;AACzB,YAAA,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU;YACjC,aAAa;SACd;AACD,QAAA,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACzB;;IAGA,qBAAqB,CAAC,KAAsB,EAAE,aAAsB,EAAA;AAClE,QAAA,MAAM,MAAM,GAA0B;AACpC,YAAA,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;AACnC,YAAA,KAAK,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,GAAG,oBAAoB,GAAG,qBAAqB;YAChF,YAAY,EAAE,KAAK,CAAC,EAAE;AACtB,YAAA,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY;AACjC,YAAA,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM;AACzB,YAAA,QAAQ,EAAE,KAAK,CAAC,MAAM,KAAK,UAAU,GAAG,SAAS,GAAG,QAAQ;YAC5D,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,aAAa;SACd;AACD,QAAA,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACzB;IAEQ,MAAM,KAAK,CAAC,MAAmB,EAAA;AACrC,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;cACtB,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;AAChC,cAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;AAE1B,QAAA,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO;AAE/B,QAAA,IAAI;AACF,YAAA,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE;gBAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;YACnC;AAAO,iBAAA,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE;;AAEjC,gBAAA,MAAMA,aAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC;YAChE;AAAO,iBAAA,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE;AACnC,gBAAA,MAAM,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAC5B;QACF;AAAE,QAAA,MAAM;;QAER;IACF;AACD;;;;"}
@@ -0,0 +1,109 @@
1
+ import * as fs from 'fs';
2
+
3
+ /**
4
+ * Audit trail for Pompelmi scan and quarantine events.
5
+ *
6
+ * Produces structured, append-only audit records suitable for:
7
+ * - compliance logging (HIPAA, SOC 2, ISO 27001)
8
+ * - SIEM ingestion
9
+ * - operational dashboards
10
+ * - incident response
11
+ *
12
+ * Usage:
13
+ * ```ts
14
+ * import { AuditTrail } from 'pompelmi/audit';
15
+ *
16
+ * const audit = new AuditTrail({ dest: 'file', path: './audit.jsonl' });
17
+ * audit.logScanComplete({ filename: 'upload.zip', verdict: 'suspicious', ... });
18
+ * audit.logQuarantine({ entryId: '...', sha256: '...', ... });
19
+ * ```
20
+ *
21
+ * @module audit
22
+ */
23
+ // ── AuditTrail ────────────────────────────────────────────────────────────────
24
+ class AuditTrail {
25
+ constructor(options = {}) {
26
+ this.options = {
27
+ output: options.output ?? { dest: 'console' },
28
+ pretty: options.pretty ?? false,
29
+ };
30
+ }
31
+ /** Log a completed scan. */
32
+ logScanComplete(report, extra) {
33
+ const record = {
34
+ timestamp: new Date().toISOString(),
35
+ event: report.verdict !== 'clean' ? 'threat.detected' : 'scan.complete',
36
+ verdict: report.verdict,
37
+ matchCount: report.matches?.length ?? 0,
38
+ durationMs: report.durationMs,
39
+ engine: report.engine,
40
+ mimeType: report.file?.mimeType,
41
+ ...extra,
42
+ };
43
+ void this.write(record);
44
+ }
45
+ /** Log a scan error. */
46
+ logScanError(error, extra) {
47
+ const record = {
48
+ timestamp: new Date().toISOString(),
49
+ event: 'scan.error',
50
+ verdict: 'clean', // unknown at this point
51
+ matchCount: 0,
52
+ error: error instanceof Error ? error.message : String(error),
53
+ ...extra,
54
+ };
55
+ void this.write(record);
56
+ }
57
+ /** Log a new quarantine entry. */
58
+ logQuarantine(entry, correlationId) {
59
+ const record = {
60
+ timestamp: new Date().toISOString(),
61
+ event: 'quarantine.created',
62
+ quarantineId: entry.id,
63
+ filename: entry.file.originalName,
64
+ sha256: entry.file.sha256,
65
+ uploadedBy: entry.file.uploadedBy,
66
+ correlationId,
67
+ };
68
+ void this.write(record);
69
+ }
70
+ /** Log a quarantine resolution (promote or delete). */
71
+ logQuarantineResolved(entry, correlationId) {
72
+ const record = {
73
+ timestamp: new Date().toISOString(),
74
+ event: entry.status === 'deleted' ? 'quarantine.deleted' : 'quarantine.resolved',
75
+ quarantineId: entry.id,
76
+ filename: entry.file.originalName,
77
+ sha256: entry.file.sha256,
78
+ decision: entry.status === 'promoted' ? 'promote' : 'delete',
79
+ reviewedBy: entry.reviewedBy,
80
+ reviewNote: entry.reviewNote,
81
+ correlationId,
82
+ };
83
+ void this.write(record);
84
+ }
85
+ async write(record) {
86
+ const line = this.options.pretty
87
+ ? JSON.stringify(record, null, 2)
88
+ : JSON.stringify(record);
89
+ const { output } = this.options;
90
+ try {
91
+ if (output.dest === 'console') {
92
+ process.stdout.write(line + '\n');
93
+ }
94
+ else if (output.dest === 'file') {
95
+ // Append a newline-delimited JSON (NDJSON) record.
96
+ await fs.promises.appendFile(output.path, line + '\n', 'utf8');
97
+ }
98
+ else if (output.dest === 'custom') {
99
+ await output.write(record);
100
+ }
101
+ }
102
+ catch {
103
+ // Audit failures must never interrupt the upload pipeline.
104
+ }
105
+ }
106
+ }
107
+
108
+ export { AuditTrail };
109
+ //# sourceMappingURL=pompelmi.audit.esm.js.map
@@ -0,0 +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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"}