pompelmi 0.6.0 → 0.7.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 +2 -0
- package/dist/pompelmi.cjs.js +110 -0
- package/dist/pompelmi.cjs.js.map +1 -1
- package/dist/pompelmi.esm.js +108 -1
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/scan.d.ts +26 -0
- package/package.json +5 -1
package/dist/pompelmi.esm.js
CHANGED
|
@@ -74,6 +74,72 @@ async function createYaraEngine() {
|
|
|
74
74
|
: mod.createBrowserEngine();
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function sniffMagicBytes(bytes) {
|
|
78
|
+
const s = (sig) => {
|
|
79
|
+
const arr = typeof sig === 'string' ? new TextEncoder().encode(sig) : new Uint8Array(sig);
|
|
80
|
+
if (bytes.length < arr.length)
|
|
81
|
+
return false;
|
|
82
|
+
for (let i = 0; i < arr.length; i++)
|
|
83
|
+
if (bytes[i] !== arr[i])
|
|
84
|
+
return false;
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
if (s([0x50, 0x4B, 0x03, 0x04]) || s([0x50, 0x4B, 0x05, 0x06]) || s([0x50, 0x4B, 0x07, 0x08]))
|
|
88
|
+
return { mime: 'application/zip', extHint: 'zip' };
|
|
89
|
+
if (s([0x25, 0x50, 0x44, 0x46, 0x2D]))
|
|
90
|
+
return { mime: 'application/pdf', extHint: 'pdf' };
|
|
91
|
+
if (s([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))
|
|
92
|
+
return { mime: 'image/png', extHint: 'png' };
|
|
93
|
+
if (s([0xFF, 0xD8, 0xFF]))
|
|
94
|
+
return { mime: 'image/jpeg', extHint: 'jpg' };
|
|
95
|
+
if (s('GIF87a') || s('GIF89a'))
|
|
96
|
+
return { mime: 'image/gif', extHint: 'gif' };
|
|
97
|
+
if (s('<?xml') || s('<svg'))
|
|
98
|
+
return { mime: 'image/svg+xml', extHint: 'svg' };
|
|
99
|
+
if (s([0x4D, 0x5A]))
|
|
100
|
+
return { mime: 'application/vnd.microsoft.portable-executable', extHint: 'exe' };
|
|
101
|
+
if (s([0x7F, 0x45, 0x4C, 0x46]))
|
|
102
|
+
return { mime: 'application/x-elf', extHint: 'elf' };
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function hasSuspiciousJpegTrailer(bytes, maxTrailer = 1000000) {
|
|
106
|
+
for (let i = bytes.length - 2; i >= 2; i--) {
|
|
107
|
+
if (bytes[i] === 0xFF && bytes[i + 1] === 0xD9) {
|
|
108
|
+
const trailer = bytes.length - (i + 2);
|
|
109
|
+
return trailer > maxTrailer;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function prefilterBrowser(bytes, filename, policy) {
|
|
115
|
+
const reasons = [];
|
|
116
|
+
const ext = (filename.split('.').pop() || '').toLowerCase();
|
|
117
|
+
if (policy.maxFileSizeBytes && bytes.byteLength > policy.maxFileSizeBytes) {
|
|
118
|
+
reasons.push(`size_exceeded:${bytes.byteLength}`);
|
|
119
|
+
}
|
|
120
|
+
if (policy.includeExtensions && policy.includeExtensions.length && !policy.includeExtensions.includes(ext)) {
|
|
121
|
+
reasons.push(`ext_denied:${ext}`);
|
|
122
|
+
}
|
|
123
|
+
const s = sniffMagicBytes(bytes);
|
|
124
|
+
if (!s)
|
|
125
|
+
reasons.push('mime_unknown');
|
|
126
|
+
if (s?.mime && policy.allowedMimeTypes && policy.allowedMimeTypes.length && !policy.allowedMimeTypes.includes(s.mime)) {
|
|
127
|
+
reasons.push(`mime_denied:${s.mime}`);
|
|
128
|
+
}
|
|
129
|
+
if (s?.extHint && ext && s.extHint !== ext) {
|
|
130
|
+
reasons.push(`ext_mismatch:${ext}->${s.extHint}`);
|
|
131
|
+
}
|
|
132
|
+
if (s?.mime === 'image/jpeg' && hasSuspiciousJpegTrailer(bytes))
|
|
133
|
+
reasons.push('jpeg_trailer_payload');
|
|
134
|
+
if (s?.mime === 'image/svg+xml' && policy.denyScriptableSvg !== false) {
|
|
135
|
+
const text = new TextDecoder().decode(bytes).toLowerCase();
|
|
136
|
+
if (text.includes('<script') || text.includes('onload=') || text.includes('href="javascript:')) {
|
|
137
|
+
reasons.push('svg_script');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const severity = reasons.length ? 'suspicious' : 'clean';
|
|
141
|
+
return { severity, reasons, mime: s?.mime };
|
|
142
|
+
}
|
|
77
143
|
/**
|
|
78
144
|
* Reads an array of File objects via FileReader and returns their text.
|
|
79
145
|
*/
|
|
@@ -120,6 +186,37 @@ async function scanFilesWithYara(files, rulesSource) {
|
|
|
120
186
|
}
|
|
121
187
|
return results;
|
|
122
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Scan files with fast browser heuristics + optional YARA.
|
|
191
|
+
* Returns content, prefilter verdict, and YARA matches.
|
|
192
|
+
*/
|
|
193
|
+
async function scanFilesWithHeuristicsAndYara(files, rulesSource, policy) {
|
|
194
|
+
let compiled;
|
|
195
|
+
try {
|
|
196
|
+
const engine = await createYaraEngine();
|
|
197
|
+
compiled = await engine.compile(rulesSource);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
console.warn('[yara] non disponibile o regole non compilate:', e);
|
|
201
|
+
}
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const file of files) {
|
|
204
|
+
const [content, bytes] = await Promise.all([file.text(), file.arrayBuffer().then(b => new Uint8Array(b))]);
|
|
205
|
+
const prefilter = prefilterBrowser(bytes, file.name, policy);
|
|
206
|
+
let matches = [];
|
|
207
|
+
if (compiled) {
|
|
208
|
+
try {
|
|
209
|
+
// Optional short-circuit: only run YARA if needed. For now, we always run it if available.
|
|
210
|
+
matches = await compiled.scan(bytes);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
console.warn(`[yara] errore scansione ${file.name}:`, e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
out.push({ file, content, prefilter, yara: { matches } });
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
123
220
|
|
|
124
221
|
/**
|
|
125
222
|
* Validates a File by MIME type and size (max 5 MB).
|
|
@@ -2993,5 +3090,15 @@ async function scanFilesWithRemoteYara(files, rulesSource, remote) {
|
|
|
2993
3090
|
return results;
|
|
2994
3091
|
}
|
|
2995
3092
|
|
|
2996
|
-
|
|
3093
|
+
function mapMatchesToVerdict(matches = []) {
|
|
3094
|
+
if (!matches.length)
|
|
3095
|
+
return 'clean';
|
|
3096
|
+
const malHints = ['trojan', 'ransom', 'worm', 'spy', 'rootkit', 'keylog', 'botnet'];
|
|
3097
|
+
const tagSet = new Set(matches.flatMap(m => (m.tags ?? []).map(t => t.toLowerCase())));
|
|
3098
|
+
const nameHit = (r) => malHints.some(h => r.toLowerCase().includes(h));
|
|
3099
|
+
const isMal = matches.some(m => nameHit(m.rule)) || tagSet.has('malware') || tagSet.has('critical');
|
|
3100
|
+
return isMal ? 'malicious' : 'suspicious';
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
export { mapMatchesToVerdict, prefilterBrowser, scanFiles, scanFilesWithHeuristicsAndYara, scanFilesWithRemoteYara, scanFilesWithYara, useFileScanner, validateFile };
|
|
2997
3104
|
//# sourceMappingURL=pompelmi.esm.js.map
|