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