pompelmi 0.11.0-dev.10 → 0.11.0-dev.12

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.
@@ -3100,5 +3100,184 @@ function mapMatchesToVerdict(matches = []) {
3100
3100
  return isMal ? 'malicious' : 'suspicious';
3101
3101
  }
3102
3102
 
3103
- export { mapMatchesToVerdict, prefilterBrowser, scanFiles, scanFilesWithHeuristicsAndYara, scanFilesWithRemoteYara, scanFilesWithYara, useFileScanner, validateFile };
3103
+ function hasAsciiToken(buf, token) {
3104
+ // Use latin1 so we can safely search binary
3105
+ return buf.indexOf(token, 0, 'latin1') !== -1;
3106
+ }
3107
+ function startsWith(buf, bytes) {
3108
+ if (buf.length < bytes.length)
3109
+ return false;
3110
+ for (let i = 0; i < bytes.length; i++)
3111
+ if (buf[i] !== bytes[i])
3112
+ return false;
3113
+ return true;
3114
+ }
3115
+ function isPDF(buf) {
3116
+ // %PDF-
3117
+ return startsWith(buf, [0x25, 0x50, 0x44, 0x46, 0x2d]);
3118
+ }
3119
+ function isOleCfb(buf) {
3120
+ // D0 CF 11 E0 A1 B1 1A E1
3121
+ const sig = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
3122
+ return startsWith(buf, sig);
3123
+ }
3124
+ function isZipLike$1(buf) {
3125
+ // PK\x03\x04
3126
+ return startsWith(buf, [0x50, 0x4b, 0x03, 0x04]);
3127
+ }
3128
+ function isPeExecutable(buf) {
3129
+ // "MZ"
3130
+ return startsWith(buf, [0x4d, 0x5a]);
3131
+ }
3132
+ /** OOXML macro hint via filename token in ZIP container */
3133
+ function hasOoxmlMacros(buf) {
3134
+ if (!isZipLike$1(buf))
3135
+ return false;
3136
+ return hasAsciiToken(buf, 'vbaProject.bin');
3137
+ }
3138
+ /** PDF risky features (/JavaScript, /OpenAction, /AA, /Launch) */
3139
+ function pdfRiskTokens(buf) {
3140
+ const tokens = ['/JavaScript', '/OpenAction', '/AA', '/Launch'];
3141
+ return tokens.filter(t => hasAsciiToken(buf, t));
3142
+ }
3143
+ const CommonHeuristicsScanner = {
3144
+ async scan(input) {
3145
+ const buf = Buffer.from(input);
3146
+ const matches = [];
3147
+ // Office macros (OLE / OOXML)
3148
+ if (isOleCfb(buf)) {
3149
+ matches.push({ rule: 'office_ole_container', severity: 'suspicious' });
3150
+ }
3151
+ if (hasOoxmlMacros(buf)) {
3152
+ matches.push({ rule: 'office_ooxml_macros', severity: 'suspicious' });
3153
+ }
3154
+ // PDF risky tokens
3155
+ if (isPDF(buf)) {
3156
+ const toks = pdfRiskTokens(buf);
3157
+ if (toks.length) {
3158
+ matches.push({
3159
+ rule: 'pdf_risky_actions',
3160
+ severity: 'suspicious',
3161
+ meta: { tokens: toks }
3162
+ });
3163
+ }
3164
+ }
3165
+ // Executable header
3166
+ if (isPeExecutable(buf)) {
3167
+ matches.push({ rule: 'pe_executable_signature', severity: 'suspicious' });
3168
+ }
3169
+ return matches;
3170
+ }
3171
+ };
3172
+
3173
+ const SIG_CEN = 0x02014b50;
3174
+ const DEFAULTS = {
3175
+ maxEntries: 1000,
3176
+ maxTotalUncompressedBytes: 500 * 1024 * 1024,
3177
+ maxEntryNameLength: 255,
3178
+ maxCompressionRatio: 1000,
3179
+ eocdSearchWindow: 70000,
3180
+ };
3181
+ function r16(buf, off) {
3182
+ return buf.readUInt16LE(off);
3183
+ }
3184
+ function r32(buf, off) {
3185
+ return buf.readUInt32LE(off);
3186
+ }
3187
+ function isZipLike(buf) {
3188
+ // local file header at start is common
3189
+ return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
3190
+ }
3191
+ function lastIndexOfEOCD(buf, window) {
3192
+ const sig = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
3193
+ const start = Math.max(0, buf.length - window);
3194
+ return buf.lastIndexOf(sig, buf.length - 1, start);
3195
+ }
3196
+ function hasTraversal(name) {
3197
+ return name.includes('../') || name.includes('..\\') || name.startsWith('/') || /^[A-Za-z]:/.test(name);
3198
+ }
3199
+ function createZipBombGuard(opts = {}) {
3200
+ const cfg = { ...DEFAULTS, ...opts };
3201
+ return {
3202
+ async scan(input) {
3203
+ const buf = Buffer.from(input);
3204
+ const matches = [];
3205
+ if (!isZipLike(buf))
3206
+ return matches;
3207
+ // Find EOCD near the end
3208
+ const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
3209
+ if (eocdPos < 0 || eocdPos + 22 > buf.length) {
3210
+ // ZIP but no EOCD — malformed or polyglot → suspicious
3211
+ matches.push({ rule: 'zip_eocd_not_found', severity: 'suspicious' });
3212
+ return matches;
3213
+ }
3214
+ const totalEntries = r16(buf, eocdPos + 10);
3215
+ const cdSize = r32(buf, eocdPos + 12);
3216
+ const cdOffset = r32(buf, eocdPos + 16);
3217
+ // Bounds check
3218
+ if (cdOffset + cdSize > buf.length) {
3219
+ matches.push({ rule: 'zip_cd_out_of_bounds', severity: 'suspicious' });
3220
+ return matches;
3221
+ }
3222
+ // Iterate central directory entries
3223
+ let ptr = cdOffset;
3224
+ let seen = 0;
3225
+ let sumComp = 0;
3226
+ let sumUnc = 0;
3227
+ while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
3228
+ const sig = r32(buf, ptr);
3229
+ if (sig !== SIG_CEN)
3230
+ break; // stop if structure breaks
3231
+ const compSize = r32(buf, ptr + 20);
3232
+ const uncSize = r32(buf, ptr + 24);
3233
+ const fnLen = r16(buf, ptr + 28);
3234
+ const exLen = r16(buf, ptr + 30);
3235
+ const cmLen = r16(buf, ptr + 32);
3236
+ const nameStart = ptr + 46;
3237
+ const nameEnd = nameStart + fnLen;
3238
+ if (nameEnd > buf.length)
3239
+ break;
3240
+ const name = buf.toString('utf8', nameStart, nameEnd);
3241
+ sumComp += compSize;
3242
+ sumUnc += uncSize;
3243
+ seen++;
3244
+ if (name.length > cfg.maxEntryNameLength) {
3245
+ matches.push({ rule: 'zip_entry_name_too_long', severity: 'suspicious', meta: { name, length: name.length } });
3246
+ }
3247
+ if (hasTraversal(name)) {
3248
+ matches.push({ rule: 'zip_path_traversal_entry', severity: 'suspicious', meta: { name } });
3249
+ }
3250
+ // move to next entry
3251
+ ptr = nameEnd + exLen + cmLen;
3252
+ }
3253
+ if (seen !== totalEntries) {
3254
+ // central dir truncated/odd, still report what we found
3255
+ matches.push({ rule: 'zip_cd_truncated', severity: 'suspicious', meta: { seen, totalEntries } });
3256
+ }
3257
+ // Heuristics thresholds
3258
+ if (seen > cfg.maxEntries) {
3259
+ matches.push({ rule: 'zip_too_many_entries', severity: 'suspicious', meta: { seen, limit: cfg.maxEntries } });
3260
+ }
3261
+ if (sumUnc > cfg.maxTotalUncompressedBytes) {
3262
+ matches.push({
3263
+ rule: 'zip_total_uncompressed_too_large',
3264
+ severity: 'suspicious',
3265
+ meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes }
3266
+ });
3267
+ }
3268
+ if (sumComp === 0 && sumUnc > 0) {
3269
+ matches.push({ rule: 'zip_suspicious_ratio', severity: 'suspicious', meta: { ratio: Infinity } });
3270
+ }
3271
+ else if (sumComp > 0) {
3272
+ const ratio = sumUnc / Math.max(1, sumComp);
3273
+ if (ratio >= cfg.maxCompressionRatio) {
3274
+ matches.push({ rule: 'zip_suspicious_ratio', severity: 'suspicious', meta: { ratio, limit: cfg.maxCompressionRatio } });
3275
+ }
3276
+ }
3277
+ return matches;
3278
+ }
3279
+ };
3280
+ }
3281
+
3282
+ export { CommonHeuristicsScanner, createZipBombGuard, mapMatchesToVerdict, prefilterBrowser, scanFiles, scanFilesWithHeuristicsAndYara, scanFilesWithRemoteYara, scanFilesWithYara, useFileScanner, validateFile };
3104
3283
  //# sourceMappingURL=pompelmi.esm.js.map