pompelmi 0.34.10 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -15
- package/dist/pompelmi.audit.cjs +13 -15
- package/dist/pompelmi.audit.cjs.map +1 -1
- package/dist/pompelmi.audit.esm.js +13 -15
- package/dist/pompelmi.audit.esm.js.map +1 -1
- package/dist/pompelmi.browser.cjs +595 -557
- package/dist/pompelmi.browser.cjs.map +1 -1
- package/dist/pompelmi.browser.esm.js +595 -557
- package/dist/pompelmi.browser.esm.js.map +1 -1
- package/dist/pompelmi.cjs +2056 -2015
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +2056 -2015
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/pompelmi.hooks.cjs +2 -2
- package/dist/pompelmi.hooks.cjs.map +1 -1
- package/dist/pompelmi.hooks.esm.js +2 -2
- package/dist/pompelmi.hooks.esm.js.map +1 -1
- package/dist/pompelmi.policy-packs.cjs +74 -73
- package/dist/pompelmi.policy-packs.cjs.map +1 -1
- package/dist/pompelmi.policy-packs.esm.js +74 -73
- package/dist/pompelmi.policy-packs.esm.js.map +1 -1
- package/dist/pompelmi.quarantine.cjs +135 -133
- package/dist/pompelmi.quarantine.cjs.map +1 -1
- package/dist/pompelmi.quarantine.esm.js +135 -133
- package/dist/pompelmi.quarantine.esm.js.map +1 -1
- package/dist/pompelmi.react.cjs +595 -557
- package/dist/pompelmi.react.cjs.map +1 -1
- package/dist/pompelmi.react.esm.js +595 -557
- package/dist/pompelmi.react.esm.js.map +1 -1
- package/dist/types/audit.d.ts +12 -12
- package/dist/types/browser-index.d.ts +12 -12
- package/dist/types/config.d.ts +4 -4
- package/dist/types/engines/dynamic-taint.d.ts +1 -1
- package/dist/types/engines/hybrid-orchestrator.d.ts +1 -1
- package/dist/types/engines/hybrid-taint-integration.d.ts +6 -6
- package/dist/types/engines/taint-policies.d.ts +4 -4
- package/dist/types/hipaa-compliance.d.ts +2 -2
- package/dist/types/hooks.d.ts +2 -2
- package/dist/types/index.d.ts +20 -20
- package/dist/types/node/scanDir.d.ts +5 -5
- package/dist/types/policy-packs.d.ts +2 -2
- package/dist/types/presets.d.ts +3 -3
- package/dist/types/quarantine/index.d.ts +3 -3
- package/dist/types/quarantine/storage.d.ts +1 -1
- package/dist/types/quarantine/types.d.ts +3 -3
- package/dist/types/quarantine/workflow.d.ts +4 -4
- package/dist/types/react-index.d.ts +2 -2
- package/dist/types/risk.d.ts +1 -1
- package/dist/types/scan/remote.d.ts +2 -2
- package/dist/types/scan.d.ts +5 -5
- package/dist/types/scanners/common-heuristics.d.ts +1 -1
- package/dist/types/scanners/zip-bomb-guard.d.ts +1 -1
- package/dist/types/src/audit.d.ts +84 -0
- package/dist/types/src/browser-index.d.ts +29 -0
- package/dist/types/src/config.d.ts +143 -0
- package/dist/types/src/engines/dynamic-taint.d.ts +102 -0
- package/dist/types/src/engines/hybrid-orchestrator.d.ts +65 -0
- package/dist/types/src/engines/hybrid-taint-integration.d.ts +129 -0
- package/dist/types/src/engines/taint-policies.d.ts +84 -0
- package/dist/types/src/hipaa-compliance.d.ts +110 -0
- package/dist/types/src/hooks.d.ts +89 -0
- package/dist/types/src/index.d.ts +29 -0
- package/dist/types/src/magic.d.ts +7 -0
- package/dist/types/src/node/scanDir.d.ts +30 -0
- package/dist/types/src/policy-packs.d.ts +98 -0
- package/dist/types/src/policy.d.ts +12 -0
- package/dist/types/src/presets.d.ts +72 -0
- package/dist/types/src/quarantine/index.d.ts +18 -0
- package/dist/types/src/quarantine/storage.d.ts +77 -0
- package/dist/types/src/quarantine/types.d.ts +78 -0
- package/dist/types/src/quarantine/workflow.d.ts +97 -0
- package/dist/types/src/react-index.d.ts +13 -0
- package/dist/types/src/risk.d.ts +18 -0
- package/dist/types/src/scan/remote.d.ts +12 -0
- package/dist/types/src/scan.d.ts +17 -0
- package/dist/types/src/scanners/common-heuristics.d.ts +14 -0
- package/dist/types/src/scanners/zip-bomb-guard.d.ts +9 -0
- package/dist/types/src/scanners/zipTraversalGuard.d.ts +19 -0
- package/dist/types/src/stream.d.ts +10 -0
- package/dist/types/src/types/decompilation.d.ts +96 -0
- package/dist/types/src/types/taint-tracking.d.ts +495 -0
- package/dist/types/src/types.d.ts +48 -0
- package/dist/types/src/useFileScanner.d.ts +15 -0
- package/dist/types/src/utils/advanced-detection.d.ts +21 -0
- package/dist/types/src/utils/batch-scanner.d.ts +62 -0
- package/dist/types/src/utils/cache-manager.d.ts +95 -0
- package/dist/types/src/utils/export.d.ts +51 -0
- package/dist/types/src/utils/performance-metrics.d.ts +68 -0
- package/dist/types/src/utils/threat-intelligence.d.ts +96 -0
- package/dist/types/src/validate.d.ts +7 -0
- package/dist/types/src/verdict.d.ts +2 -0
- package/dist/types/src/yara/browser.d.ts +7 -0
- package/dist/types/src/yara/index.d.ts +17 -0
- package/dist/types/src/yara/node.d.ts +2 -0
- package/dist/types/src/yara/remote.d.ts +10 -0
- package/dist/types/src/yara-bridge.d.ts +3 -0
- package/dist/types/src/zip.d.ts +13 -0
- package/dist/types/types/decompilation.d.ts +4 -4
- package/dist/types/types/taint-tracking.d.ts +19 -19
- package/dist/types/types.d.ts +3 -3
- package/dist/types/useFileScanner.d.ts +1 -1
- package/dist/types/utils/advanced-detection.d.ts +1 -1
- package/dist/types/utils/batch-scanner.d.ts +3 -3
- package/dist/types/utils/cache-manager.d.ts +1 -1
- package/dist/types/utils/export.d.ts +2 -2
- package/dist/types/utils/threat-intelligence.d.ts +4 -4
- package/dist/types/verdict.d.ts +1 -1
- package/dist/types/yara/browser.d.ts +1 -1
- package/dist/types/yara/index.d.ts +1 -1
- package/dist/types/yara/node.d.ts +1 -1
- package/dist/types/yara/remote.d.ts +2 -2
- package/package.json +7 -7
|
@@ -1,8 +1,238 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
|
|
3
|
+
const MB$1 = 1024 * 1024;
|
|
4
|
+
const DEFAULT_POLICY = {
|
|
5
|
+
includeExtensions: ["zip", "png", "jpg", "jpeg", "pdf"],
|
|
6
|
+
allowedMimeTypes: ["application/zip", "image/png", "image/jpeg", "application/pdf", "text/plain"],
|
|
7
|
+
maxFileSizeBytes: 20 * MB$1,
|
|
8
|
+
timeoutMs: 5000,
|
|
9
|
+
concurrency: 4,
|
|
10
|
+
failClosed: true,
|
|
11
|
+
};
|
|
12
|
+
function definePolicy(input = {}) {
|
|
13
|
+
const p = { ...DEFAULT_POLICY, ...input };
|
|
14
|
+
if (!Array.isArray(p.includeExtensions))
|
|
15
|
+
throw new TypeError("includeExtensions must be string[]");
|
|
16
|
+
if (!Array.isArray(p.allowedMimeTypes))
|
|
17
|
+
throw new TypeError("allowedMimeTypes must be string[]");
|
|
18
|
+
if (!(Number.isFinite(p.maxFileSizeBytes) && p.maxFileSizeBytes > 0))
|
|
19
|
+
throw new TypeError("maxFileSizeBytes must be > 0");
|
|
20
|
+
if (!(Number.isFinite(p.timeoutMs) && p.timeoutMs > 0))
|
|
21
|
+
throw new TypeError("timeoutMs must be > 0");
|
|
22
|
+
if (!(Number.isInteger(p.concurrency) && p.concurrency > 0))
|
|
23
|
+
throw new TypeError("concurrency must be > 0");
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Policy packs for Pompelmi.
|
|
29
|
+
*
|
|
30
|
+
* Pre-configured, named policies for common upload scenarios. Each pack
|
|
31
|
+
* defines the file type allowlist, size limits, and timeout appropriate for
|
|
32
|
+
* its use case.
|
|
33
|
+
*
|
|
34
|
+
* All packs are built on `definePolicy` and are fully overridable:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { POLICY_PACKS } from 'pompelmi/policy-packs';
|
|
38
|
+
*
|
|
39
|
+
* // Use a pack as-is:
|
|
40
|
+
* const policy = POLICY_PACKS['images-only'];
|
|
41
|
+
*
|
|
42
|
+
* // Or override individual fields:
|
|
43
|
+
* import { definePolicy } from 'pompelmi';
|
|
44
|
+
* const custom = definePolicy({ ...POLICY_PACKS['documents-only'], maxFileSizeBytes: 5 * 1024 * 1024 });
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* These packs are *deterministic* and *descriptor-based* — they do not
|
|
48
|
+
* depend on any external threat intelligence feed.
|
|
49
|
+
*
|
|
50
|
+
* @module policy-packs
|
|
51
|
+
*/
|
|
52
|
+
const KB = 1024;
|
|
53
|
+
const MB = 1024 * KB;
|
|
54
|
+
// ── Policy packs ──────────────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Documents-only policy.
|
|
57
|
+
*
|
|
58
|
+
* Appropriate for: document management APIs, PDF/Office file upload endpoints,
|
|
59
|
+
* data import pipelines.
|
|
60
|
+
*
|
|
61
|
+
* Allowed: PDF, Word (.docx/.doc), Excel (.xlsx/.xls), PowerPoint (.pptx/.ppt),
|
|
62
|
+
* CSV, plain text, JSON, YAML, ODT/ODS/ODP (OpenDocument).
|
|
63
|
+
* Max size: 25 MB.
|
|
64
|
+
*/
|
|
65
|
+
const DOCUMENTS_ONLY = definePolicy({
|
|
66
|
+
includeExtensions: [
|
|
67
|
+
"pdf",
|
|
68
|
+
"doc",
|
|
69
|
+
"docx",
|
|
70
|
+
"xls",
|
|
71
|
+
"xlsx",
|
|
72
|
+
"ppt",
|
|
73
|
+
"pptx",
|
|
74
|
+
"odt",
|
|
75
|
+
"ods",
|
|
76
|
+
"odp",
|
|
77
|
+
"csv",
|
|
78
|
+
"txt",
|
|
79
|
+
"json",
|
|
80
|
+
"yaml",
|
|
81
|
+
"yml",
|
|
82
|
+
"md",
|
|
83
|
+
],
|
|
84
|
+
allowedMimeTypes: [
|
|
85
|
+
"application/pdf",
|
|
86
|
+
"application/msword",
|
|
87
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
88
|
+
"application/vnd.ms-excel",
|
|
89
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
90
|
+
"application/vnd.ms-powerpoint",
|
|
91
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
92
|
+
"application/vnd.oasis.opendocument.text",
|
|
93
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
94
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
95
|
+
"text/csv",
|
|
96
|
+
"text/plain",
|
|
97
|
+
"application/json",
|
|
98
|
+
"text/yaml",
|
|
99
|
+
"text/markdown",
|
|
100
|
+
],
|
|
101
|
+
maxFileSizeBytes: 25 * MB,
|
|
102
|
+
timeoutMs: 10000,
|
|
103
|
+
concurrency: 4,
|
|
104
|
+
failClosed: true,
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* Images-only policy.
|
|
108
|
+
*
|
|
109
|
+
* Appropriate for: avatar uploads, product image APIs, content platforms with
|
|
110
|
+
* user-generated imagery.
|
|
111
|
+
*
|
|
112
|
+
* Allowed: JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP, ICO.
|
|
113
|
+
* Max size: 10 MB.
|
|
114
|
+
* Note: SVG is intentionally excluded — inline SVGs can contain scripts.
|
|
115
|
+
*/
|
|
116
|
+
const IMAGES_ONLY = definePolicy({
|
|
117
|
+
includeExtensions: ["jpg", "jpeg", "png", "gif", "webp", "avif", "tiff", "tif", "bmp", "ico"],
|
|
118
|
+
allowedMimeTypes: [
|
|
119
|
+
"image/jpeg",
|
|
120
|
+
"image/png",
|
|
121
|
+
"image/gif",
|
|
122
|
+
"image/webp",
|
|
123
|
+
"image/avif",
|
|
124
|
+
"image/tiff",
|
|
125
|
+
"image/bmp",
|
|
126
|
+
"image/x-icon",
|
|
127
|
+
"image/vnd.microsoft.icon",
|
|
128
|
+
],
|
|
129
|
+
maxFileSizeBytes: 10 * MB,
|
|
130
|
+
timeoutMs: 5000,
|
|
131
|
+
concurrency: 8,
|
|
132
|
+
failClosed: true,
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* Strict public-upload policy.
|
|
136
|
+
*
|
|
137
|
+
* Appropriate for: anonymous or low-trust upload endpoints, public APIs,
|
|
138
|
+
* any surface exposed to untrusted users.
|
|
139
|
+
*
|
|
140
|
+
* Aggressive size limit (5 MB), short timeout, fail-closed, narrow MIME
|
|
141
|
+
* allowlist. Only allows plain images and PDF.
|
|
142
|
+
*/
|
|
143
|
+
const STRICT_PUBLIC_UPLOAD = definePolicy({
|
|
144
|
+
includeExtensions: ["jpg", "jpeg", "png", "webp", "pdf"],
|
|
145
|
+
allowedMimeTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
|
|
146
|
+
maxFileSizeBytes: 5 * MB,
|
|
147
|
+
timeoutMs: 4000,
|
|
148
|
+
concurrency: 2,
|
|
149
|
+
failClosed: true,
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* Conservative default policy.
|
|
153
|
+
*
|
|
154
|
+
* A hardened version of the built-in `DEFAULT_POLICY` suitable for
|
|
155
|
+
* production without further customisation. Stricter size limit and
|
|
156
|
+
* shorter timeout than the permissive default.
|
|
157
|
+
*/
|
|
158
|
+
const CONSERVATIVE_DEFAULT = definePolicy({
|
|
159
|
+
includeExtensions: ["zip", "png", "jpg", "jpeg", "pdf", "txt", "csv", "docx", "xlsx"],
|
|
160
|
+
allowedMimeTypes: [
|
|
161
|
+
"application/zip",
|
|
162
|
+
"image/png",
|
|
163
|
+
"image/jpeg",
|
|
164
|
+
"application/pdf",
|
|
165
|
+
"text/plain",
|
|
166
|
+
"text/csv",
|
|
167
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
168
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
169
|
+
],
|
|
170
|
+
maxFileSizeBytes: 10 * MB,
|
|
171
|
+
timeoutMs: 8000,
|
|
172
|
+
concurrency: 4,
|
|
173
|
+
failClosed: true,
|
|
174
|
+
});
|
|
175
|
+
/**
|
|
176
|
+
* Archives policy.
|
|
177
|
+
*
|
|
178
|
+
* Appropriate for: endpoints that accept ZIP, tar, or compressed archives.
|
|
179
|
+
* Combines a generous size allowance with a longer timeout for deep inspection.
|
|
180
|
+
*
|
|
181
|
+
* NOTE: Pair this policy with `createZipBombGuard()` to defend against
|
|
182
|
+
* decompression-bomb attacks:
|
|
183
|
+
*
|
|
184
|
+
* ```ts
|
|
185
|
+
* import { composeScanners, createZipBombGuard, CommonHeuristicsScanner } from 'pompelmi';
|
|
186
|
+
* const scanner = composeScanners(
|
|
187
|
+
* [['zipGuard', createZipBombGuard()], ['heuristics', CommonHeuristicsScanner]]
|
|
188
|
+
* );
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
const ARCHIVES = definePolicy({
|
|
192
|
+
includeExtensions: ["zip", "tar", "gz", "tgz", "bz2", "xz", "7z", "rar"],
|
|
193
|
+
allowedMimeTypes: [
|
|
194
|
+
"application/zip",
|
|
195
|
+
"application/x-tar",
|
|
196
|
+
"application/gzip",
|
|
197
|
+
"application/x-bzip2",
|
|
198
|
+
"application/x-xz",
|
|
199
|
+
"application/x-7z-compressed",
|
|
200
|
+
"application/x-rar-compressed",
|
|
201
|
+
],
|
|
202
|
+
maxFileSizeBytes: 100 * MB,
|
|
203
|
+
timeoutMs: 30000,
|
|
204
|
+
concurrency: 2,
|
|
205
|
+
failClosed: true,
|
|
206
|
+
});
|
|
207
|
+
/**
|
|
208
|
+
* Named map of all built-in policy packs.
|
|
209
|
+
*
|
|
210
|
+
* ```ts
|
|
211
|
+
* import { POLICY_PACKS } from 'pompelmi/policy-packs';
|
|
212
|
+
* const policy = POLICY_PACKS['strict-public-upload'];
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
const POLICY_PACKS = {
|
|
216
|
+
"documents-only": DOCUMENTS_ONLY,
|
|
217
|
+
"images-only": IMAGES_ONLY,
|
|
218
|
+
"strict-public-upload": STRICT_PUBLIC_UPLOAD,
|
|
219
|
+
"conservative-default": CONSERVATIVE_DEFAULT,
|
|
220
|
+
archives: ARCHIVES,
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Look up a policy pack by name.
|
|
224
|
+
* Throws if the name is not recognised.
|
|
225
|
+
*/
|
|
226
|
+
function getPolicyPack(name) {
|
|
227
|
+
const policy = POLICY_PACKS[name];
|
|
228
|
+
if (!policy)
|
|
229
|
+
throw new Error(`Unknown policy pack: '${name}'. Valid names: ${Object.keys(POLICY_PACKS).join(", ")}`);
|
|
230
|
+
return policy;
|
|
231
|
+
}
|
|
232
|
+
|
|
3
233
|
function hasAsciiToken(buf, token) {
|
|
4
234
|
// Use latin1 so we can safely search binary
|
|
5
|
-
return buf.indexOf(token, 0,
|
|
235
|
+
return buf.indexOf(token, 0, "latin1") !== -1;
|
|
6
236
|
}
|
|
7
237
|
function startsWith(buf, bytes) {
|
|
8
238
|
if (buf.length < bytes.length)
|
|
@@ -18,7 +248,7 @@ function isPDF(buf) {
|
|
|
18
248
|
}
|
|
19
249
|
function isOleCfb(buf) {
|
|
20
250
|
// D0 CF 11 E0 A1 B1 1A E1
|
|
21
|
-
const sig = [
|
|
251
|
+
const sig = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
|
|
22
252
|
return startsWith(buf, sig);
|
|
23
253
|
}
|
|
24
254
|
function isZipLike$1(buf) {
|
|
@@ -33,12 +263,12 @@ function isPeExecutable(buf) {
|
|
|
33
263
|
function hasOoxmlMacros(buf) {
|
|
34
264
|
if (!isZipLike$1(buf))
|
|
35
265
|
return false;
|
|
36
|
-
return hasAsciiToken(buf,
|
|
266
|
+
return hasAsciiToken(buf, "vbaProject.bin");
|
|
37
267
|
}
|
|
38
268
|
/** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
|
|
39
269
|
function pdfRiskTokens(buf) {
|
|
40
|
-
const tokens = [
|
|
41
|
-
return tokens.filter(t => hasAsciiToken(buf, t));
|
|
270
|
+
const tokens = ["/JavaScript", "/OpenAction", "/AA", "/Launch"];
|
|
271
|
+
return tokens.filter((t) => hasAsciiToken(buf, t));
|
|
42
272
|
}
|
|
43
273
|
const CommonHeuristicsScanner = {
|
|
44
274
|
async scan(input) {
|
|
@@ -46,33 +276,37 @@ const CommonHeuristicsScanner = {
|
|
|
46
276
|
const matches = [];
|
|
47
277
|
// Office macros (OLE / OOXML)
|
|
48
278
|
if (isOleCfb(buf)) {
|
|
49
|
-
matches.push({ rule:
|
|
279
|
+
matches.push({ rule: "office_ole_container", severity: "suspicious" });
|
|
50
280
|
}
|
|
51
281
|
if (hasOoxmlMacros(buf)) {
|
|
52
|
-
matches.push({ rule:
|
|
282
|
+
matches.push({ rule: "office_ooxml_macros", severity: "suspicious" });
|
|
53
283
|
}
|
|
54
284
|
// PDF risky tokens
|
|
55
285
|
if (isPDF(buf)) {
|
|
56
286
|
const toks = pdfRiskTokens(buf);
|
|
57
287
|
if (toks.length) {
|
|
58
288
|
matches.push({
|
|
59
|
-
rule:
|
|
60
|
-
severity:
|
|
61
|
-
meta: { tokens: toks }
|
|
289
|
+
rule: "pdf_risky_actions",
|
|
290
|
+
severity: "suspicious",
|
|
291
|
+
meta: { tokens: toks },
|
|
62
292
|
});
|
|
63
293
|
}
|
|
64
294
|
}
|
|
65
295
|
// Executable header
|
|
66
296
|
if (isPeExecutable(buf)) {
|
|
67
|
-
matches.push({ rule:
|
|
297
|
+
matches.push({ rule: "pe_executable_signature", severity: "suspicious" });
|
|
68
298
|
}
|
|
69
299
|
// EICAR test file
|
|
70
300
|
const EICAR_NEEDLE = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!";
|
|
71
301
|
if (hasAsciiToken(buf, EICAR_NEEDLE)) {
|
|
72
|
-
matches.push({
|
|
302
|
+
matches.push({
|
|
303
|
+
rule: "eicar_test_file",
|
|
304
|
+
severity: "high",
|
|
305
|
+
meta: { note: "EICAR standard antivirus test file detected" },
|
|
306
|
+
});
|
|
73
307
|
}
|
|
74
308
|
return matches;
|
|
75
|
-
}
|
|
309
|
+
},
|
|
76
310
|
};
|
|
77
311
|
|
|
78
312
|
function toScanFn(s) {
|
|
@@ -111,7 +345,13 @@ async function runWithTimeout(fn, timeoutMs) {
|
|
|
111
345
|
return fn();
|
|
112
346
|
return new Promise((resolve, reject) => {
|
|
113
347
|
const timer = setTimeout(() => reject(new Error("scanner timeout")), timeoutMs);
|
|
114
|
-
fn().then((v) => {
|
|
348
|
+
fn().then((v) => {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
resolve(v);
|
|
351
|
+
}, (e) => {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
reject(e);
|
|
354
|
+
});
|
|
115
355
|
});
|
|
116
356
|
}
|
|
117
357
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -122,7 +362,9 @@ function composeScanners(...args) {
|
|
|
122
362
|
if (Array.isArray(first) &&
|
|
123
363
|
(first.length === 0 || (Array.isArray(first[0]) && typeof first[0][0] === "string"))) {
|
|
124
364
|
const entries = first;
|
|
125
|
-
const opts = rest.length > 0 &&
|
|
365
|
+
const opts = rest.length > 0 &&
|
|
366
|
+
!Array.isArray(rest[0]) &&
|
|
367
|
+
typeof rest[0] !== "function" &&
|
|
126
368
|
!(typeof rest[0] === "object" && rest[0] !== null && "scan" in rest[0])
|
|
127
369
|
? rest[0]
|
|
128
370
|
: {};
|
|
@@ -130,7 +372,7 @@ function composeScanners(...args) {
|
|
|
130
372
|
const all = [];
|
|
131
373
|
if (opts.parallel) {
|
|
132
374
|
// Parallel execution — collect all results then return
|
|
133
|
-
const results = await Promise.allSettled(entries.map(([
|
|
375
|
+
const results = await Promise.allSettled(entries.map(([_name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
|
|
134
376
|
for (let i = 0; i < results.length; i++) {
|
|
135
377
|
const result = results[i];
|
|
136
378
|
if (result.status === "fulfilled" && Array.isArray(result.value)) {
|
|
@@ -184,152 +426,61 @@ function composeScanners(...args) {
|
|
|
184
426
|
};
|
|
185
427
|
}
|
|
186
428
|
function createPresetScanner(preset, opts = {}) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
scanners.push(CommonHeuristicsScanner);
|
|
429
|
+
const baseScanners = [CommonHeuristicsScanner];
|
|
430
|
+
const dynamicScannerPromises = [];
|
|
190
431
|
// Add decompilation scanners based on preset
|
|
191
|
-
if (preset ===
|
|
192
|
-
preset ===
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
pythonPath: opts.pythonPath,
|
|
205
|
-
binaryNinjaPath: opts.binaryNinjaPath
|
|
206
|
-
});
|
|
207
|
-
scanners.push(binjaScanner);
|
|
208
|
-
}).catch(() => {
|
|
209
|
-
// Binary Ninja engine not available - silently skip
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// Engine not installed
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (!opts.decompilationEngine || opts.decompilationEngine === 'ghidra-pcode' || opts.decompilationEngine === 'both') {
|
|
217
|
-
try {
|
|
218
|
-
// Dynamic import for Ghidra engine (when implemented) - using Function to bypass TypeScript type checking
|
|
219
|
-
const importModule = new Function('specifier', 'return import(specifier)');
|
|
220
|
-
importModule('@pompelmi/engine-ghidra').then((mod) => {
|
|
221
|
-
const ghidraScanner = mod.createGhidraScanner({
|
|
222
|
-
timeout: opts.decompilationTimeout || opts.timeout || 30000,
|
|
223
|
-
depth,
|
|
224
|
-
ghidraPath: opts.ghidraPath,
|
|
225
|
-
analyzeHeadless: opts.analyzeHeadless
|
|
226
|
-
});
|
|
227
|
-
scanners.push(ghidraScanner);
|
|
228
|
-
}).catch(() => {
|
|
229
|
-
// Ghidra engine not available - silently skip
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
// Engine not installed
|
|
234
|
-
}
|
|
432
|
+
if (preset === "decompilation-basic" ||
|
|
433
|
+
preset === "decompilation-deep" ||
|
|
434
|
+
preset === "malware-analysis" ||
|
|
435
|
+
opts.enableDecompilation) {
|
|
436
|
+
const depth = preset === "decompilation-deep" || preset === "malware-analysis"
|
|
437
|
+
? "deep"
|
|
438
|
+
: preset === "decompilation-basic"
|
|
439
|
+
? "basic"
|
|
440
|
+
: opts.decompilationDepth || "basic";
|
|
441
|
+
let importModule;
|
|
442
|
+
try {
|
|
443
|
+
// Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
|
|
444
|
+
importModule = new Function("specifier", "return import(specifier)");
|
|
235
445
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Fallback scanner that returns no matches
|
|
239
|
-
return async (_input, _ctx) => {
|
|
240
|
-
return [];
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
return composeScanners(...scanners);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Performance monitoring utilities for pompelmi scans
|
|
248
|
-
* @module utils/performance-metrics
|
|
249
|
-
*/
|
|
250
|
-
/**
|
|
251
|
-
* Track performance metrics for a scan operation
|
|
252
|
-
*/
|
|
253
|
-
class PerformanceTracker {
|
|
254
|
-
constructor() {
|
|
255
|
-
this.checkpoints = new Map();
|
|
256
|
-
this.startTime = Date.now();
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Mark a checkpoint in the scan process
|
|
260
|
-
*/
|
|
261
|
-
checkpoint(name) {
|
|
262
|
-
this.checkpoints.set(name, Date.now());
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Get duration since start or since a specific checkpoint
|
|
266
|
-
*/
|
|
267
|
-
getDuration(since) {
|
|
268
|
-
const now = Date.now();
|
|
269
|
-
if (since && this.checkpoints.has(since)) {
|
|
270
|
-
return now - (this.checkpoints.get(since) ?? now);
|
|
446
|
+
catch {
|
|
447
|
+
importModule = undefined;
|
|
271
448
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
: undefined,
|
|
285
|
-
yaraDurationMs: this.checkpoints.has('yara_end')
|
|
286
|
-
? (this.checkpoints.get('yara_end') ?? 0) - (this.checkpoints.get('yara_start') ?? 0)
|
|
287
|
-
: undefined,
|
|
288
|
-
prepDurationMs: this.checkpoints.has('prep_end')
|
|
289
|
-
? (this.checkpoints.get('prep_end') ?? 0) - this.startTime
|
|
290
|
-
: undefined,
|
|
291
|
-
throughputBps: throughput,
|
|
292
|
-
bytesScanned,
|
|
293
|
-
startedAt: this.startTime,
|
|
294
|
-
completedAt: Date.now(),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Aggregate statistics from multiple scan reports
|
|
300
|
-
*/
|
|
301
|
-
function aggregateScanStats(reports) {
|
|
302
|
-
let cleanCount = 0;
|
|
303
|
-
let suspiciousCount = 0;
|
|
304
|
-
let maliciousCount = 0;
|
|
305
|
-
let totalDuration = 0;
|
|
306
|
-
let totalBytes = 0;
|
|
307
|
-
let validDurationCount = 0;
|
|
308
|
-
for (const report of reports) {
|
|
309
|
-
if (report.verdict === 'clean')
|
|
310
|
-
cleanCount++;
|
|
311
|
-
else if (report.verdict === 'suspicious')
|
|
312
|
-
suspiciousCount++;
|
|
313
|
-
else if (report.verdict === 'malicious')
|
|
314
|
-
maliciousCount++;
|
|
315
|
-
if (report.durationMs !== undefined) {
|
|
316
|
-
totalDuration += report.durationMs;
|
|
317
|
-
validDurationCount++;
|
|
449
|
+
if (importModule &&
|
|
450
|
+
(!opts.decompilationEngine ||
|
|
451
|
+
opts.decompilationEngine === "binaryninja-hlil" ||
|
|
452
|
+
opts.decompilationEngine === "both")) {
|
|
453
|
+
dynamicScannerPromises.push(importModule("@pompelmi/engine-binaryninja")
|
|
454
|
+
.then((mod) => mod.createBinaryNinjaScanner({
|
|
455
|
+
timeout: opts.decompilationTimeout || opts.timeout || 30000,
|
|
456
|
+
depth,
|
|
457
|
+
pythonPath: opts.pythonPath,
|
|
458
|
+
binaryNinjaPath: opts.binaryNinjaPath,
|
|
459
|
+
}))
|
|
460
|
+
.catch(() => null));
|
|
318
461
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
462
|
+
if (importModule &&
|
|
463
|
+
(!opts.decompilationEngine ||
|
|
464
|
+
opts.decompilationEngine === "ghidra-pcode" ||
|
|
465
|
+
opts.decompilationEngine === "both")) {
|
|
466
|
+
dynamicScannerPromises.push(importModule("@pompelmi/engine-ghidra")
|
|
467
|
+
.then((mod) => mod.createGhidraScanner({
|
|
468
|
+
timeout: opts.decompilationTimeout || opts.timeout || 30000,
|
|
469
|
+
depth,
|
|
470
|
+
ghidraPath: opts.ghidraPath,
|
|
471
|
+
analyzeHeadless: opts.analyzeHeadless,
|
|
472
|
+
}))
|
|
473
|
+
.catch(() => null));
|
|
321
474
|
}
|
|
322
475
|
}
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
avgThroughputBps: avgThroughput,
|
|
332
|
-
totalBytesScanned: totalBytes,
|
|
476
|
+
let composedScannerPromise;
|
|
477
|
+
const getComposedScanner = async () => {
|
|
478
|
+
composedScannerPromise ?? (composedScannerPromise = Promise.all(dynamicScannerPromises).then((dynamicScanners) => composeScanners(...baseScanners, ...dynamicScanners.filter((scanner) => scanner !== null))));
|
|
479
|
+
return composedScannerPromise;
|
|
480
|
+
};
|
|
481
|
+
return async (input, ctx) => {
|
|
482
|
+
const scanner = await getComposedScanner();
|
|
483
|
+
return scanner(input, ctx);
|
|
333
484
|
};
|
|
334
485
|
}
|
|
335
486
|
|
|
@@ -346,25 +497,25 @@ function detectPolyglot(bytes) {
|
|
|
346
497
|
// Check for PDF/ZIP polyglot
|
|
347
498
|
if (isPDFZipPolyglot(bytes)) {
|
|
348
499
|
matches.push({
|
|
349
|
-
rule:
|
|
350
|
-
severity:
|
|
351
|
-
meta: { description:
|
|
500
|
+
rule: "polyglot_pdf_zip",
|
|
501
|
+
severity: "high",
|
|
502
|
+
meta: { description: "File can be interpreted as both PDF and ZIP" },
|
|
352
503
|
});
|
|
353
504
|
}
|
|
354
505
|
// Check for image/script polyglot
|
|
355
506
|
if (isImageScriptPolyglot(bytes)) {
|
|
356
507
|
matches.push({
|
|
357
|
-
rule:
|
|
358
|
-
severity:
|
|
359
|
-
meta: { description:
|
|
508
|
+
rule: "polyglot_image_script",
|
|
509
|
+
severity: "high",
|
|
510
|
+
meta: { description: "Image file contains executable script content" },
|
|
360
511
|
});
|
|
361
512
|
}
|
|
362
513
|
// Check for GIFAR (GIF/JAR polyglot)
|
|
363
514
|
if (isGIFAR(bytes)) {
|
|
364
515
|
matches.push({
|
|
365
|
-
rule:
|
|
366
|
-
severity:
|
|
367
|
-
meta: { description:
|
|
516
|
+
rule: "polyglot_gifar",
|
|
517
|
+
severity: "critical",
|
|
518
|
+
meta: { description: "GIF file contains Java archive" },
|
|
368
519
|
});
|
|
369
520
|
}
|
|
370
521
|
return matches;
|
|
@@ -374,7 +525,7 @@ function detectPolyglot(bytes) {
|
|
|
374
525
|
*/
|
|
375
526
|
function detectObfuscatedScripts(bytes) {
|
|
376
527
|
const matches = [];
|
|
377
|
-
const text = new TextDecoder(
|
|
528
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
|
|
378
529
|
// Check for common obfuscation patterns
|
|
379
530
|
const obfuscationPatterns = [
|
|
380
531
|
/eval\s*\(\s*unescape\s*\(/gi,
|
|
@@ -386,10 +537,10 @@ function detectObfuscatedScripts(bytes) {
|
|
|
386
537
|
for (const pattern of obfuscationPatterns) {
|
|
387
538
|
if (pattern.test(text)) {
|
|
388
539
|
matches.push({
|
|
389
|
-
rule:
|
|
390
|
-
severity:
|
|
540
|
+
rule: "obfuscated_script",
|
|
541
|
+
severity: "medium",
|
|
391
542
|
meta: {
|
|
392
|
-
description:
|
|
543
|
+
description: "Detected obfuscated script content",
|
|
393
544
|
pattern: pattern.source,
|
|
394
545
|
},
|
|
395
546
|
});
|
|
@@ -429,7 +580,10 @@ function isPDFZipPolyglot(bytes) {
|
|
|
429
580
|
// Check for ZIP signature anywhere in the file
|
|
430
581
|
let hasZIP = false;
|
|
431
582
|
for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
|
|
432
|
-
if (bytes[i] === 0x50 &&
|
|
583
|
+
if (bytes[i] === 0x50 &&
|
|
584
|
+
bytes[i + 1] === 0x4b &&
|
|
585
|
+
bytes[i + 2] === 0x03 &&
|
|
586
|
+
bytes[i + 3] === 0x04) {
|
|
433
587
|
hasZIP = true;
|
|
434
588
|
break;
|
|
435
589
|
}
|
|
@@ -440,14 +594,13 @@ function isImageScriptPolyglot(bytes) {
|
|
|
440
594
|
if (bytes.length < 100)
|
|
441
595
|
return false;
|
|
442
596
|
// Check for image signatures
|
|
443
|
-
const isImage = (
|
|
444
|
-
(bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] ===
|
|
445
|
-
(bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) // GIF
|
|
446
|
-
);
|
|
597
|
+
const isImage = (bytes[0] === 0xff && bytes[1] === 0xd8) || // JPEG
|
|
598
|
+
(bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) || // PNG
|
|
599
|
+
(bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46); // GIF
|
|
447
600
|
if (!isImage)
|
|
448
601
|
return false;
|
|
449
602
|
// Check for script content
|
|
450
|
-
const text = new TextDecoder(
|
|
603
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
451
604
|
return /<script|javascript:|eval\(|function\s*\(/i.test(text);
|
|
452
605
|
}
|
|
453
606
|
function isGIFAR(bytes) {
|
|
@@ -458,7 +611,10 @@ function isGIFAR(bytes) {
|
|
|
458
611
|
// Check for ZIP/JAR signature
|
|
459
612
|
let hasZIP = false;
|
|
460
613
|
for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
|
|
461
|
-
if (bytes[i] === 0x50 &&
|
|
614
|
+
if (bytes[i] === 0x50 &&
|
|
615
|
+
bytes[i + 1] === 0x4b &&
|
|
616
|
+
bytes[i + 2] === 0x03 &&
|
|
617
|
+
bytes[i + 3] === 0x04) {
|
|
462
618
|
hasZIP = true;
|
|
463
619
|
break;
|
|
464
620
|
}
|
|
@@ -470,13 +626,13 @@ function isArchive(bytes) {
|
|
|
470
626
|
return false;
|
|
471
627
|
return (
|
|
472
628
|
// ZIP
|
|
473
|
-
(bytes[0] === 0x50 && bytes[1] ===
|
|
629
|
+
(bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) ||
|
|
474
630
|
// RAR
|
|
475
631
|
(bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
|
|
476
632
|
// 7z
|
|
477
|
-
(bytes[0] === 0x37 && bytes[1] ===
|
|
633
|
+
(bytes[0] === 0x37 && bytes[1] === 0x7a && bytes[2] === 0xbc && bytes[3] === 0xaf) ||
|
|
478
634
|
// tar.gz
|
|
479
|
-
(bytes[0] ===
|
|
635
|
+
(bytes[0] === 0x1f && bytes[1] === 0x8b));
|
|
480
636
|
}
|
|
481
637
|
|
|
482
638
|
/**
|
|
@@ -504,10 +660,10 @@ class ScanCacheManager {
|
|
|
504
660
|
* Generate cache key from file content
|
|
505
661
|
*/
|
|
506
662
|
generateKey(content, preset) {
|
|
507
|
-
const hash = createHash(
|
|
663
|
+
const hash = createHash("sha256")
|
|
508
664
|
.update(content)
|
|
509
|
-
.update(preset ||
|
|
510
|
-
.digest(
|
|
665
|
+
.update(preset || "default")
|
|
666
|
+
.digest("hex");
|
|
511
667
|
return hash;
|
|
512
668
|
}
|
|
513
669
|
/**
|
|
@@ -651,35 +807,132 @@ function getDefaultCache(options) {
|
|
|
651
807
|
return defaultCache;
|
|
652
808
|
}
|
|
653
809
|
|
|
654
|
-
/**
|
|
810
|
+
/**
|
|
811
|
+
* Performance monitoring utilities for pompelmi scans
|
|
812
|
+
* @module utils/performance-metrics
|
|
813
|
+
*/
|
|
814
|
+
/**
|
|
815
|
+
* Track performance metrics for a scan operation
|
|
816
|
+
*/
|
|
817
|
+
class PerformanceTracker {
|
|
818
|
+
constructor() {
|
|
819
|
+
this.checkpoints = new Map();
|
|
820
|
+
this.startTime = Date.now();
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Mark a checkpoint in the scan process
|
|
824
|
+
*/
|
|
825
|
+
checkpoint(name) {
|
|
826
|
+
this.checkpoints.set(name, Date.now());
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Get duration since start or since a specific checkpoint
|
|
830
|
+
*/
|
|
831
|
+
getDuration(since) {
|
|
832
|
+
const now = Date.now();
|
|
833
|
+
if (since && this.checkpoints.has(since)) {
|
|
834
|
+
return now - (this.checkpoints.get(since) ?? now);
|
|
835
|
+
}
|
|
836
|
+
return now - this.startTime;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Generate final metrics report
|
|
840
|
+
*/
|
|
841
|
+
getMetrics(bytesScanned) {
|
|
842
|
+
const totalDuration = this.getDuration();
|
|
843
|
+
const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
|
|
844
|
+
return {
|
|
845
|
+
totalDurationMs: totalDuration,
|
|
846
|
+
heuristicsDurationMs: this.checkpoints.has("heuristics_end")
|
|
847
|
+
? (this.checkpoints.get("heuristics_end") ?? 0) -
|
|
848
|
+
(this.checkpoints.get("heuristics_start") ?? 0)
|
|
849
|
+
: undefined,
|
|
850
|
+
yaraDurationMs: this.checkpoints.has("yara_end")
|
|
851
|
+
? (this.checkpoints.get("yara_end") ?? 0) - (this.checkpoints.get("yara_start") ?? 0)
|
|
852
|
+
: undefined,
|
|
853
|
+
prepDurationMs: this.checkpoints.has("prep_end")
|
|
854
|
+
? (this.checkpoints.get("prep_end") ?? 0) - this.startTime
|
|
855
|
+
: undefined,
|
|
856
|
+
throughputBps: throughput,
|
|
857
|
+
bytesScanned,
|
|
858
|
+
startedAt: this.startTime,
|
|
859
|
+
completedAt: Date.now(),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Aggregate statistics from multiple scan reports
|
|
865
|
+
*/
|
|
866
|
+
function aggregateScanStats(reports) {
|
|
867
|
+
let cleanCount = 0;
|
|
868
|
+
let suspiciousCount = 0;
|
|
869
|
+
let maliciousCount = 0;
|
|
870
|
+
let totalDuration = 0;
|
|
871
|
+
let totalBytes = 0;
|
|
872
|
+
let validDurationCount = 0;
|
|
873
|
+
for (const report of reports) {
|
|
874
|
+
if (report.verdict === "clean")
|
|
875
|
+
cleanCount++;
|
|
876
|
+
else if (report.verdict === "suspicious")
|
|
877
|
+
suspiciousCount++;
|
|
878
|
+
else if (report.verdict === "malicious")
|
|
879
|
+
maliciousCount++;
|
|
880
|
+
if (report.durationMs !== undefined) {
|
|
881
|
+
totalDuration += report.durationMs;
|
|
882
|
+
validDurationCount++;
|
|
883
|
+
}
|
|
884
|
+
if (report.file?.size !== undefined) {
|
|
885
|
+
totalBytes += report.file.size;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
|
|
889
|
+
const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
|
|
890
|
+
return {
|
|
891
|
+
totalScans: reports.length,
|
|
892
|
+
cleanCount,
|
|
893
|
+
suspiciousCount,
|
|
894
|
+
maliciousCount,
|
|
895
|
+
avgDurationMs: avgDuration,
|
|
896
|
+
avgThroughputBps: avgThroughput,
|
|
897
|
+
totalBytesScanned: totalBytes,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Mappa veloce estensione -> mime (basic) */
|
|
655
902
|
function guessMimeByExt(name) {
|
|
656
903
|
if (!name)
|
|
657
904
|
return;
|
|
658
|
-
const ext = name.toLowerCase().split(
|
|
905
|
+
const ext = name.toLowerCase().split(".").pop();
|
|
659
906
|
switch (ext) {
|
|
660
|
-
case
|
|
661
|
-
|
|
662
|
-
case
|
|
663
|
-
|
|
664
|
-
case
|
|
665
|
-
case
|
|
666
|
-
|
|
907
|
+
case "zip":
|
|
908
|
+
return "application/zip";
|
|
909
|
+
case "png":
|
|
910
|
+
return "image/png";
|
|
911
|
+
case "jpg":
|
|
912
|
+
case "jpeg":
|
|
913
|
+
return "image/jpeg";
|
|
914
|
+
case "pdf":
|
|
915
|
+
return "application/pdf";
|
|
916
|
+
case "txt":
|
|
917
|
+
return "text/plain";
|
|
918
|
+
default:
|
|
919
|
+
return;
|
|
667
920
|
}
|
|
668
921
|
}
|
|
669
922
|
/** Heuristica semplice per verdetto */
|
|
670
923
|
function computeVerdict(matches) {
|
|
671
924
|
if (!matches.length)
|
|
672
|
-
return
|
|
925
|
+
return "clean";
|
|
673
926
|
// se la regola contiene 'zip_' lo marchiamo "suspicious"
|
|
674
|
-
const anyHigh = matches.some(m => (m.tags ?? []).includes(
|
|
675
|
-
return anyHigh ?
|
|
927
|
+
const anyHigh = matches.some((m) => (m.tags ?? []).includes("critical") || (m.tags ?? []).includes("high"));
|
|
928
|
+
return anyHigh ? "malicious" : "suspicious";
|
|
676
929
|
}
|
|
677
930
|
/** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
|
|
678
931
|
function toYaraMatches(ms) {
|
|
679
|
-
return ms.map(m => ({
|
|
932
|
+
return ms.map((m) => ({
|
|
680
933
|
rule: m.rule,
|
|
681
|
-
namespace:
|
|
682
|
-
tags: [
|
|
934
|
+
namespace: "heuristics",
|
|
935
|
+
tags: ["heuristics"].concat(m.severity ? [m.severity] : []),
|
|
683
936
|
meta: m.meta,
|
|
684
937
|
}));
|
|
685
938
|
}
|
|
@@ -693,26 +946,28 @@ async function scanBytes(input, opts = {}) {
|
|
|
693
946
|
return cached;
|
|
694
947
|
}
|
|
695
948
|
}
|
|
696
|
-
const perfTracker =
|
|
949
|
+
const perfTracker = opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking
|
|
697
950
|
? new PerformanceTracker()
|
|
698
951
|
: null;
|
|
699
|
-
perfTracker?.checkpoint(
|
|
700
|
-
const preset = opts.preset ?? opts.config?.defaultPreset ??
|
|
952
|
+
perfTracker?.checkpoint("prep_start");
|
|
953
|
+
const preset = opts.preset ?? opts.config?.defaultPreset ?? "zip-basic";
|
|
701
954
|
const ctx = {
|
|
702
955
|
...opts.ctx,
|
|
703
956
|
mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
|
|
704
957
|
size: opts.ctx?.size ?? input.byteLength,
|
|
705
958
|
};
|
|
706
|
-
perfTracker?.checkpoint(
|
|
707
|
-
perfTracker?.checkpoint(
|
|
959
|
+
perfTracker?.checkpoint("prep_end");
|
|
960
|
+
perfTracker?.checkpoint("heuristics_start");
|
|
708
961
|
const scanFn = createPresetScanner(preset);
|
|
709
|
-
const matchesH = await (typeof scanFn === "function"
|
|
710
|
-
|
|
711
|
-
|
|
962
|
+
const matchesH = await (typeof scanFn === "function"
|
|
963
|
+
? scanFn
|
|
964
|
+
: scanFn.scan)(input, ctx);
|
|
965
|
+
const allMatches = [...matchesH];
|
|
966
|
+
perfTracker?.checkpoint("heuristics_end");
|
|
712
967
|
// Advanced detection (enabled by default, can be overridden by config)
|
|
713
968
|
const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
|
|
714
969
|
if (advancedEnabled) {
|
|
715
|
-
perfTracker?.checkpoint(
|
|
970
|
+
perfTracker?.checkpoint("advanced_start");
|
|
716
971
|
// Detect polyglot files
|
|
717
972
|
if (opts.config?.advanced?.enablePolyglotDetection !== false) {
|
|
718
973
|
const polyglotMatches = detectPolyglot(input);
|
|
@@ -727,37 +982,38 @@ async function scanBytes(input, opts = {}) {
|
|
|
727
982
|
if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
|
|
728
983
|
const nestingAnalysis = analyzeNestedArchives(input);
|
|
729
984
|
const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
|
|
730
|
-
if (nestingAnalysis.hasExcessiveNesting ||
|
|
985
|
+
if (nestingAnalysis.hasExcessiveNesting || nestingAnalysis.depth > maxDepth) {
|
|
731
986
|
allMatches.push({
|
|
732
|
-
rule:
|
|
733
|
-
severity:
|
|
987
|
+
rule: "excessive_archive_nesting",
|
|
988
|
+
severity: "high",
|
|
734
989
|
meta: {
|
|
735
|
-
description:
|
|
990
|
+
description: "Excessive archive nesting detected",
|
|
736
991
|
depth: nestingAnalysis.depth,
|
|
737
992
|
maxAllowed: maxDepth,
|
|
738
993
|
},
|
|
739
994
|
});
|
|
740
995
|
}
|
|
741
996
|
}
|
|
742
|
-
perfTracker?.checkpoint(
|
|
997
|
+
perfTracker?.checkpoint("advanced_end");
|
|
743
998
|
}
|
|
744
999
|
const matches = toYaraMatches(allMatches);
|
|
745
1000
|
const verdict = computeVerdict(matches);
|
|
746
1001
|
perfTracker ? perfTracker.getDuration() : Date.now();
|
|
747
1002
|
const durationMs = perfTracker ? perfTracker.getDuration() : 0;
|
|
748
1003
|
const report = {
|
|
749
|
-
ok: verdict ===
|
|
1004
|
+
ok: verdict === "clean",
|
|
750
1005
|
verdict,
|
|
751
1006
|
matches,
|
|
752
|
-
reasons: matches.map(m => m.rule),
|
|
1007
|
+
reasons: matches.map((m) => m.rule),
|
|
753
1008
|
file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
|
|
754
1009
|
durationMs,
|
|
755
|
-
engine:
|
|
1010
|
+
engine: "heuristics",
|
|
756
1011
|
truncated: false,
|
|
757
1012
|
timedOut: false,
|
|
758
1013
|
};
|
|
759
1014
|
// Add performance metrics if tracking enabled
|
|
760
|
-
if (perfTracker &&
|
|
1015
|
+
if (perfTracker &&
|
|
1016
|
+
(opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
|
|
761
1017
|
report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
|
|
762
1018
|
}
|
|
763
1019
|
// Cache result if enabled
|
|
@@ -771,10 +1027,7 @@ async function scanBytes(input, opts = {}) {
|
|
|
771
1027
|
}
|
|
772
1028
|
/** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
|
|
773
1029
|
async function scanFile(filePath, opts = {}) {
|
|
774
|
-
const [{ readFile, stat }, path] = await Promise.all([
|
|
775
|
-
import('fs/promises'),
|
|
776
|
-
import('path'),
|
|
777
|
-
]);
|
|
1030
|
+
const [{ readFile, stat }, path] = await Promise.all([import('fs/promises'), import('path')]);
|
|
778
1031
|
const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
|
|
779
1032
|
const ctx = {
|
|
780
1033
|
filename: path.basename(filePath),
|
|
@@ -798,21 +1051,6 @@ async function scanFiles(files, opts = {}) {
|
|
|
798
1051
|
return out;
|
|
799
1052
|
}
|
|
800
1053
|
|
|
801
|
-
/**
|
|
802
|
-
* Validates a File by MIME type and size (max 5 MB).
|
|
803
|
-
*/
|
|
804
|
-
function validateFile(file) {
|
|
805
|
-
const maxSize = 5 * 1024 * 1024;
|
|
806
|
-
const allowedTypes = ['text/plain', 'application/json', 'text/csv'];
|
|
807
|
-
if (!allowedTypes.includes(file.type)) {
|
|
808
|
-
return { valid: false, error: 'Unsupported file type' };
|
|
809
|
-
}
|
|
810
|
-
if (file.size > maxSize) {
|
|
811
|
-
return { valid: false, error: 'File too large (max 5 MB)' };
|
|
812
|
-
}
|
|
813
|
-
return { valid: true };
|
|
814
|
-
}
|
|
815
|
-
|
|
816
1054
|
const SIG_CEN = 0x02014b50;
|
|
817
1055
|
const DEFAULTS = {
|
|
818
1056
|
maxEntries: 1000,
|
|
@@ -829,7 +1067,7 @@ function r32(buf, off) {
|
|
|
829
1067
|
}
|
|
830
1068
|
function isZipLike(buf) {
|
|
831
1069
|
// local file header at start is common
|
|
832
|
-
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
|
|
1070
|
+
return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
|
|
833
1071
|
}
|
|
834
1072
|
function lastIndexOfEOCD(buf, window) {
|
|
835
1073
|
const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
|
@@ -838,7 +1076,7 @@ function lastIndexOfEOCD(buf, window) {
|
|
|
838
1076
|
return idx >= start ? idx : -1;
|
|
839
1077
|
}
|
|
840
1078
|
function hasTraversal(name) {
|
|
841
|
-
return name.includes(
|
|
1079
|
+
return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
|
|
842
1080
|
}
|
|
843
1081
|
function createZipBombGuard(opts = {}) {
|
|
844
1082
|
const cfg = { ...DEFAULTS, ...opts };
|
|
@@ -852,7 +1090,7 @@ function createZipBombGuard(opts = {}) {
|
|
|
852
1090
|
const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
|
|
853
1091
|
if (eocdPos < 0 || eocdPos + 22 > buf.length) {
|
|
854
1092
|
// ZIP but no EOCD — malformed or polyglot → suspicious
|
|
855
|
-
matches.push({ rule:
|
|
1093
|
+
matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
|
|
856
1094
|
return matches;
|
|
857
1095
|
}
|
|
858
1096
|
const totalEntries = r16(buf, eocdPos + 10);
|
|
@@ -860,7 +1098,7 @@ function createZipBombGuard(opts = {}) {
|
|
|
860
1098
|
const cdOffset = r32(buf, eocdPos + 16);
|
|
861
1099
|
// Bounds check
|
|
862
1100
|
if (cdOffset + cdSize > buf.length) {
|
|
863
|
-
matches.push({ rule:
|
|
1101
|
+
matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
|
|
864
1102
|
return matches;
|
|
865
1103
|
}
|
|
866
1104
|
// Iterate central directory entries
|
|
@@ -881,287 +1119,68 @@ function createZipBombGuard(opts = {}) {
|
|
|
881
1119
|
const nameEnd = nameStart + fnLen;
|
|
882
1120
|
if (nameEnd > buf.length)
|
|
883
1121
|
break;
|
|
884
|
-
const name = buf.toString(
|
|
1122
|
+
const name = buf.toString("utf8", nameStart, nameEnd);
|
|
885
1123
|
sumComp += compSize;
|
|
886
1124
|
sumUnc += uncSize;
|
|
887
1125
|
seen++;
|
|
888
1126
|
if (name.length > cfg.maxEntryNameLength) {
|
|
889
|
-
matches.push({
|
|
1127
|
+
matches.push({
|
|
1128
|
+
rule: "zip_entry_name_too_long",
|
|
1129
|
+
severity: "medium",
|
|
1130
|
+
meta: { name, length: name.length },
|
|
1131
|
+
});
|
|
890
1132
|
}
|
|
891
1133
|
if (hasTraversal(name)) {
|
|
892
|
-
matches.push({ rule:
|
|
1134
|
+
matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
|
|
893
1135
|
}
|
|
894
1136
|
// move to next entry
|
|
895
1137
|
ptr = nameEnd + exLen + cmLen;
|
|
896
1138
|
}
|
|
897
1139
|
if (seen !== totalEntries) {
|
|
898
1140
|
// central dir truncated/odd, still report what we found
|
|
899
|
-
matches.push({
|
|
1141
|
+
matches.push({
|
|
1142
|
+
rule: "zip_cd_truncated",
|
|
1143
|
+
severity: "medium",
|
|
1144
|
+
meta: { seen, totalEntries },
|
|
1145
|
+
});
|
|
900
1146
|
}
|
|
901
1147
|
// Heuristics thresholds
|
|
902
1148
|
if (seen > cfg.maxEntries) {
|
|
903
|
-
matches.push({
|
|
1149
|
+
matches.push({
|
|
1150
|
+
rule: "zip_too_many_entries",
|
|
1151
|
+
severity: "medium",
|
|
1152
|
+
meta: { seen, limit: cfg.maxEntries },
|
|
1153
|
+
});
|
|
904
1154
|
}
|
|
905
1155
|
if (sumUnc > cfg.maxTotalUncompressedBytes) {
|
|
906
1156
|
matches.push({
|
|
907
|
-
rule:
|
|
908
|
-
severity:
|
|
909
|
-
meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
|
|
1157
|
+
rule: "zip_total_uncompressed_too_large",
|
|
1158
|
+
severity: "medium",
|
|
1159
|
+
meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
|
|
910
1160
|
});
|
|
911
1161
|
}
|
|
912
1162
|
if (sumComp === 0 && sumUnc > 0) {
|
|
913
|
-
matches.push({
|
|
1163
|
+
matches.push({
|
|
1164
|
+
rule: "zip_suspicious_ratio",
|
|
1165
|
+
severity: "medium",
|
|
1166
|
+
meta: { ratio: Infinity },
|
|
1167
|
+
});
|
|
914
1168
|
}
|
|
915
1169
|
else if (sumComp > 0) {
|
|
916
1170
|
const ratio = sumUnc / Math.max(1, sumComp);
|
|
917
1171
|
if (ratio >= cfg.maxCompressionRatio) {
|
|
918
|
-
matches.push({
|
|
1172
|
+
matches.push({
|
|
1173
|
+
rule: "zip_suspicious_ratio",
|
|
1174
|
+
severity: "medium",
|
|
1175
|
+
meta: { ratio, limit: cfg.maxCompressionRatio },
|
|
1176
|
+
});
|
|
919
1177
|
}
|
|
920
1178
|
}
|
|
921
1179
|
return matches;
|
|
922
|
-
}
|
|
1180
|
+
},
|
|
923
1181
|
};
|
|
924
1182
|
}
|
|
925
1183
|
|
|
926
|
-
const MB$1 = 1024 * 1024;
|
|
927
|
-
const DEFAULT_POLICY = {
|
|
928
|
-
includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf'],
|
|
929
|
-
allowedMimeTypes: ['application/zip', 'image/png', 'image/jpeg', 'application/pdf', 'text/plain'],
|
|
930
|
-
maxFileSizeBytes: 20 * MB$1,
|
|
931
|
-
timeoutMs: 5000,
|
|
932
|
-
concurrency: 4,
|
|
933
|
-
failClosed: true
|
|
934
|
-
};
|
|
935
|
-
function definePolicy(input = {}) {
|
|
936
|
-
const p = { ...DEFAULT_POLICY, ...input };
|
|
937
|
-
if (!Array.isArray(p.includeExtensions))
|
|
938
|
-
throw new TypeError('includeExtensions must be string[]');
|
|
939
|
-
if (!Array.isArray(p.allowedMimeTypes))
|
|
940
|
-
throw new TypeError('allowedMimeTypes must be string[]');
|
|
941
|
-
if (!(Number.isFinite(p.maxFileSizeBytes) && p.maxFileSizeBytes > 0))
|
|
942
|
-
throw new TypeError('maxFileSizeBytes must be > 0');
|
|
943
|
-
if (!(Number.isFinite(p.timeoutMs) && p.timeoutMs > 0))
|
|
944
|
-
throw new TypeError('timeoutMs must be > 0');
|
|
945
|
-
if (!(Number.isInteger(p.concurrency) && p.concurrency > 0))
|
|
946
|
-
throw new TypeError('concurrency must be > 0');
|
|
947
|
-
return p;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Policy packs for Pompelmi.
|
|
952
|
-
*
|
|
953
|
-
* Pre-configured, named policies for common upload scenarios. Each pack
|
|
954
|
-
* defines the file type allowlist, size limits, and timeout appropriate for
|
|
955
|
-
* its use case.
|
|
956
|
-
*
|
|
957
|
-
* All packs are built on `definePolicy` and are fully overridable:
|
|
958
|
-
*
|
|
959
|
-
* ```ts
|
|
960
|
-
* import { POLICY_PACKS } from 'pompelmi/policy-packs';
|
|
961
|
-
*
|
|
962
|
-
* // Use a pack as-is:
|
|
963
|
-
* const policy = POLICY_PACKS['images-only'];
|
|
964
|
-
*
|
|
965
|
-
* // Or override individual fields:
|
|
966
|
-
* import { definePolicy } from 'pompelmi';
|
|
967
|
-
* const custom = definePolicy({ ...POLICY_PACKS['documents-only'], maxFileSizeBytes: 5 * 1024 * 1024 });
|
|
968
|
-
* ```
|
|
969
|
-
*
|
|
970
|
-
* These packs are *deterministic* and *descriptor-based* — they do not
|
|
971
|
-
* depend on any external threat intelligence feed.
|
|
972
|
-
*
|
|
973
|
-
* @module policy-packs
|
|
974
|
-
*/
|
|
975
|
-
const KB = 1024;
|
|
976
|
-
const MB = 1024 * KB;
|
|
977
|
-
// ── Policy packs ──────────────────────────────────────────────────────────────
|
|
978
|
-
/**
|
|
979
|
-
* Documents-only policy.
|
|
980
|
-
*
|
|
981
|
-
* Appropriate for: document management APIs, PDF/Office file upload endpoints,
|
|
982
|
-
* data import pipelines.
|
|
983
|
-
*
|
|
984
|
-
* Allowed: PDF, Word (.docx/.doc), Excel (.xlsx/.xls), PowerPoint (.pptx/.ppt),
|
|
985
|
-
* CSV, plain text, JSON, YAML, ODT/ODS/ODP (OpenDocument).
|
|
986
|
-
* Max size: 25 MB.
|
|
987
|
-
*/
|
|
988
|
-
const DOCUMENTS_ONLY = definePolicy({
|
|
989
|
-
includeExtensions: [
|
|
990
|
-
'pdf',
|
|
991
|
-
'doc', 'docx',
|
|
992
|
-
'xls', 'xlsx',
|
|
993
|
-
'ppt', 'pptx',
|
|
994
|
-
'odt', 'ods', 'odp',
|
|
995
|
-
'csv',
|
|
996
|
-
'txt',
|
|
997
|
-
'json',
|
|
998
|
-
'yaml', 'yml',
|
|
999
|
-
'md',
|
|
1000
|
-
],
|
|
1001
|
-
allowedMimeTypes: [
|
|
1002
|
-
'application/pdf',
|
|
1003
|
-
'application/msword',
|
|
1004
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1005
|
-
'application/vnd.ms-excel',
|
|
1006
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1007
|
-
'application/vnd.ms-powerpoint',
|
|
1008
|
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
1009
|
-
'application/vnd.oasis.opendocument.text',
|
|
1010
|
-
'application/vnd.oasis.opendocument.spreadsheet',
|
|
1011
|
-
'application/vnd.oasis.opendocument.presentation',
|
|
1012
|
-
'text/csv',
|
|
1013
|
-
'text/plain',
|
|
1014
|
-
'application/json',
|
|
1015
|
-
'text/yaml',
|
|
1016
|
-
'text/markdown',
|
|
1017
|
-
],
|
|
1018
|
-
maxFileSizeBytes: 25 * MB,
|
|
1019
|
-
timeoutMs: 10000,
|
|
1020
|
-
concurrency: 4,
|
|
1021
|
-
failClosed: true,
|
|
1022
|
-
});
|
|
1023
|
-
/**
|
|
1024
|
-
* Images-only policy.
|
|
1025
|
-
*
|
|
1026
|
-
* Appropriate for: avatar uploads, product image APIs, content platforms with
|
|
1027
|
-
* user-generated imagery.
|
|
1028
|
-
*
|
|
1029
|
-
* Allowed: JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP, ICO.
|
|
1030
|
-
* Max size: 10 MB.
|
|
1031
|
-
* Note: SVG is intentionally excluded — inline SVGs can contain scripts.
|
|
1032
|
-
*/
|
|
1033
|
-
const IMAGES_ONLY = definePolicy({
|
|
1034
|
-
includeExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'tiff', 'tif', 'bmp', 'ico'],
|
|
1035
|
-
allowedMimeTypes: [
|
|
1036
|
-
'image/jpeg',
|
|
1037
|
-
'image/png',
|
|
1038
|
-
'image/gif',
|
|
1039
|
-
'image/webp',
|
|
1040
|
-
'image/avif',
|
|
1041
|
-
'image/tiff',
|
|
1042
|
-
'image/bmp',
|
|
1043
|
-
'image/x-icon',
|
|
1044
|
-
'image/vnd.microsoft.icon',
|
|
1045
|
-
],
|
|
1046
|
-
maxFileSizeBytes: 10 * MB,
|
|
1047
|
-
timeoutMs: 5000,
|
|
1048
|
-
concurrency: 8,
|
|
1049
|
-
failClosed: true,
|
|
1050
|
-
});
|
|
1051
|
-
/**
|
|
1052
|
-
* Strict public-upload policy.
|
|
1053
|
-
*
|
|
1054
|
-
* Appropriate for: anonymous or low-trust upload endpoints, public APIs,
|
|
1055
|
-
* any surface exposed to untrusted users.
|
|
1056
|
-
*
|
|
1057
|
-
* Aggressive size limit (5 MB), short timeout, fail-closed, narrow MIME
|
|
1058
|
-
* allowlist. Only allows plain images and PDF.
|
|
1059
|
-
*/
|
|
1060
|
-
const STRICT_PUBLIC_UPLOAD = definePolicy({
|
|
1061
|
-
includeExtensions: ['jpg', 'jpeg', 'png', 'webp', 'pdf'],
|
|
1062
|
-
allowedMimeTypes: [
|
|
1063
|
-
'image/jpeg',
|
|
1064
|
-
'image/png',
|
|
1065
|
-
'image/webp',
|
|
1066
|
-
'application/pdf',
|
|
1067
|
-
],
|
|
1068
|
-
maxFileSizeBytes: 5 * MB,
|
|
1069
|
-
timeoutMs: 4000,
|
|
1070
|
-
concurrency: 2,
|
|
1071
|
-
failClosed: true,
|
|
1072
|
-
});
|
|
1073
|
-
/**
|
|
1074
|
-
* Conservative default policy.
|
|
1075
|
-
*
|
|
1076
|
-
* A hardened version of the built-in `DEFAULT_POLICY` suitable for
|
|
1077
|
-
* production without further customisation. Stricter size limit and
|
|
1078
|
-
* shorter timeout than the permissive default.
|
|
1079
|
-
*/
|
|
1080
|
-
const CONSERVATIVE_DEFAULT = definePolicy({
|
|
1081
|
-
includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf', 'txt', 'csv', 'docx', 'xlsx'],
|
|
1082
|
-
allowedMimeTypes: [
|
|
1083
|
-
'application/zip',
|
|
1084
|
-
'image/png',
|
|
1085
|
-
'image/jpeg',
|
|
1086
|
-
'application/pdf',
|
|
1087
|
-
'text/plain',
|
|
1088
|
-
'text/csv',
|
|
1089
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1090
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1091
|
-
],
|
|
1092
|
-
maxFileSizeBytes: 10 * MB,
|
|
1093
|
-
timeoutMs: 8000,
|
|
1094
|
-
concurrency: 4,
|
|
1095
|
-
failClosed: true,
|
|
1096
|
-
});
|
|
1097
|
-
/**
|
|
1098
|
-
* Archives policy.
|
|
1099
|
-
*
|
|
1100
|
-
* Appropriate for: endpoints that accept ZIP, tar, or compressed archives.
|
|
1101
|
-
* Combines a generous size allowance with a longer timeout for deep inspection.
|
|
1102
|
-
*
|
|
1103
|
-
* NOTE: Pair this policy with `createZipBombGuard()` to defend against
|
|
1104
|
-
* decompression-bomb attacks:
|
|
1105
|
-
*
|
|
1106
|
-
* ```ts
|
|
1107
|
-
* import { composeScanners, createZipBombGuard, CommonHeuristicsScanner } from 'pompelmi';
|
|
1108
|
-
* const scanner = composeScanners(
|
|
1109
|
-
* [['zipGuard', createZipBombGuard()], ['heuristics', CommonHeuristicsScanner]]
|
|
1110
|
-
* );
|
|
1111
|
-
* ```
|
|
1112
|
-
*/
|
|
1113
|
-
const ARCHIVES = definePolicy({
|
|
1114
|
-
includeExtensions: ['zip', 'tar', 'gz', 'tgz', 'bz2', 'xz', '7z', 'rar'],
|
|
1115
|
-
allowedMimeTypes: [
|
|
1116
|
-
'application/zip',
|
|
1117
|
-
'application/x-tar',
|
|
1118
|
-
'application/gzip',
|
|
1119
|
-
'application/x-bzip2',
|
|
1120
|
-
'application/x-xz',
|
|
1121
|
-
'application/x-7z-compressed',
|
|
1122
|
-
'application/x-rar-compressed',
|
|
1123
|
-
],
|
|
1124
|
-
maxFileSizeBytes: 100 * MB,
|
|
1125
|
-
timeoutMs: 30000,
|
|
1126
|
-
concurrency: 2,
|
|
1127
|
-
failClosed: true,
|
|
1128
|
-
});
|
|
1129
|
-
/**
|
|
1130
|
-
* Named map of all built-in policy packs.
|
|
1131
|
-
*
|
|
1132
|
-
* ```ts
|
|
1133
|
-
* import { POLICY_PACKS } from 'pompelmi/policy-packs';
|
|
1134
|
-
* const policy = POLICY_PACKS['strict-public-upload'];
|
|
1135
|
-
* ```
|
|
1136
|
-
*/
|
|
1137
|
-
const POLICY_PACKS = {
|
|
1138
|
-
'documents-only': DOCUMENTS_ONLY,
|
|
1139
|
-
'images-only': IMAGES_ONLY,
|
|
1140
|
-
'strict-public-upload': STRICT_PUBLIC_UPLOAD,
|
|
1141
|
-
'conservative-default': CONSERVATIVE_DEFAULT,
|
|
1142
|
-
'archives': ARCHIVES,
|
|
1143
|
-
};
|
|
1144
|
-
/**
|
|
1145
|
-
* Look up a policy pack by name.
|
|
1146
|
-
* Throws if the name is not recognised.
|
|
1147
|
-
*/
|
|
1148
|
-
function getPolicyPack(name) {
|
|
1149
|
-
const policy = POLICY_PACKS[name];
|
|
1150
|
-
if (!policy)
|
|
1151
|
-
throw new Error(`Unknown policy pack: '${name}'. Valid names: ${Object.keys(POLICY_PACKS).join(', ')}`);
|
|
1152
|
-
return policy;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
function mapMatchesToVerdict(matches = []) {
|
|
1156
|
-
if (!matches.length)
|
|
1157
|
-
return 'clean';
|
|
1158
|
-
const malHints = ['trojan', 'ransom', 'worm', 'spy', 'rootkit', 'keylog', 'botnet'];
|
|
1159
|
-
const tagSet = new Set(matches.flatMap(m => (m.tags ?? []).map(t => t.toLowerCase())));
|
|
1160
|
-
const nameHit = (r) => malHints.some(h => r.toLowerCase().includes(h));
|
|
1161
|
-
const isMal = matches.some(m => nameHit(m.rule)) || tagSet.has('malware') || tagSet.has('critical');
|
|
1162
|
-
return isMal ? 'malicious' : 'suspicious';
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1184
|
/**
|
|
1166
1185
|
* Export utilities for scan results
|
|
1167
1186
|
* @module utils/export
|
|
@@ -1177,19 +1196,15 @@ class ScanResultExporter {
|
|
|
1177
1196
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1178
1197
|
if (!options.includeDetails) {
|
|
1179
1198
|
// Simplified output
|
|
1180
|
-
const simplified = data.map(r => ({
|
|
1199
|
+
const simplified = data.map((r) => ({
|
|
1181
1200
|
verdict: r.verdict,
|
|
1182
1201
|
file: r.file?.name,
|
|
1183
1202
|
matches: r.matches.length,
|
|
1184
1203
|
durationMs: r.durationMs,
|
|
1185
1204
|
}));
|
|
1186
|
-
return options.prettyPrint
|
|
1187
|
-
? JSON.stringify(simplified, null, 2)
|
|
1188
|
-
: JSON.stringify(simplified);
|
|
1205
|
+
return options.prettyPrint ? JSON.stringify(simplified, null, 2) : JSON.stringify(simplified);
|
|
1189
1206
|
}
|
|
1190
|
-
return options.prettyPrint
|
|
1191
|
-
? JSON.stringify(data, null, 2)
|
|
1192
|
-
: JSON.stringify(data);
|
|
1207
|
+
return options.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
1193
1208
|
}
|
|
1194
1209
|
/**
|
|
1195
1210
|
* Export to CSV format
|
|
@@ -1197,68 +1212,68 @@ class ScanResultExporter {
|
|
|
1197
1212
|
toCSV(reports, options = {}) {
|
|
1198
1213
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1199
1214
|
const headers = [
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1215
|
+
"filename",
|
|
1216
|
+
"verdict",
|
|
1217
|
+
"matches_count",
|
|
1218
|
+
"file_size",
|
|
1219
|
+
"mime_type",
|
|
1220
|
+
"duration_ms",
|
|
1221
|
+
"engine",
|
|
1207
1222
|
];
|
|
1208
1223
|
if (options.includeDetails) {
|
|
1209
|
-
headers.push(
|
|
1224
|
+
headers.push("reasons", "match_rules");
|
|
1210
1225
|
}
|
|
1211
|
-
const rows = data.map(report => {
|
|
1226
|
+
const rows = data.map((report) => {
|
|
1212
1227
|
const row = [
|
|
1213
|
-
this.escapeCsv(report.file?.name ||
|
|
1228
|
+
this.escapeCsv(report.file?.name || "unknown"),
|
|
1214
1229
|
report.verdict,
|
|
1215
1230
|
report.matches.length.toString(),
|
|
1216
1231
|
(report.file?.size || 0).toString(),
|
|
1217
|
-
this.escapeCsv(report.file?.mimeType ||
|
|
1232
|
+
this.escapeCsv(report.file?.mimeType || "unknown"),
|
|
1218
1233
|
(report.durationMs || 0).toString(),
|
|
1219
|
-
report.engine ||
|
|
1234
|
+
report.engine || "unknown",
|
|
1220
1235
|
];
|
|
1221
1236
|
if (options.includeDetails) {
|
|
1222
|
-
row.push(this.escapeCsv((report.reasons || []).join(
|
|
1237
|
+
row.push(this.escapeCsv((report.reasons || []).join("; ")), this.escapeCsv(report.matches.map((m) => m.rule).join("; ")));
|
|
1223
1238
|
}
|
|
1224
|
-
return row.join(
|
|
1239
|
+
return row.join(",");
|
|
1225
1240
|
});
|
|
1226
|
-
return [headers.join(
|
|
1241
|
+
return [headers.join(","), ...rows].join("\n");
|
|
1227
1242
|
}
|
|
1228
1243
|
/**
|
|
1229
1244
|
* Export to Markdown format
|
|
1230
1245
|
*/
|
|
1231
1246
|
toMarkdown(reports, options = {}) {
|
|
1232
1247
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1233
|
-
let md =
|
|
1248
|
+
let md = "# Scan Results\n\n";
|
|
1234
1249
|
md += `**Total Scans:** ${data.length}\n\n`;
|
|
1235
|
-
const clean = data.filter(r => r.verdict ===
|
|
1236
|
-
const suspicious = data.filter(r => r.verdict ===
|
|
1237
|
-
const malicious = data.filter(r => r.verdict ===
|
|
1238
|
-
md +=
|
|
1250
|
+
const clean = data.filter((r) => r.verdict === "clean").length;
|
|
1251
|
+
const suspicious = data.filter((r) => r.verdict === "suspicious").length;
|
|
1252
|
+
const malicious = data.filter((r) => r.verdict === "malicious").length;
|
|
1253
|
+
md += "## Summary\n\n";
|
|
1239
1254
|
md += `- ✅ Clean: ${clean}\n`;
|
|
1240
1255
|
md += `- ⚠️ Suspicious: ${suspicious}\n`;
|
|
1241
1256
|
md += `- ❌ Malicious: ${malicious}\n\n`;
|
|
1242
|
-
md +=
|
|
1257
|
+
md += "## Detailed Results\n\n";
|
|
1243
1258
|
for (const report of data) {
|
|
1244
|
-
const icon = report.verdict ===
|
|
1245
|
-
md += `### ${icon} ${report.file?.name ||
|
|
1259
|
+
const icon = report.verdict === "clean" ? "✅" : report.verdict === "suspicious" ? "⚠️" : "❌";
|
|
1260
|
+
md += `### ${icon} ${report.file?.name || "Unknown"}\n\n`;
|
|
1246
1261
|
md += `- **Verdict:** ${report.verdict}\n`;
|
|
1247
1262
|
md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
|
|
1248
|
-
md += `- **MIME Type:** ${report.file?.mimeType ||
|
|
1263
|
+
md += `- **MIME Type:** ${report.file?.mimeType || "unknown"}\n`;
|
|
1249
1264
|
md += `- **Duration:** ${report.durationMs || 0}ms\n`;
|
|
1250
1265
|
md += `- **Matches:** ${report.matches.length}\n`;
|
|
1251
1266
|
if (options.includeDetails && report.matches.length > 0) {
|
|
1252
|
-
md +=
|
|
1267
|
+
md += "\n**Match Details:**\n";
|
|
1253
1268
|
for (const match of report.matches) {
|
|
1254
1269
|
md += `- ${match.rule}`;
|
|
1255
1270
|
if (match.tags && match.tags.length > 0) {
|
|
1256
|
-
md += ` (${match.tags.join(
|
|
1271
|
+
md += ` (${match.tags.join(", ")})`;
|
|
1257
1272
|
}
|
|
1258
|
-
md +=
|
|
1273
|
+
md += "\n";
|
|
1259
1274
|
}
|
|
1260
1275
|
}
|
|
1261
|
-
md +=
|
|
1276
|
+
md += "\n";
|
|
1262
1277
|
}
|
|
1263
1278
|
return md;
|
|
1264
1279
|
}
|
|
@@ -1268,20 +1283,20 @@ class ScanResultExporter {
|
|
|
1268
1283
|
*/
|
|
1269
1284
|
toSARIF(reports, options = {}) {
|
|
1270
1285
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1271
|
-
const results = data.flatMap(report => {
|
|
1272
|
-
if (report.verdict ===
|
|
1286
|
+
const results = data.flatMap((report) => {
|
|
1287
|
+
if (report.verdict === "clean")
|
|
1273
1288
|
return [];
|
|
1274
|
-
return report.matches.map(match => ({
|
|
1289
|
+
return report.matches.map((match) => ({
|
|
1275
1290
|
ruleId: match.rule,
|
|
1276
|
-
level: report.verdict ===
|
|
1291
|
+
level: report.verdict === "malicious" ? "error" : "warning",
|
|
1277
1292
|
message: {
|
|
1278
|
-
text: `${match.rule} detected in ${report.file?.name ||
|
|
1293
|
+
text: `${match.rule} detected in ${report.file?.name || "unknown file"}`,
|
|
1279
1294
|
},
|
|
1280
1295
|
locations: [
|
|
1281
1296
|
{
|
|
1282
1297
|
physicalLocation: {
|
|
1283
1298
|
artifactLocation: {
|
|
1284
|
-
uri: report.file?.name ||
|
|
1299
|
+
uri: report.file?.name || "unknown",
|
|
1285
1300
|
},
|
|
1286
1301
|
},
|
|
1287
1302
|
},
|
|
@@ -1293,33 +1308,31 @@ class ScanResultExporter {
|
|
|
1293
1308
|
}));
|
|
1294
1309
|
});
|
|
1295
1310
|
const sarif = {
|
|
1296
|
-
version:
|
|
1297
|
-
$schema:
|
|
1311
|
+
version: "2.1.0",
|
|
1312
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
1298
1313
|
runs: [
|
|
1299
1314
|
{
|
|
1300
1315
|
tool: {
|
|
1301
1316
|
driver: {
|
|
1302
|
-
name:
|
|
1303
|
-
version:
|
|
1304
|
-
informationUri:
|
|
1317
|
+
name: "Pompelmi",
|
|
1318
|
+
version: "0.29.0",
|
|
1319
|
+
informationUri: "https://pompelmi.github.io/pompelmi/",
|
|
1305
1320
|
},
|
|
1306
1321
|
},
|
|
1307
1322
|
results,
|
|
1308
1323
|
},
|
|
1309
1324
|
],
|
|
1310
1325
|
};
|
|
1311
|
-
return options.prettyPrint
|
|
1312
|
-
? JSON.stringify(sarif, null, 2)
|
|
1313
|
-
: JSON.stringify(sarif);
|
|
1326
|
+
return options.prettyPrint ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
|
|
1314
1327
|
}
|
|
1315
1328
|
/**
|
|
1316
1329
|
* Export to HTML format
|
|
1317
1330
|
*/
|
|
1318
1331
|
toHTML(reports, options = {}) {
|
|
1319
1332
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1320
|
-
const clean = data.filter(r => r.verdict ===
|
|
1321
|
-
const suspicious = data.filter(r => r.verdict ===
|
|
1322
|
-
const malicious = data.filter(r => r.verdict ===
|
|
1333
|
+
const clean = data.filter((r) => r.verdict === "clean").length;
|
|
1334
|
+
const suspicious = data.filter((r) => r.verdict === "suspicious").length;
|
|
1335
|
+
const malicious = data.filter((r) => r.verdict === "malicious").length;
|
|
1323
1336
|
let html = `<!DOCTYPE html>
|
|
1324
1337
|
<html lang="en">
|
|
1325
1338
|
<head>
|
|
@@ -1351,11 +1364,11 @@ class ScanResultExporter {
|
|
|
1351
1364
|
for (const report of data) {
|
|
1352
1365
|
const statusClass = report.verdict;
|
|
1353
1366
|
html += `<div class="result ${statusClass}">`;
|
|
1354
|
-
html += `<h3>${this.escapeHtml(report.file?.name ||
|
|
1367
|
+
html += `<h3>${this.escapeHtml(report.file?.name || "Unknown")}</h3>`;
|
|
1355
1368
|
html += `<table>`;
|
|
1356
1369
|
html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
|
|
1357
1370
|
html += `<tr><th>Size</th><td>${this.formatBytes(report.file?.size || 0)}</td></tr>`;
|
|
1358
|
-
html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType ||
|
|
1371
|
+
html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || "unknown")}</td></tr>`;
|
|
1359
1372
|
html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
|
|
1360
1373
|
html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
|
|
1361
1374
|
html += `</table>`;
|
|
@@ -1364,7 +1377,7 @@ class ScanResultExporter {
|
|
|
1364
1377
|
for (const match of report.matches) {
|
|
1365
1378
|
html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
|
|
1366
1379
|
if (match.tags && match.tags.length > 0) {
|
|
1367
|
-
html += ` ${match.tags.map(tag => `<span class="badge">${this.escapeHtml(tag)}</span>`).join(
|
|
1380
|
+
html += ` ${match.tags.map((tag) => `<span class="badge">${this.escapeHtml(tag)}</span>`).join("")}`;
|
|
1368
1381
|
}
|
|
1369
1382
|
html += `</li>`;
|
|
1370
1383
|
}
|
|
@@ -1380,41 +1393,41 @@ class ScanResultExporter {
|
|
|
1380
1393
|
*/
|
|
1381
1394
|
export(reports, format, options = {}) {
|
|
1382
1395
|
switch (format) {
|
|
1383
|
-
case
|
|
1396
|
+
case "json":
|
|
1384
1397
|
return this.toJSON(reports, options);
|
|
1385
|
-
case
|
|
1398
|
+
case "csv":
|
|
1386
1399
|
return this.toCSV(reports, options);
|
|
1387
|
-
case
|
|
1400
|
+
case "markdown":
|
|
1388
1401
|
return this.toMarkdown(reports, options);
|
|
1389
|
-
case
|
|
1402
|
+
case "html":
|
|
1390
1403
|
return this.toHTML(reports, options);
|
|
1391
|
-
case
|
|
1404
|
+
case "sarif":
|
|
1392
1405
|
return this.toSARIF(reports, options);
|
|
1393
1406
|
default:
|
|
1394
1407
|
throw new Error(`Unsupported export format: ${format}`);
|
|
1395
1408
|
}
|
|
1396
1409
|
}
|
|
1397
1410
|
escapeCsv(value) {
|
|
1398
|
-
if (value.includes(
|
|
1411
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
1399
1412
|
return `"${value.replace(/"/g, '""')}"`;
|
|
1400
1413
|
}
|
|
1401
1414
|
return value;
|
|
1402
1415
|
}
|
|
1403
1416
|
escapeHtml(value) {
|
|
1404
1417
|
return value
|
|
1405
|
-
.replace(/&/g,
|
|
1406
|
-
.replace(/</g,
|
|
1407
|
-
.replace(/>/g,
|
|
1408
|
-
.replace(/"/g,
|
|
1409
|
-
.replace(/'/g,
|
|
1418
|
+
.replace(/&/g, "&")
|
|
1419
|
+
.replace(/</g, "<")
|
|
1420
|
+
.replace(/>/g, ">")
|
|
1421
|
+
.replace(/"/g, """)
|
|
1422
|
+
.replace(/'/g, "'");
|
|
1410
1423
|
}
|
|
1411
1424
|
formatBytes(bytes) {
|
|
1412
1425
|
if (bytes === 0)
|
|
1413
|
-
return
|
|
1426
|
+
return "0 Bytes";
|
|
1414
1427
|
const k = 1024;
|
|
1415
|
-
const sizes = [
|
|
1428
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
1416
1429
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1417
|
-
return Math.round(bytes /
|
|
1430
|
+
return Math.round((bytes / k ** i) * 100) / 100 + " " + sizes[i];
|
|
1418
1431
|
}
|
|
1419
1432
|
}
|
|
1420
1433
|
/**
|
|
@@ -1425,5 +1438,30 @@ function exportScanResults(reports, format, options) {
|
|
|
1425
1438
|
return exporter.export(reports, format, options);
|
|
1426
1439
|
}
|
|
1427
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Validates a File by MIME type and size (max 5 MB).
|
|
1443
|
+
*/
|
|
1444
|
+
function validateFile(file) {
|
|
1445
|
+
const maxSize = 5 * 1024 * 1024;
|
|
1446
|
+
const allowedTypes = ["text/plain", "application/json", "text/csv"];
|
|
1447
|
+
if (!allowedTypes.includes(file.type)) {
|
|
1448
|
+
return { valid: false, error: "Unsupported file type" };
|
|
1449
|
+
}
|
|
1450
|
+
if (file.size > maxSize) {
|
|
1451
|
+
return { valid: false, error: "File too large (max 5 MB)" };
|
|
1452
|
+
}
|
|
1453
|
+
return { valid: true };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function mapMatchesToVerdict(matches = []) {
|
|
1457
|
+
if (!matches.length)
|
|
1458
|
+
return "clean";
|
|
1459
|
+
const malHints = ["trojan", "ransom", "worm", "spy", "rootkit", "keylog", "botnet"];
|
|
1460
|
+
const tagSet = new Set(matches.flatMap((m) => (m.tags ?? []).map((t) => t.toLowerCase())));
|
|
1461
|
+
const nameHit = (r) => malHints.some((h) => r.toLowerCase().includes(h));
|
|
1462
|
+
const isMal = matches.some((m) => nameHit(m.rule)) || tagSet.has("malware") || tagSet.has("critical");
|
|
1463
|
+
return isMal ? "malicious" : "suspicious";
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1428
1466
|
export { ARCHIVES, CONSERVATIVE_DEFAULT, CommonHeuristicsScanner, DEFAULT_POLICY, DOCUMENTS_ONLY, IMAGES_ONLY, POLICY_PACKS, PerformanceTracker, STRICT_PUBLIC_UPLOAD, ScanResultExporter, aggregateScanStats, analyzeNestedArchives, composeScanners, createPresetScanner, createZipBombGuard, definePolicy, detectObfuscatedScripts, detectPolyglot, exportScanResults, getPolicyPack, mapMatchesToVerdict, scanBytes, scanFile, scanFiles, validateFile };
|
|
1429
1467
|
//# sourceMappingURL=pompelmi.browser.esm.js.map
|