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