ktg-plugin-marketplace/plugins/llm-security/tests/lib/build-zip.mjs
Kjell Tore Guttormsen fe0193956d feat(llm-security): /security ide-scan <url> — Marketplace/OpenVSX/direct VSIX (v6.4.0)
Pre-installation verification of VS Code extensions via URL — fetch a remote
VSIX, extract it in a hardened sandbox, and run the existing IDE scanner
pipeline against it. No npm dependencies.

Sources:
- VS Code Marketplace (publisher.gallery.vsassets.io direct download)
- OpenVSX (open-vsx.org official API)
- Direct .vsix HTTPS URLs

Defenses:
- HTTPS-only, TLS verified, manual redirect with per-source host whitelist
- 30s total timeout via AbortController
- 50MB compressed cap, 500MB uncompressed, 100x expansion ratio
- Zero-dep ZIP extractor: zip-slip, absolute paths, drive letters, NUL bytes,
  symlinks (Unix mode 0xA000), depth limits, ZIP64 rejected, encrypted rejected
- SHA-256 streamed during fetch, surfaced in meta.source
- Temp dir cleanup in all paths (try/finally)

Files:
- scanners/lib/vsix-fetch.mjs (HTTPS fetcher, host whitelist, streaming SHA-256)
- scanners/lib/zip-extract.mjs (zero-dep parser with hardening caps)
- knowledge/marketplace-api-notes.md (endpoint reference)
- 3 test files (48 tests added: vsix-fetch, zip-extract, ide-extension-url)

Tests: 1296 → 1344 (all green).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 17:16:26 +02:00

97 lines
3.3 KiB
JavaScript

// build-zip.mjs — Minimal synthetic ZIP builder for tests.
// Supports STORE method only. Lets tests construct adversarial archives that
// real zip tools refuse to emit (zip-slip names, symlink mode bits, oversized
// uncompressed sizes for bomb tests).
import { crc32 } from 'node:zlib';
const SIG_LFH = 0x04034b50;
const SIG_CD = 0x02014b50;
const SIG_EOCD = 0x06054b50;
function crc(buf) {
return crc32(buf) >>> 0;
}
/**
* Build a ZIP buffer from a list of entries.
* @param {Array<{ name: string, data: Buffer|string, externalAttr?: number, versionMadeBy?: number, declaredUncompSize?: number, declaredCompSize?: number }>} entries
* @returns {Buffer}
*/
export function buildZip(entries) {
const lfhParts = [];
const cdParts = [];
let offset = 0;
for (const entry of entries) {
const nameBuf = Buffer.from(entry.name, 'utf8');
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '', 'utf8');
const compSize = entry.declaredCompSize ?? data.length;
const uncompSize = entry.declaredUncompSize ?? data.length;
const c = crc(data);
// Local file header (30 bytes)
const lfh = Buffer.alloc(30);
lfh.writeUInt32LE(SIG_LFH, 0);
lfh.writeUInt16LE(20, 4); // version needed
lfh.writeUInt16LE(0, 6); // flags
lfh.writeUInt16LE(0, 8); // method = STORE
lfh.writeUInt16LE(0, 10); // time
lfh.writeUInt16LE(0, 12); // date
lfh.writeUInt32LE(c, 14); // crc32
lfh.writeUInt32LE(compSize, 18); // compressed size
lfh.writeUInt32LE(uncompSize, 22); // uncompressed size
lfh.writeUInt16LE(nameBuf.length, 26);
lfh.writeUInt16LE(0, 28); // extra len
lfhParts.push(lfh, nameBuf, data);
const thisLfhOffset = offset;
offset += lfh.length + nameBuf.length + data.length;
// Central directory header (46 bytes)
const cd = Buffer.alloc(46);
cd.writeUInt32LE(SIG_CD, 0);
cd.writeUInt16LE(entry.versionMadeBy ?? (3 << 8) | 20, 4); // OS=Unix(3), version=20
cd.writeUInt16LE(20, 6);
cd.writeUInt16LE(0, 8);
cd.writeUInt16LE(0, 10);
cd.writeUInt16LE(0, 12);
cd.writeUInt16LE(0, 14);
cd.writeUInt32LE(c, 16);
cd.writeUInt32LE(compSize, 20);
cd.writeUInt32LE(uncompSize, 24);
cd.writeUInt16LE(nameBuf.length, 28);
cd.writeUInt16LE(0, 30);
cd.writeUInt16LE(0, 32); // comment len
cd.writeUInt16LE(0, 34); // disk start
cd.writeUInt16LE(0, 36); // internal attrs
cd.writeUInt32LE((entry.externalAttr ?? 0) >>> 0, 38); // external attrs (unsigned)
cd.writeUInt32LE(thisLfhOffset, 42);
cdParts.push(cd, nameBuf);
}
const lfhSection = Buffer.concat(lfhParts);
const cdSection = Buffer.concat(cdParts);
const cdOffset = lfhSection.length;
const cdSize = cdSection.length;
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(SIG_EOCD, 0);
eocd.writeUInt16LE(0, 4);
eocd.writeUInt16LE(0, 6);
eocd.writeUInt16LE(entries.length, 8);
eocd.writeUInt16LE(entries.length, 10);
eocd.writeUInt32LE(cdSize, 12);
eocd.writeUInt32LE(cdOffset, 16);
eocd.writeUInt16LE(0, 20);
return Buffer.concat([lfhSection, cdSection, eocd]);
}
/** Convenience: produce a unix mode in the upper 16 bits of externalAttr. */
export function unixModeAttr(mode) {
return (mode & 0xFFFF) << 16;
}
export const MODE_SYMLINK = 0xA1FF; // S_IFLNK | rwxrwxrwx