pompelmi 0.35.5 → 1.1.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.
- package/.claude/settings.local.json +45 -0
- package/LICENSE +12 -18
- package/README.md +174 -181
- package/eslint.config.mjs +8 -0
- package/package.json +26 -251
- package/src/ClamAVDatabaseUpdater.js +48 -0
- package/src/ClamAVInstaller.js +49 -0
- package/src/ClamAVScanner.js +37 -0
- package/src/ClamdScanner.js +81 -0
- package/src/InstallerCommand.js +11 -0
- package/src/config.js +22 -0
- package/src/constants.js +3 -0
- package/src/favicon.ico +0 -0
- package/src/grapefruit.png +0 -0
- package/src/index.js +5 -0
- package/test_out.txt +74 -0
- package/CHANGELOG.md +0 -71
- package/dist/pompelmi.audit.cjs +0 -128
- package/dist/pompelmi.audit.cjs.map +0 -1
- package/dist/pompelmi.audit.esm.js +0 -107
- package/dist/pompelmi.audit.esm.js.map +0 -1
- package/dist/pompelmi.browser.cjs +0 -1549
- package/dist/pompelmi.browser.cjs.map +0 -1
- package/dist/pompelmi.browser.esm.js +0 -1523
- package/dist/pompelmi.browser.esm.js.map +0 -1
- package/dist/pompelmi.cjs +0 -2591
- package/dist/pompelmi.cjs.map +0 -1
- package/dist/pompelmi.esm.js +0 -2525
- package/dist/pompelmi.esm.js.map +0 -1
- package/dist/pompelmi.hooks.cjs +0 -75
- package/dist/pompelmi.hooks.cjs.map +0 -1
- package/dist/pompelmi.hooks.esm.js +0 -72
- package/dist/pompelmi.hooks.esm.js.map +0 -1
- package/dist/pompelmi.policy-packs.cjs +0 -240
- package/dist/pompelmi.policy-packs.cjs.map +0 -1
- package/dist/pompelmi.policy-packs.esm.js +0 -232
- package/dist/pompelmi.policy-packs.esm.js.map +0 -1
- package/dist/pompelmi.quarantine.cjs +0 -317
- package/dist/pompelmi.quarantine.cjs.map +0 -1
- package/dist/pompelmi.quarantine.esm.js +0 -293
- package/dist/pompelmi.quarantine.esm.js.map +0 -1
- package/dist/pompelmi.react.cjs +0 -1580
- package/dist/pompelmi.react.cjs.map +0 -1
- package/dist/pompelmi.react.esm.js +0 -1553
- package/dist/pompelmi.react.esm.js.map +0 -1
- package/dist/types/audit.d.ts +0 -84
- package/dist/types/browser-index.d.ts +0 -29
- package/dist/types/config.d.ts +0 -143
- package/dist/types/engines/dynamic-taint.d.ts +0 -102
- package/dist/types/engines/hybrid-orchestrator.d.ts +0 -65
- package/dist/types/engines/hybrid-taint-integration.d.ts +0 -129
- package/dist/types/engines/taint-policies.d.ts +0 -84
- package/dist/types/hipaa-compliance.d.ts +0 -110
- package/dist/types/hooks.d.ts +0 -89
- package/dist/types/index.d.ts +0 -29
- package/dist/types/magic.d.ts +0 -7
- package/dist/types/node/scanDir.d.ts +0 -30
- package/dist/types/policy-packs.d.ts +0 -98
- package/dist/types/policy.d.ts +0 -12
- package/dist/types/presets.d.ts +0 -72
- package/dist/types/quarantine/index.d.ts +0 -18
- package/dist/types/quarantine/storage.d.ts +0 -77
- package/dist/types/quarantine/types.d.ts +0 -78
- package/dist/types/quarantine/workflow.d.ts +0 -97
- package/dist/types/react-index.d.ts +0 -13
- package/dist/types/risk.d.ts +0 -18
- package/dist/types/scan/remote.d.ts +0 -12
- package/dist/types/scan.d.ts +0 -17
- package/dist/types/scanners/common-heuristics.d.ts +0 -14
- package/dist/types/scanners/zip-bomb-guard.d.ts +0 -9
- package/dist/types/scanners/zipTraversalGuard.d.ts +0 -19
- package/dist/types/src/audit.d.ts +0 -84
- package/dist/types/src/browser-index.d.ts +0 -29
- package/dist/types/src/config.d.ts +0 -143
- package/dist/types/src/engines/dynamic-taint.d.ts +0 -102
- package/dist/types/src/engines/hybrid-orchestrator.d.ts +0 -65
- package/dist/types/src/engines/hybrid-taint-integration.d.ts +0 -129
- package/dist/types/src/engines/taint-policies.d.ts +0 -84
- package/dist/types/src/hipaa-compliance.d.ts +0 -110
- package/dist/types/src/hooks.d.ts +0 -89
- package/dist/types/src/index.d.ts +0 -29
- package/dist/types/src/magic.d.ts +0 -7
- package/dist/types/src/node/scanDir.d.ts +0 -30
- package/dist/types/src/policy-packs.d.ts +0 -98
- package/dist/types/src/policy.d.ts +0 -12
- package/dist/types/src/presets.d.ts +0 -72
- package/dist/types/src/quarantine/index.d.ts +0 -18
- package/dist/types/src/quarantine/storage.d.ts +0 -77
- package/dist/types/src/quarantine/types.d.ts +0 -78
- package/dist/types/src/quarantine/workflow.d.ts +0 -97
- package/dist/types/src/react-index.d.ts +0 -13
- package/dist/types/src/risk.d.ts +0 -18
- package/dist/types/src/scan/remote.d.ts +0 -12
- package/dist/types/src/scan.d.ts +0 -17
- package/dist/types/src/scanners/common-heuristics.d.ts +0 -14
- package/dist/types/src/scanners/zip-bomb-guard.d.ts +0 -11
- package/dist/types/src/scanners/zipTraversalGuard.d.ts +0 -19
- package/dist/types/src/stream.d.ts +0 -10
- package/dist/types/src/types/decompilation.d.ts +0 -96
- package/dist/types/src/types/taint-tracking.d.ts +0 -495
- package/dist/types/src/types.d.ts +0 -48
- package/dist/types/src/useFileScanner.d.ts +0 -15
- package/dist/types/src/utils/advanced-detection.d.ts +0 -21
- package/dist/types/src/utils/batch-scanner.d.ts +0 -62
- package/dist/types/src/utils/cache-manager.d.ts +0 -95
- package/dist/types/src/utils/export.d.ts +0 -51
- package/dist/types/src/utils/performance-metrics.d.ts +0 -68
- package/dist/types/src/utils/threat-intelligence.d.ts +0 -96
- package/dist/types/src/validate.d.ts +0 -7
- package/dist/types/src/verdict.d.ts +0 -2
- package/dist/types/src/yara/browser.d.ts +0 -7
- package/dist/types/src/yara/index.d.ts +0 -17
- package/dist/types/src/yara/node.d.ts +0 -2
- package/dist/types/src/yara/remote.d.ts +0 -10
- package/dist/types/src/yara-bridge.d.ts +0 -3
- package/dist/types/src/zip.d.ts +0 -13
- package/dist/types/stream.d.ts +0 -10
- package/dist/types/types/decompilation.d.ts +0 -96
- package/dist/types/types/taint-tracking.d.ts +0 -495
- package/dist/types/types.d.ts +0 -48
- package/dist/types/useFileScanner.d.ts +0 -15
- package/dist/types/utils/advanced-detection.d.ts +0 -21
- package/dist/types/utils/batch-scanner.d.ts +0 -62
- package/dist/types/utils/cache-manager.d.ts +0 -95
- package/dist/types/utils/export.d.ts +0 -51
- package/dist/types/utils/performance-metrics.d.ts +0 -68
- package/dist/types/utils/threat-intelligence.d.ts +0 -96
- package/dist/types/validate.d.ts +0 -7
- package/dist/types/verdict.d.ts +0 -2
- package/dist/types/yara/browser.d.ts +0 -7
- package/dist/types/yara/index.d.ts +0 -17
- package/dist/types/yara/node.d.ts +0 -2
- package/dist/types/yara/remote.d.ts +0 -10
- package/dist/types/yara-bridge.d.ts +0 -3
- package/dist/types/zip.d.ts +0 -13
package/CHANGELOG.md
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## Unreleased (main)
|
|
9
|
-
|
|
10
|
-
### Highlights
|
|
11
|
-
|
|
12
|
-
- Fixed `@pompelmi/fastify-plugin` multipart dependency wiring and removed an unnecessary promise hop.
|
|
13
|
-
- Refined the root README layout for faster first-run onboarding and clearer repo entry points.
|
|
14
|
-
- Refreshed README badges and demo media so the public repo surface is easier to scan quickly.
|
|
15
|
-
- Added verified mention badges to strengthen the top-level trust signals around adoption.
|
|
16
|
-
- Tightened onboarding copy across the repo surfaces to make the docs-and-examples path easier to follow.
|
|
17
|
-
|
|
18
|
-
### Notes
|
|
19
|
-
|
|
20
|
-
This section summarizes changes since the last tag: `v0.34.8`.
|
|
21
|
-
Post-tag activity is currently limited, so the highlights above also surface the most recent user-visible commits from the current `v0.34.x` line for context.
|
|
22
|
-
For full details, see GitHub Releases / tag diffs.
|
|
23
|
-
|
|
24
|
-
## [0.27.1] - 2026-01-26
|
|
25
|
-
|
|
26
|
-
### Security
|
|
27
|
-
- 🔐 **Critical Security Fixes**: Fixed 89 vulnerabilities (6 critical, 36 high, 35 moderate, 12 low)
|
|
28
|
-
- 🔐 **Dependency Updates**: Updated 26 package overrides including esbuild, vite, astro, next, body-parser, qs, lodash
|
|
29
|
-
- 🔐 **CVE Fixes**: Patched multiple CVEs in dependencies
|
|
30
|
-
|
|
31
|
-
### Fixed
|
|
32
|
-
- 🐛 Fixed GitHub Actions workflow with correct pnpm/action-setup SHA
|
|
33
|
-
- 🐛 Resolved CI/CD pipeline execution errors
|
|
34
|
-
|
|
35
|
-
## [0.27.0] - 2026-01-26
|
|
36
|
-
|
|
37
|
-
### Added
|
|
38
|
-
- 🚀 **Enhanced Performance Monitoring**: Added detailed performance metrics tracking for scan operations
|
|
39
|
-
- 🔒 **Advanced Threat Detection**: Improved heuristics engine with better polyglot file detection
|
|
40
|
-
- 📊 **Scan Statistics API**: New utility functions to aggregate and analyze scan results
|
|
41
|
-
- 🛡️ **Enhanced ZIP Bomb Protection**: Improved nested archive detection with configurable depth limits
|
|
42
|
-
- 🔍 **Content Analysis**: Advanced content inspection for embedded scripts and obfuscated code
|
|
43
|
-
- 📝 **Better TypeScript Types**: Enhanced type definitions for improved developer experience
|
|
44
|
-
- ⚡ **Async Performance**: Optimized async operations for better throughput
|
|
45
|
-
- 🎯 **Scan Context Enrichment**: Enhanced metadata collection during file scanning
|
|
46
|
-
|
|
47
|
-
### Improved
|
|
48
|
-
- 🔧 **Error Handling**: More descriptive error messages with actionable suggestions
|
|
49
|
-
- 📈 **Memory Efficiency**: Reduced memory footprint for large file operations
|
|
50
|
-
- 🚦 **CI/CD Pipeline**: Enhanced GitHub Actions workflows with better caching
|
|
51
|
-
- 📚 **Documentation**: Updated examples and API documentation
|
|
52
|
-
- 🧪 **Test Coverage**: Added comprehensive test cases for new features
|
|
53
|
-
|
|
54
|
-
### Fixed
|
|
55
|
-
- 🐛 Fixed edge cases in MIME type detection
|
|
56
|
-
- 🐛 Resolved memory leaks in stream processing
|
|
57
|
-
- 🐛 Corrected verdict mapping for multi-threaded scenarios
|
|
58
|
-
|
|
59
|
-
### Security
|
|
60
|
-
- 🔐 Updated dependencies to patch known vulnerabilities
|
|
61
|
-
- 🔐 Enhanced input validation for all public APIs
|
|
62
|
-
- 🔐 Improved sanitization for file metadata
|
|
63
|
-
|
|
64
|
-
## [0.26.0] - 2025-12-15
|
|
65
|
-
|
|
66
|
-
### Added
|
|
67
|
-
- Initial stable release with core scanning functionality
|
|
68
|
-
- YARA integration support
|
|
69
|
-
- ZIP bomb protection
|
|
70
|
-
- Framework adapters (Express, Koa, Fastify, Next.js)
|
|
71
|
-
- Browser and Node.js support
|
package/dist/pompelmi.audit.cjs
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
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 ? JSON.stringify(record, null, 2) : JSON.stringify(record);
|
|
108
|
-
const { output } = this.options;
|
|
109
|
-
try {
|
|
110
|
-
if (output.dest === "console") {
|
|
111
|
-
process.stdout.write(line + "\n");
|
|
112
|
-
}
|
|
113
|
-
else if (output.dest === "file") {
|
|
114
|
-
// Append a newline-delimited JSON (NDJSON) record.
|
|
115
|
-
await fs__namespace.promises.appendFile(output.path, line + "\n", "utf8");
|
|
116
|
-
}
|
|
117
|
-
else if (output.dest === "custom") {
|
|
118
|
-
await output.write(record);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// Audit failures must never interrupt the upload pipeline.
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
exports.AuditTrail = AuditTrail;
|
|
128
|
-
//# sourceMappingURL=pompelmi.audit.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|
|
@@ -1,107 +0,0 @@
|
|
|
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 ? JSON.stringify(record, null, 2) : JSON.stringify(record);
|
|
87
|
-
const { output } = this.options;
|
|
88
|
-
try {
|
|
89
|
-
if (output.dest === "console") {
|
|
90
|
-
process.stdout.write(line + "\n");
|
|
91
|
-
}
|
|
92
|
-
else if (output.dest === "file") {
|
|
93
|
-
// Append a newline-delimited JSON (NDJSON) record.
|
|
94
|
-
await fs.promises.appendFile(output.path, line + "\n", "utf8");
|
|
95
|
-
}
|
|
96
|
-
else if (output.dest === "custom") {
|
|
97
|
-
await output.write(record);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Audit failures must never interrupt the upload pipeline.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export { AuditTrail };
|
|
107
|
-
//# sourceMappingURL=pompelmi.audit.esm.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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":";;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,KAGC,EAAA;AAED,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,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;AAE3F,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,MAAM,EAAE,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;;;;"}
|