pompelmi 0.34.10 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +585 -534
- package/dist/pompelmi.browser.cjs.map +1 -1
- package/dist/pompelmi.browser.esm.js +585 -534
- package/dist/pompelmi.browser.esm.js.map +1 -1
- package/dist/pompelmi.cjs +2066 -2016
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +2066 -2016
- 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 +585 -534
- package/dist/pompelmi.react.cjs.map +1 -1
- package/dist/pompelmi.react.esm.js +585 -534
- 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 +6 -6
|
@@ -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
|
: {};
|
|
@@ -188,24 +430,32 @@ function createPresetScanner(preset, opts = {}) {
|
|
|
188
430
|
// Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
|
|
189
431
|
scanners.push(CommonHeuristicsScanner);
|
|
190
432
|
// Add decompilation scanners based on preset
|
|
191
|
-
if (preset ===
|
|
192
|
-
preset ===
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
433
|
+
if (preset === "decompilation-basic" ||
|
|
434
|
+
preset === "decompilation-deep" ||
|
|
435
|
+
preset === "malware-analysis" ||
|
|
436
|
+
opts.enableDecompilation) {
|
|
437
|
+
const depth = preset === "decompilation-deep"
|
|
438
|
+
? "deep"
|
|
439
|
+
: preset === "decompilation-basic"
|
|
440
|
+
? "basic"
|
|
441
|
+
: opts.decompilationDepth || "basic";
|
|
442
|
+
if (!opts.decompilationEngine ||
|
|
443
|
+
opts.decompilationEngine === "binaryninja-hlil" ||
|
|
444
|
+
opts.decompilationEngine === "both") {
|
|
197
445
|
try {
|
|
198
446
|
// Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
|
|
199
|
-
const importModule = new Function(
|
|
200
|
-
importModule(
|
|
447
|
+
const importModule = new Function("specifier", "return import(specifier)");
|
|
448
|
+
importModule("@pompelmi/engine-binaryninja")
|
|
449
|
+
.then((mod) => {
|
|
201
450
|
const binjaScanner = mod.createBinaryNinjaScanner({
|
|
202
451
|
timeout: opts.decompilationTimeout || opts.timeout || 30000,
|
|
203
452
|
depth,
|
|
204
453
|
pythonPath: opts.pythonPath,
|
|
205
|
-
binaryNinjaPath: opts.binaryNinjaPath
|
|
454
|
+
binaryNinjaPath: opts.binaryNinjaPath,
|
|
206
455
|
});
|
|
207
456
|
scanners.push(binjaScanner);
|
|
208
|
-
})
|
|
457
|
+
})
|
|
458
|
+
.catch(() => {
|
|
209
459
|
// Binary Ninja engine not available - silently skip
|
|
210
460
|
});
|
|
211
461
|
}
|
|
@@ -213,19 +463,23 @@ function createPresetScanner(preset, opts = {}) {
|
|
|
213
463
|
// Engine not installed
|
|
214
464
|
}
|
|
215
465
|
}
|
|
216
|
-
if (!opts.decompilationEngine ||
|
|
466
|
+
if (!opts.decompilationEngine ||
|
|
467
|
+
opts.decompilationEngine === "ghidra-pcode" ||
|
|
468
|
+
opts.decompilationEngine === "both") {
|
|
217
469
|
try {
|
|
218
470
|
// Dynamic import for Ghidra engine (when implemented) - using Function to bypass TypeScript type checking
|
|
219
|
-
const importModule = new Function(
|
|
220
|
-
importModule(
|
|
471
|
+
const importModule = new Function("specifier", "return import(specifier)");
|
|
472
|
+
importModule("@pompelmi/engine-ghidra")
|
|
473
|
+
.then((mod) => {
|
|
221
474
|
const ghidraScanner = mod.createGhidraScanner({
|
|
222
475
|
timeout: opts.decompilationTimeout || opts.timeout || 30000,
|
|
223
476
|
depth,
|
|
224
477
|
ghidraPath: opts.ghidraPath,
|
|
225
|
-
analyzeHeadless: opts.analyzeHeadless
|
|
478
|
+
analyzeHeadless: opts.analyzeHeadless,
|
|
226
479
|
});
|
|
227
480
|
scanners.push(ghidraScanner);
|
|
228
|
-
})
|
|
481
|
+
})
|
|
482
|
+
.catch(() => {
|
|
229
483
|
// Ghidra engine not available - silently skip
|
|
230
484
|
});
|
|
231
485
|
}
|
|
@@ -243,96 +497,6 @@ function createPresetScanner(preset, opts = {}) {
|
|
|
243
497
|
return composeScanners(...scanners);
|
|
244
498
|
}
|
|
245
499
|
|
|
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);
|
|
271
|
-
}
|
|
272
|
-
return now - this.startTime;
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Generate final metrics report
|
|
276
|
-
*/
|
|
277
|
-
getMetrics(bytesScanned) {
|
|
278
|
-
const totalDuration = this.getDuration();
|
|
279
|
-
const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
|
|
280
|
-
return {
|
|
281
|
-
totalDurationMs: totalDuration,
|
|
282
|
-
heuristicsDurationMs: this.checkpoints.has('heuristics_end')
|
|
283
|
-
? (this.checkpoints.get('heuristics_end') ?? 0) - (this.checkpoints.get('heuristics_start') ?? 0)
|
|
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++;
|
|
318
|
-
}
|
|
319
|
-
if (report.file?.size !== undefined) {
|
|
320
|
-
totalBytes += report.file.size;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
|
|
324
|
-
const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
|
|
325
|
-
return {
|
|
326
|
-
totalScans: reports.length,
|
|
327
|
-
cleanCount,
|
|
328
|
-
suspiciousCount,
|
|
329
|
-
maliciousCount,
|
|
330
|
-
avgDurationMs: avgDuration,
|
|
331
|
-
avgThroughputBps: avgThroughput,
|
|
332
|
-
totalBytesScanned: totalBytes,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
500
|
/**
|
|
337
501
|
* Advanced threat detection utilities
|
|
338
502
|
* @module utils/advanced-detection
|
|
@@ -346,25 +510,25 @@ function detectPolyglot(bytes) {
|
|
|
346
510
|
// Check for PDF/ZIP polyglot
|
|
347
511
|
if (isPDFZipPolyglot(bytes)) {
|
|
348
512
|
matches.push({
|
|
349
|
-
rule:
|
|
350
|
-
severity:
|
|
351
|
-
meta: { description:
|
|
513
|
+
rule: "polyglot_pdf_zip",
|
|
514
|
+
severity: "high",
|
|
515
|
+
meta: { description: "File can be interpreted as both PDF and ZIP" },
|
|
352
516
|
});
|
|
353
517
|
}
|
|
354
518
|
// Check for image/script polyglot
|
|
355
519
|
if (isImageScriptPolyglot(bytes)) {
|
|
356
520
|
matches.push({
|
|
357
|
-
rule:
|
|
358
|
-
severity:
|
|
359
|
-
meta: { description:
|
|
521
|
+
rule: "polyglot_image_script",
|
|
522
|
+
severity: "high",
|
|
523
|
+
meta: { description: "Image file contains executable script content" },
|
|
360
524
|
});
|
|
361
525
|
}
|
|
362
526
|
// Check for GIFAR (GIF/JAR polyglot)
|
|
363
527
|
if (isGIFAR(bytes)) {
|
|
364
528
|
matches.push({
|
|
365
|
-
rule:
|
|
366
|
-
severity:
|
|
367
|
-
meta: { description:
|
|
529
|
+
rule: "polyglot_gifar",
|
|
530
|
+
severity: "critical",
|
|
531
|
+
meta: { description: "GIF file contains Java archive" },
|
|
368
532
|
});
|
|
369
533
|
}
|
|
370
534
|
return matches;
|
|
@@ -374,7 +538,7 @@ function detectPolyglot(bytes) {
|
|
|
374
538
|
*/
|
|
375
539
|
function detectObfuscatedScripts(bytes) {
|
|
376
540
|
const matches = [];
|
|
377
|
-
const text = new TextDecoder(
|
|
541
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.slice(0, Math.min(64 * 1024, bytes.length)));
|
|
378
542
|
// Check for common obfuscation patterns
|
|
379
543
|
const obfuscationPatterns = [
|
|
380
544
|
/eval\s*\(\s*unescape\s*\(/gi,
|
|
@@ -386,10 +550,10 @@ function detectObfuscatedScripts(bytes) {
|
|
|
386
550
|
for (const pattern of obfuscationPatterns) {
|
|
387
551
|
if (pattern.test(text)) {
|
|
388
552
|
matches.push({
|
|
389
|
-
rule:
|
|
390
|
-
severity:
|
|
553
|
+
rule: "obfuscated_script",
|
|
554
|
+
severity: "medium",
|
|
391
555
|
meta: {
|
|
392
|
-
description:
|
|
556
|
+
description: "Detected obfuscated script content",
|
|
393
557
|
pattern: pattern.source,
|
|
394
558
|
},
|
|
395
559
|
});
|
|
@@ -429,7 +593,10 @@ function isPDFZipPolyglot(bytes) {
|
|
|
429
593
|
// Check for ZIP signature anywhere in the file
|
|
430
594
|
let hasZIP = false;
|
|
431
595
|
for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
|
|
432
|
-
if (bytes[i] === 0x50 &&
|
|
596
|
+
if (bytes[i] === 0x50 &&
|
|
597
|
+
bytes[i + 1] === 0x4b &&
|
|
598
|
+
bytes[i + 2] === 0x03 &&
|
|
599
|
+
bytes[i + 3] === 0x04) {
|
|
433
600
|
hasZIP = true;
|
|
434
601
|
break;
|
|
435
602
|
}
|
|
@@ -440,14 +607,13 @@ function isImageScriptPolyglot(bytes) {
|
|
|
440
607
|
if (bytes.length < 100)
|
|
441
608
|
return false;
|
|
442
609
|
// 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
|
-
);
|
|
610
|
+
const isImage = (bytes[0] === 0xff && bytes[1] === 0xd8) || // JPEG
|
|
611
|
+
(bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) || // PNG
|
|
612
|
+
(bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46); // GIF
|
|
447
613
|
if (!isImage)
|
|
448
614
|
return false;
|
|
449
615
|
// Check for script content
|
|
450
|
-
const text = new TextDecoder(
|
|
616
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
451
617
|
return /<script|javascript:|eval\(|function\s*\(/i.test(text);
|
|
452
618
|
}
|
|
453
619
|
function isGIFAR(bytes) {
|
|
@@ -458,7 +624,10 @@ function isGIFAR(bytes) {
|
|
|
458
624
|
// Check for ZIP/JAR signature
|
|
459
625
|
let hasZIP = false;
|
|
460
626
|
for (let i = 0; i < Math.min(bytes.length - 4, 1024); i++) {
|
|
461
|
-
if (bytes[i] === 0x50 &&
|
|
627
|
+
if (bytes[i] === 0x50 &&
|
|
628
|
+
bytes[i + 1] === 0x4b &&
|
|
629
|
+
bytes[i + 2] === 0x03 &&
|
|
630
|
+
bytes[i + 3] === 0x04) {
|
|
462
631
|
hasZIP = true;
|
|
463
632
|
break;
|
|
464
633
|
}
|
|
@@ -470,13 +639,13 @@ function isArchive(bytes) {
|
|
|
470
639
|
return false;
|
|
471
640
|
return (
|
|
472
641
|
// ZIP
|
|
473
|
-
(bytes[0] === 0x50 && bytes[1] ===
|
|
642
|
+
(bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04) ||
|
|
474
643
|
// RAR
|
|
475
644
|
(bytes[0] === 0x52 && bytes[1] === 0x61 && bytes[2] === 0x72 && bytes[3] === 0x21) ||
|
|
476
645
|
// 7z
|
|
477
|
-
(bytes[0] === 0x37 && bytes[1] ===
|
|
646
|
+
(bytes[0] === 0x37 && bytes[1] === 0x7a && bytes[2] === 0xbc && bytes[3] === 0xaf) ||
|
|
478
647
|
// tar.gz
|
|
479
|
-
(bytes[0] ===
|
|
648
|
+
(bytes[0] === 0x1f && bytes[1] === 0x8b));
|
|
480
649
|
}
|
|
481
650
|
|
|
482
651
|
/**
|
|
@@ -504,10 +673,10 @@ class ScanCacheManager {
|
|
|
504
673
|
* Generate cache key from file content
|
|
505
674
|
*/
|
|
506
675
|
generateKey(content, preset) {
|
|
507
|
-
const hash = createHash(
|
|
676
|
+
const hash = createHash("sha256")
|
|
508
677
|
.update(content)
|
|
509
|
-
.update(preset ||
|
|
510
|
-
.digest(
|
|
678
|
+
.update(preset || "default")
|
|
679
|
+
.digest("hex");
|
|
511
680
|
return hash;
|
|
512
681
|
}
|
|
513
682
|
/**
|
|
@@ -651,35 +820,132 @@ function getDefaultCache(options) {
|
|
|
651
820
|
return defaultCache;
|
|
652
821
|
}
|
|
653
822
|
|
|
654
|
-
/**
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
823
|
+
/**
|
|
824
|
+
* Performance monitoring utilities for pompelmi scans
|
|
825
|
+
* @module utils/performance-metrics
|
|
826
|
+
*/
|
|
827
|
+
/**
|
|
828
|
+
* Track performance metrics for a scan operation
|
|
829
|
+
*/
|
|
830
|
+
class PerformanceTracker {
|
|
831
|
+
constructor() {
|
|
832
|
+
this.checkpoints = new Map();
|
|
833
|
+
this.startTime = Date.now();
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Mark a checkpoint in the scan process
|
|
837
|
+
*/
|
|
838
|
+
checkpoint(name) {
|
|
839
|
+
this.checkpoints.set(name, Date.now());
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Get duration since start or since a specific checkpoint
|
|
843
|
+
*/
|
|
844
|
+
getDuration(since) {
|
|
845
|
+
const now = Date.now();
|
|
846
|
+
if (since && this.checkpoints.has(since)) {
|
|
847
|
+
return now - (this.checkpoints.get(since) ?? now);
|
|
848
|
+
}
|
|
849
|
+
return now - this.startTime;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Generate final metrics report
|
|
853
|
+
*/
|
|
854
|
+
getMetrics(bytesScanned) {
|
|
855
|
+
const totalDuration = this.getDuration();
|
|
856
|
+
const throughput = totalDuration > 0 ? (bytesScanned / totalDuration) * 1000 : 0;
|
|
857
|
+
return {
|
|
858
|
+
totalDurationMs: totalDuration,
|
|
859
|
+
heuristicsDurationMs: this.checkpoints.has("heuristics_end")
|
|
860
|
+
? (this.checkpoints.get("heuristics_end") ?? 0) -
|
|
861
|
+
(this.checkpoints.get("heuristics_start") ?? 0)
|
|
862
|
+
: undefined,
|
|
863
|
+
yaraDurationMs: this.checkpoints.has("yara_end")
|
|
864
|
+
? (this.checkpoints.get("yara_end") ?? 0) - (this.checkpoints.get("yara_start") ?? 0)
|
|
865
|
+
: undefined,
|
|
866
|
+
prepDurationMs: this.checkpoints.has("prep_end")
|
|
867
|
+
? (this.checkpoints.get("prep_end") ?? 0) - this.startTime
|
|
868
|
+
: undefined,
|
|
869
|
+
throughputBps: throughput,
|
|
870
|
+
bytesScanned,
|
|
871
|
+
startedAt: this.startTime,
|
|
872
|
+
completedAt: Date.now(),
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Aggregate statistics from multiple scan reports
|
|
878
|
+
*/
|
|
879
|
+
function aggregateScanStats(reports) {
|
|
880
|
+
let cleanCount = 0;
|
|
881
|
+
let suspiciousCount = 0;
|
|
882
|
+
let maliciousCount = 0;
|
|
883
|
+
let totalDuration = 0;
|
|
884
|
+
let totalBytes = 0;
|
|
885
|
+
let validDurationCount = 0;
|
|
886
|
+
for (const report of reports) {
|
|
887
|
+
if (report.verdict === "clean")
|
|
888
|
+
cleanCount++;
|
|
889
|
+
else if (report.verdict === "suspicious")
|
|
890
|
+
suspiciousCount++;
|
|
891
|
+
else if (report.verdict === "malicious")
|
|
892
|
+
maliciousCount++;
|
|
893
|
+
if (report.durationMs !== undefined) {
|
|
894
|
+
totalDuration += report.durationMs;
|
|
895
|
+
validDurationCount++;
|
|
896
|
+
}
|
|
897
|
+
if (report.file?.size !== undefined) {
|
|
898
|
+
totalBytes += report.file.size;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const avgDuration = validDurationCount > 0 ? totalDuration / validDurationCount : 0;
|
|
902
|
+
const avgThroughput = totalDuration > 0 ? (totalBytes / totalDuration) * 1000 : 0;
|
|
903
|
+
return {
|
|
904
|
+
totalScans: reports.length,
|
|
905
|
+
cleanCount,
|
|
906
|
+
suspiciousCount,
|
|
907
|
+
maliciousCount,
|
|
908
|
+
avgDurationMs: avgDuration,
|
|
909
|
+
avgThroughputBps: avgThroughput,
|
|
910
|
+
totalBytesScanned: totalBytes,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Mappa veloce estensione -> mime (basic) */
|
|
915
|
+
function guessMimeByExt(name) {
|
|
916
|
+
if (!name)
|
|
917
|
+
return;
|
|
918
|
+
const ext = name.toLowerCase().split(".").pop();
|
|
919
|
+
switch (ext) {
|
|
920
|
+
case "zip":
|
|
921
|
+
return "application/zip";
|
|
922
|
+
case "png":
|
|
923
|
+
return "image/png";
|
|
924
|
+
case "jpg":
|
|
925
|
+
case "jpeg":
|
|
926
|
+
return "image/jpeg";
|
|
927
|
+
case "pdf":
|
|
928
|
+
return "application/pdf";
|
|
929
|
+
case "txt":
|
|
930
|
+
return "text/plain";
|
|
931
|
+
default:
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/** Heuristica semplice per verdetto */
|
|
936
|
+
function computeVerdict(matches) {
|
|
937
|
+
if (!matches.length)
|
|
938
|
+
return "clean";
|
|
939
|
+
// se la regola contiene 'zip_' lo marchiamo "suspicious"
|
|
940
|
+
const anyHigh = matches.some((m) => (m.tags ?? []).includes("critical") || (m.tags ?? []).includes("high"));
|
|
941
|
+
return anyHigh ? "malicious" : "suspicious";
|
|
942
|
+
}
|
|
943
|
+
/** Converte i Match (heuristics) in YaraMatch-like per uniformare l'output */
|
|
944
|
+
function toYaraMatches(ms) {
|
|
945
|
+
return ms.map((m) => ({
|
|
680
946
|
rule: m.rule,
|
|
681
|
-
namespace:
|
|
682
|
-
tags: [
|
|
947
|
+
namespace: "heuristics",
|
|
948
|
+
tags: ["heuristics"].concat(m.severity ? [m.severity] : []),
|
|
683
949
|
meta: m.meta,
|
|
684
950
|
}));
|
|
685
951
|
}
|
|
@@ -693,26 +959,28 @@ async function scanBytes(input, opts = {}) {
|
|
|
693
959
|
return cached;
|
|
694
960
|
}
|
|
695
961
|
}
|
|
696
|
-
const perfTracker =
|
|
962
|
+
const perfTracker = opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking
|
|
697
963
|
? new PerformanceTracker()
|
|
698
964
|
: null;
|
|
699
|
-
perfTracker?.checkpoint(
|
|
700
|
-
const preset = opts.preset ?? opts.config?.defaultPreset ??
|
|
965
|
+
perfTracker?.checkpoint("prep_start");
|
|
966
|
+
const preset = opts.preset ?? opts.config?.defaultPreset ?? "zip-basic";
|
|
701
967
|
const ctx = {
|
|
702
968
|
...opts.ctx,
|
|
703
969
|
mimeType: opts.ctx?.mimeType ?? guessMimeByExt(opts.ctx?.filename),
|
|
704
970
|
size: opts.ctx?.size ?? input.byteLength,
|
|
705
971
|
};
|
|
706
|
-
perfTracker?.checkpoint(
|
|
707
|
-
perfTracker?.checkpoint(
|
|
972
|
+
perfTracker?.checkpoint("prep_end");
|
|
973
|
+
perfTracker?.checkpoint("heuristics_start");
|
|
708
974
|
const scanFn = createPresetScanner(preset);
|
|
709
|
-
const matchesH = await (typeof scanFn === "function"
|
|
710
|
-
|
|
711
|
-
|
|
975
|
+
const matchesH = await (typeof scanFn === "function"
|
|
976
|
+
? scanFn
|
|
977
|
+
: scanFn.scan)(input, ctx);
|
|
978
|
+
const allMatches = [...matchesH];
|
|
979
|
+
perfTracker?.checkpoint("heuristics_end");
|
|
712
980
|
// Advanced detection (enabled by default, can be overridden by config)
|
|
713
981
|
const advancedEnabled = opts.enableAdvancedDetection ?? opts.config?.advanced?.enablePolyglotDetection ?? true;
|
|
714
982
|
if (advancedEnabled) {
|
|
715
|
-
perfTracker?.checkpoint(
|
|
983
|
+
perfTracker?.checkpoint("advanced_start");
|
|
716
984
|
// Detect polyglot files
|
|
717
985
|
if (opts.config?.advanced?.enablePolyglotDetection !== false) {
|
|
718
986
|
const polyglotMatches = detectPolyglot(input);
|
|
@@ -727,37 +995,38 @@ async function scanBytes(input, opts = {}) {
|
|
|
727
995
|
if (opts.config?.advanced?.enableNestedArchiveAnalysis !== false) {
|
|
728
996
|
const nestingAnalysis = analyzeNestedArchives(input);
|
|
729
997
|
const maxDepth = opts.config?.advanced?.maxArchiveDepth ?? 5;
|
|
730
|
-
if (nestingAnalysis.hasExcessiveNesting ||
|
|
998
|
+
if (nestingAnalysis.hasExcessiveNesting || nestingAnalysis.depth > maxDepth) {
|
|
731
999
|
allMatches.push({
|
|
732
|
-
rule:
|
|
733
|
-
severity:
|
|
1000
|
+
rule: "excessive_archive_nesting",
|
|
1001
|
+
severity: "high",
|
|
734
1002
|
meta: {
|
|
735
|
-
description:
|
|
1003
|
+
description: "Excessive archive nesting detected",
|
|
736
1004
|
depth: nestingAnalysis.depth,
|
|
737
1005
|
maxAllowed: maxDepth,
|
|
738
1006
|
},
|
|
739
1007
|
});
|
|
740
1008
|
}
|
|
741
1009
|
}
|
|
742
|
-
perfTracker?.checkpoint(
|
|
1010
|
+
perfTracker?.checkpoint("advanced_end");
|
|
743
1011
|
}
|
|
744
1012
|
const matches = toYaraMatches(allMatches);
|
|
745
1013
|
const verdict = computeVerdict(matches);
|
|
746
1014
|
perfTracker ? perfTracker.getDuration() : Date.now();
|
|
747
1015
|
const durationMs = perfTracker ? perfTracker.getDuration() : 0;
|
|
748
1016
|
const report = {
|
|
749
|
-
ok: verdict ===
|
|
1017
|
+
ok: verdict === "clean",
|
|
750
1018
|
verdict,
|
|
751
1019
|
matches,
|
|
752
|
-
reasons: matches.map(m => m.rule),
|
|
1020
|
+
reasons: matches.map((m) => m.rule),
|
|
753
1021
|
file: { name: ctx.filename, mimeType: ctx.mimeType, size: ctx.size },
|
|
754
1022
|
durationMs,
|
|
755
|
-
engine:
|
|
1023
|
+
engine: "heuristics",
|
|
756
1024
|
truncated: false,
|
|
757
1025
|
timedOut: false,
|
|
758
1026
|
};
|
|
759
1027
|
// Add performance metrics if tracking enabled
|
|
760
|
-
if (perfTracker &&
|
|
1028
|
+
if (perfTracker &&
|
|
1029
|
+
(opts.enablePerformanceTracking || opts.config?.performance?.enablePerformanceTracking)) {
|
|
761
1030
|
report.performanceMetrics = perfTracker.getMetrics(input.byteLength);
|
|
762
1031
|
}
|
|
763
1032
|
// Cache result if enabled
|
|
@@ -771,10 +1040,7 @@ async function scanBytes(input, opts = {}) {
|
|
|
771
1040
|
}
|
|
772
1041
|
/** Scan di un file su disco (Node). Import dinamico per non vincolare il bundle browser. */
|
|
773
1042
|
async function scanFile(filePath, opts = {}) {
|
|
774
|
-
const [{ readFile, stat }, path] = await Promise.all([
|
|
775
|
-
import('fs/promises'),
|
|
776
|
-
import('path'),
|
|
777
|
-
]);
|
|
1043
|
+
const [{ readFile, stat }, path] = await Promise.all([import('fs/promises'), import('path')]);
|
|
778
1044
|
const [buf, st] = await Promise.all([readFile(filePath), stat(filePath)]);
|
|
779
1045
|
const ctx = {
|
|
780
1046
|
filename: path.basename(filePath),
|
|
@@ -798,21 +1064,6 @@ async function scanFiles(files, opts = {}) {
|
|
|
798
1064
|
return out;
|
|
799
1065
|
}
|
|
800
1066
|
|
|
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
1067
|
const SIG_CEN = 0x02014b50;
|
|
817
1068
|
const DEFAULTS = {
|
|
818
1069
|
maxEntries: 1000,
|
|
@@ -829,7 +1080,7 @@ function r32(buf, off) {
|
|
|
829
1080
|
}
|
|
830
1081
|
function isZipLike(buf) {
|
|
831
1082
|
// local file header at start is common
|
|
832
|
-
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
|
|
1083
|
+
return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
|
|
833
1084
|
}
|
|
834
1085
|
function lastIndexOfEOCD(buf, window) {
|
|
835
1086
|
const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
|
@@ -838,7 +1089,7 @@ function lastIndexOfEOCD(buf, window) {
|
|
|
838
1089
|
return idx >= start ? idx : -1;
|
|
839
1090
|
}
|
|
840
1091
|
function hasTraversal(name) {
|
|
841
|
-
return name.includes(
|
|
1092
|
+
return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
|
|
842
1093
|
}
|
|
843
1094
|
function createZipBombGuard(opts = {}) {
|
|
844
1095
|
const cfg = { ...DEFAULTS, ...opts };
|
|
@@ -852,7 +1103,7 @@ function createZipBombGuard(opts = {}) {
|
|
|
852
1103
|
const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
|
|
853
1104
|
if (eocdPos < 0 || eocdPos + 22 > buf.length) {
|
|
854
1105
|
// ZIP but no EOCD — malformed or polyglot → suspicious
|
|
855
|
-
matches.push({ rule:
|
|
1106
|
+
matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
|
|
856
1107
|
return matches;
|
|
857
1108
|
}
|
|
858
1109
|
const totalEntries = r16(buf, eocdPos + 10);
|
|
@@ -860,7 +1111,7 @@ function createZipBombGuard(opts = {}) {
|
|
|
860
1111
|
const cdOffset = r32(buf, eocdPos + 16);
|
|
861
1112
|
// Bounds check
|
|
862
1113
|
if (cdOffset + cdSize > buf.length) {
|
|
863
|
-
matches.push({ rule:
|
|
1114
|
+
matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
|
|
864
1115
|
return matches;
|
|
865
1116
|
}
|
|
866
1117
|
// Iterate central directory entries
|
|
@@ -881,287 +1132,68 @@ function createZipBombGuard(opts = {}) {
|
|
|
881
1132
|
const nameEnd = nameStart + fnLen;
|
|
882
1133
|
if (nameEnd > buf.length)
|
|
883
1134
|
break;
|
|
884
|
-
const name = buf.toString(
|
|
1135
|
+
const name = buf.toString("utf8", nameStart, nameEnd);
|
|
885
1136
|
sumComp += compSize;
|
|
886
1137
|
sumUnc += uncSize;
|
|
887
1138
|
seen++;
|
|
888
1139
|
if (name.length > cfg.maxEntryNameLength) {
|
|
889
|
-
matches.push({
|
|
1140
|
+
matches.push({
|
|
1141
|
+
rule: "zip_entry_name_too_long",
|
|
1142
|
+
severity: "medium",
|
|
1143
|
+
meta: { name, length: name.length },
|
|
1144
|
+
});
|
|
890
1145
|
}
|
|
891
1146
|
if (hasTraversal(name)) {
|
|
892
|
-
matches.push({ rule:
|
|
1147
|
+
matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
|
|
893
1148
|
}
|
|
894
1149
|
// move to next entry
|
|
895
1150
|
ptr = nameEnd + exLen + cmLen;
|
|
896
1151
|
}
|
|
897
1152
|
if (seen !== totalEntries) {
|
|
898
1153
|
// central dir truncated/odd, still report what we found
|
|
899
|
-
matches.push({
|
|
1154
|
+
matches.push({
|
|
1155
|
+
rule: "zip_cd_truncated",
|
|
1156
|
+
severity: "medium",
|
|
1157
|
+
meta: { seen, totalEntries },
|
|
1158
|
+
});
|
|
900
1159
|
}
|
|
901
1160
|
// Heuristics thresholds
|
|
902
1161
|
if (seen > cfg.maxEntries) {
|
|
903
|
-
matches.push({
|
|
1162
|
+
matches.push({
|
|
1163
|
+
rule: "zip_too_many_entries",
|
|
1164
|
+
severity: "medium",
|
|
1165
|
+
meta: { seen, limit: cfg.maxEntries },
|
|
1166
|
+
});
|
|
904
1167
|
}
|
|
905
1168
|
if (sumUnc > cfg.maxTotalUncompressedBytes) {
|
|
906
1169
|
matches.push({
|
|
907
|
-
rule:
|
|
908
|
-
severity:
|
|
909
|
-
meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
|
|
1170
|
+
rule: "zip_total_uncompressed_too_large",
|
|
1171
|
+
severity: "medium",
|
|
1172
|
+
meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
|
|
910
1173
|
});
|
|
911
1174
|
}
|
|
912
1175
|
if (sumComp === 0 && sumUnc > 0) {
|
|
913
|
-
matches.push({
|
|
1176
|
+
matches.push({
|
|
1177
|
+
rule: "zip_suspicious_ratio",
|
|
1178
|
+
severity: "medium",
|
|
1179
|
+
meta: { ratio: Infinity },
|
|
1180
|
+
});
|
|
914
1181
|
}
|
|
915
1182
|
else if (sumComp > 0) {
|
|
916
1183
|
const ratio = sumUnc / Math.max(1, sumComp);
|
|
917
1184
|
if (ratio >= cfg.maxCompressionRatio) {
|
|
918
|
-
matches.push({
|
|
1185
|
+
matches.push({
|
|
1186
|
+
rule: "zip_suspicious_ratio",
|
|
1187
|
+
severity: "medium",
|
|
1188
|
+
meta: { ratio, limit: cfg.maxCompressionRatio },
|
|
1189
|
+
});
|
|
919
1190
|
}
|
|
920
1191
|
}
|
|
921
1192
|
return matches;
|
|
922
|
-
}
|
|
1193
|
+
},
|
|
923
1194
|
};
|
|
924
1195
|
}
|
|
925
1196
|
|
|
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
1197
|
/**
|
|
1166
1198
|
* Export utilities for scan results
|
|
1167
1199
|
* @module utils/export
|
|
@@ -1177,19 +1209,15 @@ class ScanResultExporter {
|
|
|
1177
1209
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1178
1210
|
if (!options.includeDetails) {
|
|
1179
1211
|
// Simplified output
|
|
1180
|
-
const simplified = data.map(r => ({
|
|
1212
|
+
const simplified = data.map((r) => ({
|
|
1181
1213
|
verdict: r.verdict,
|
|
1182
1214
|
file: r.file?.name,
|
|
1183
1215
|
matches: r.matches.length,
|
|
1184
1216
|
durationMs: r.durationMs,
|
|
1185
1217
|
}));
|
|
1186
|
-
return options.prettyPrint
|
|
1187
|
-
? JSON.stringify(simplified, null, 2)
|
|
1188
|
-
: JSON.stringify(simplified);
|
|
1218
|
+
return options.prettyPrint ? JSON.stringify(simplified, null, 2) : JSON.stringify(simplified);
|
|
1189
1219
|
}
|
|
1190
|
-
return options.prettyPrint
|
|
1191
|
-
? JSON.stringify(data, null, 2)
|
|
1192
|
-
: JSON.stringify(data);
|
|
1220
|
+
return options.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
1193
1221
|
}
|
|
1194
1222
|
/**
|
|
1195
1223
|
* Export to CSV format
|
|
@@ -1197,68 +1225,68 @@ class ScanResultExporter {
|
|
|
1197
1225
|
toCSV(reports, options = {}) {
|
|
1198
1226
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1199
1227
|
const headers = [
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1228
|
+
"filename",
|
|
1229
|
+
"verdict",
|
|
1230
|
+
"matches_count",
|
|
1231
|
+
"file_size",
|
|
1232
|
+
"mime_type",
|
|
1233
|
+
"duration_ms",
|
|
1234
|
+
"engine",
|
|
1207
1235
|
];
|
|
1208
1236
|
if (options.includeDetails) {
|
|
1209
|
-
headers.push(
|
|
1237
|
+
headers.push("reasons", "match_rules");
|
|
1210
1238
|
}
|
|
1211
|
-
const rows = data.map(report => {
|
|
1239
|
+
const rows = data.map((report) => {
|
|
1212
1240
|
const row = [
|
|
1213
|
-
this.escapeCsv(report.file?.name ||
|
|
1241
|
+
this.escapeCsv(report.file?.name || "unknown"),
|
|
1214
1242
|
report.verdict,
|
|
1215
1243
|
report.matches.length.toString(),
|
|
1216
1244
|
(report.file?.size || 0).toString(),
|
|
1217
|
-
this.escapeCsv(report.file?.mimeType ||
|
|
1245
|
+
this.escapeCsv(report.file?.mimeType || "unknown"),
|
|
1218
1246
|
(report.durationMs || 0).toString(),
|
|
1219
|
-
report.engine ||
|
|
1247
|
+
report.engine || "unknown",
|
|
1220
1248
|
];
|
|
1221
1249
|
if (options.includeDetails) {
|
|
1222
|
-
row.push(this.escapeCsv((report.reasons || []).join(
|
|
1250
|
+
row.push(this.escapeCsv((report.reasons || []).join("; ")), this.escapeCsv(report.matches.map((m) => m.rule).join("; ")));
|
|
1223
1251
|
}
|
|
1224
|
-
return row.join(
|
|
1252
|
+
return row.join(",");
|
|
1225
1253
|
});
|
|
1226
|
-
return [headers.join(
|
|
1254
|
+
return [headers.join(","), ...rows].join("\n");
|
|
1227
1255
|
}
|
|
1228
1256
|
/**
|
|
1229
1257
|
* Export to Markdown format
|
|
1230
1258
|
*/
|
|
1231
1259
|
toMarkdown(reports, options = {}) {
|
|
1232
1260
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1233
|
-
let md =
|
|
1261
|
+
let md = "# Scan Results\n\n";
|
|
1234
1262
|
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 +=
|
|
1263
|
+
const clean = data.filter((r) => r.verdict === "clean").length;
|
|
1264
|
+
const suspicious = data.filter((r) => r.verdict === "suspicious").length;
|
|
1265
|
+
const malicious = data.filter((r) => r.verdict === "malicious").length;
|
|
1266
|
+
md += "## Summary\n\n";
|
|
1239
1267
|
md += `- ✅ Clean: ${clean}\n`;
|
|
1240
1268
|
md += `- ⚠️ Suspicious: ${suspicious}\n`;
|
|
1241
1269
|
md += `- ❌ Malicious: ${malicious}\n\n`;
|
|
1242
|
-
md +=
|
|
1270
|
+
md += "## Detailed Results\n\n";
|
|
1243
1271
|
for (const report of data) {
|
|
1244
|
-
const icon = report.verdict ===
|
|
1245
|
-
md += `### ${icon} ${report.file?.name ||
|
|
1272
|
+
const icon = report.verdict === "clean" ? "✅" : report.verdict === "suspicious" ? "⚠️" : "❌";
|
|
1273
|
+
md += `### ${icon} ${report.file?.name || "Unknown"}\n\n`;
|
|
1246
1274
|
md += `- **Verdict:** ${report.verdict}\n`;
|
|
1247
1275
|
md += `- **Size:** ${this.formatBytes(report.file?.size || 0)}\n`;
|
|
1248
|
-
md += `- **MIME Type:** ${report.file?.mimeType ||
|
|
1276
|
+
md += `- **MIME Type:** ${report.file?.mimeType || "unknown"}\n`;
|
|
1249
1277
|
md += `- **Duration:** ${report.durationMs || 0}ms\n`;
|
|
1250
1278
|
md += `- **Matches:** ${report.matches.length}\n`;
|
|
1251
1279
|
if (options.includeDetails && report.matches.length > 0) {
|
|
1252
|
-
md +=
|
|
1280
|
+
md += "\n**Match Details:**\n";
|
|
1253
1281
|
for (const match of report.matches) {
|
|
1254
1282
|
md += `- ${match.rule}`;
|
|
1255
1283
|
if (match.tags && match.tags.length > 0) {
|
|
1256
|
-
md += ` (${match.tags.join(
|
|
1284
|
+
md += ` (${match.tags.join(", ")})`;
|
|
1257
1285
|
}
|
|
1258
|
-
md +=
|
|
1286
|
+
md += "\n";
|
|
1259
1287
|
}
|
|
1260
1288
|
}
|
|
1261
|
-
md +=
|
|
1289
|
+
md += "\n";
|
|
1262
1290
|
}
|
|
1263
1291
|
return md;
|
|
1264
1292
|
}
|
|
@@ -1268,20 +1296,20 @@ class ScanResultExporter {
|
|
|
1268
1296
|
*/
|
|
1269
1297
|
toSARIF(reports, options = {}) {
|
|
1270
1298
|
const data = Array.isArray(reports) ? reports : [reports];
|
|
1271
|
-
const results = data.flatMap(report => {
|
|
1272
|
-
if (report.verdict ===
|
|
1299
|
+
const results = data.flatMap((report) => {
|
|
1300
|
+
if (report.verdict === "clean")
|
|
1273
1301
|
return [];
|
|
1274
|
-
return report.matches.map(match => ({
|
|
1302
|
+
return report.matches.map((match) => ({
|
|
1275
1303
|
ruleId: match.rule,
|
|
1276
|
-
level: report.verdict ===
|
|
1304
|
+
level: report.verdict === "malicious" ? "error" : "warning",
|
|
1277
1305
|
message: {
|
|
1278
|
-
text: `${match.rule} detected in ${report.file?.name ||
|
|
1306
|
+
text: `${match.rule} detected in ${report.file?.name || "unknown file"}`,
|
|
1279
1307
|
},
|
|
1280
1308
|
locations: [
|
|
1281
1309
|
{
|
|
1282
1310
|
physicalLocation: {
|
|
1283
1311
|
artifactLocation: {
|
|
1284
|
-
uri: report.file?.name ||
|
|
1312
|
+
uri: report.file?.name || "unknown",
|
|
1285
1313
|
},
|
|
1286
1314
|
},
|
|
1287
1315
|
},
|
|
@@ -1293,33 +1321,31 @@ class ScanResultExporter {
|
|
|
1293
1321
|
}));
|
|
1294
1322
|
});
|
|
1295
1323
|
const sarif = {
|
|
1296
|
-
version:
|
|
1297
|
-
$schema:
|
|
1324
|
+
version: "2.1.0",
|
|
1325
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
1298
1326
|
runs: [
|
|
1299
1327
|
{
|
|
1300
1328
|
tool: {
|
|
1301
1329
|
driver: {
|
|
1302
|
-
name:
|
|
1303
|
-
version:
|
|
1304
|
-
informationUri:
|
|
1330
|
+
name: "Pompelmi",
|
|
1331
|
+
version: "0.29.0",
|
|
1332
|
+
informationUri: "https://pompelmi.github.io/pompelmi/",
|
|
1305
1333
|
},
|
|
1306
1334
|
},
|
|
1307
1335
|
results,
|
|
1308
1336
|
},
|
|
1309
1337
|
],
|
|
1310
1338
|
};
|
|
1311
|
-
return options.prettyPrint
|
|
1312
|
-
? JSON.stringify(sarif, null, 2)
|
|
1313
|
-
: JSON.stringify(sarif);
|
|
1339
|
+
return options.prettyPrint ? JSON.stringify(sarif, null, 2) : JSON.stringify(sarif);
|
|
1314
1340
|
}
|
|
1315
1341
|
/**
|
|
1316
1342
|
* Export to HTML format
|
|
1317
1343
|
*/
|
|
1318
1344
|
toHTML(reports, options = {}) {
|
|
1319
1345
|
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 ===
|
|
1346
|
+
const clean = data.filter((r) => r.verdict === "clean").length;
|
|
1347
|
+
const suspicious = data.filter((r) => r.verdict === "suspicious").length;
|
|
1348
|
+
const malicious = data.filter((r) => r.verdict === "malicious").length;
|
|
1323
1349
|
let html = `<!DOCTYPE html>
|
|
1324
1350
|
<html lang="en">
|
|
1325
1351
|
<head>
|
|
@@ -1351,11 +1377,11 @@ class ScanResultExporter {
|
|
|
1351
1377
|
for (const report of data) {
|
|
1352
1378
|
const statusClass = report.verdict;
|
|
1353
1379
|
html += `<div class="result ${statusClass}">`;
|
|
1354
|
-
html += `<h3>${this.escapeHtml(report.file?.name ||
|
|
1380
|
+
html += `<h3>${this.escapeHtml(report.file?.name || "Unknown")}</h3>`;
|
|
1355
1381
|
html += `<table>`;
|
|
1356
1382
|
html += `<tr><th>Verdict</th><td>${report.verdict.toUpperCase()}</td></tr>`;
|
|
1357
1383
|
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 ||
|
|
1384
|
+
html += `<tr><th>MIME Type</th><td>${this.escapeHtml(report.file?.mimeType || "unknown")}</td></tr>`;
|
|
1359
1385
|
html += `<tr><th>Duration</th><td>${report.durationMs || 0}ms</td></tr>`;
|
|
1360
1386
|
html += `<tr><th>Matches</th><td>${report.matches.length}</td></tr>`;
|
|
1361
1387
|
html += `</table>`;
|
|
@@ -1364,7 +1390,7 @@ class ScanResultExporter {
|
|
|
1364
1390
|
for (const match of report.matches) {
|
|
1365
1391
|
html += `<li><strong>${this.escapeHtml(match.rule)}</strong>`;
|
|
1366
1392
|
if (match.tags && match.tags.length > 0) {
|
|
1367
|
-
html += ` ${match.tags.map(tag => `<span class="badge">${this.escapeHtml(tag)}</span>`).join(
|
|
1393
|
+
html += ` ${match.tags.map((tag) => `<span class="badge">${this.escapeHtml(tag)}</span>`).join("")}`;
|
|
1368
1394
|
}
|
|
1369
1395
|
html += `</li>`;
|
|
1370
1396
|
}
|
|
@@ -1380,41 +1406,41 @@ class ScanResultExporter {
|
|
|
1380
1406
|
*/
|
|
1381
1407
|
export(reports, format, options = {}) {
|
|
1382
1408
|
switch (format) {
|
|
1383
|
-
case
|
|
1409
|
+
case "json":
|
|
1384
1410
|
return this.toJSON(reports, options);
|
|
1385
|
-
case
|
|
1411
|
+
case "csv":
|
|
1386
1412
|
return this.toCSV(reports, options);
|
|
1387
|
-
case
|
|
1413
|
+
case "markdown":
|
|
1388
1414
|
return this.toMarkdown(reports, options);
|
|
1389
|
-
case
|
|
1415
|
+
case "html":
|
|
1390
1416
|
return this.toHTML(reports, options);
|
|
1391
|
-
case
|
|
1417
|
+
case "sarif":
|
|
1392
1418
|
return this.toSARIF(reports, options);
|
|
1393
1419
|
default:
|
|
1394
1420
|
throw new Error(`Unsupported export format: ${format}`);
|
|
1395
1421
|
}
|
|
1396
1422
|
}
|
|
1397
1423
|
escapeCsv(value) {
|
|
1398
|
-
if (value.includes(
|
|
1424
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
1399
1425
|
return `"${value.replace(/"/g, '""')}"`;
|
|
1400
1426
|
}
|
|
1401
1427
|
return value;
|
|
1402
1428
|
}
|
|
1403
1429
|
escapeHtml(value) {
|
|
1404
1430
|
return value
|
|
1405
|
-
.replace(/&/g,
|
|
1406
|
-
.replace(/</g,
|
|
1407
|
-
.replace(/>/g,
|
|
1408
|
-
.replace(/"/g,
|
|
1409
|
-
.replace(/'/g,
|
|
1431
|
+
.replace(/&/g, "&")
|
|
1432
|
+
.replace(/</g, "<")
|
|
1433
|
+
.replace(/>/g, ">")
|
|
1434
|
+
.replace(/"/g, """)
|
|
1435
|
+
.replace(/'/g, "'");
|
|
1410
1436
|
}
|
|
1411
1437
|
formatBytes(bytes) {
|
|
1412
1438
|
if (bytes === 0)
|
|
1413
|
-
return
|
|
1439
|
+
return "0 Bytes";
|
|
1414
1440
|
const k = 1024;
|
|
1415
|
-
const sizes = [
|
|
1441
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
1416
1442
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1417
|
-
return Math.round(bytes /
|
|
1443
|
+
return Math.round((bytes / k ** i) * 100) / 100 + " " + sizes[i];
|
|
1418
1444
|
}
|
|
1419
1445
|
}
|
|
1420
1446
|
/**
|
|
@@ -1425,5 +1451,30 @@ function exportScanResults(reports, format, options) {
|
|
|
1425
1451
|
return exporter.export(reports, format, options);
|
|
1426
1452
|
}
|
|
1427
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* Validates a File by MIME type and size (max 5 MB).
|
|
1456
|
+
*/
|
|
1457
|
+
function validateFile(file) {
|
|
1458
|
+
const maxSize = 5 * 1024 * 1024;
|
|
1459
|
+
const allowedTypes = ["text/plain", "application/json", "text/csv"];
|
|
1460
|
+
if (!allowedTypes.includes(file.type)) {
|
|
1461
|
+
return { valid: false, error: "Unsupported file type" };
|
|
1462
|
+
}
|
|
1463
|
+
if (file.size > maxSize) {
|
|
1464
|
+
return { valid: false, error: "File too large (max 5 MB)" };
|
|
1465
|
+
}
|
|
1466
|
+
return { valid: true };
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function mapMatchesToVerdict(matches = []) {
|
|
1470
|
+
if (!matches.length)
|
|
1471
|
+
return "clean";
|
|
1472
|
+
const malHints = ["trojan", "ransom", "worm", "spy", "rootkit", "keylog", "botnet"];
|
|
1473
|
+
const tagSet = new Set(matches.flatMap((m) => (m.tags ?? []).map((t) => t.toLowerCase())));
|
|
1474
|
+
const nameHit = (r) => malHints.some((h) => r.toLowerCase().includes(h));
|
|
1475
|
+
const isMal = matches.some((m) => nameHit(m.rule)) || tagSet.has("malware") || tagSet.has("critical");
|
|
1476
|
+
return isMal ? "malicious" : "suspicious";
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1428
1479
|
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
1480
|
//# sourceMappingURL=pompelmi.browser.esm.js.map
|