import { deflateRawSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { inventoryGpoContainer } from '../../server/gpo/gpo-container-inventory'; import { inventoryZipArchive } from '../../server/gpo/zip-inventory'; import { ImportError } from '../../src/shared/errors'; interface SyntheticZipEntry { path: string; content: string | Uint8Array; method?: 0 | 8; declaredUncompressedBytes?: number; crcOverride?: number; madeBySystem?: number; externalAttributes?: number; } function crc32(bytes: Uint8Array): number { let value = 0xffffffff; for (const byte of bytes) { value ^= byte; for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (0xedb88320 & -(value & 1)); } return (value ^ 0xffffffff) >>> 0; } function syntheticZip(entries: SyntheticZipEntry[]): Uint8Array { const localParts: Buffer[] = []; const centralParts: Buffer[] = []; let localOffset = 0; for (const entry of entries) { const name = Buffer.from(entry.path, 'utf8'); const content = typeof entry.content === 'string' ? Buffer.from(entry.content, 'utf8') : Buffer.from(entry.content); const method = entry.method ?? 8; const compressed = method === 8 ? deflateRawSync(content) : content; const checksum = entry.crcOverride ?? crc32(content); const declaredSize = entry.declaredUncompressedBytes ?? content.length; const local = Buffer.alloc(30 + name.length); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0x0800, 6); local.writeUInt16LE(method, 8); local.writeUInt32LE(checksum, 14); local.writeUInt32LE(compressed.length, 18); local.writeUInt32LE(declaredSize, 22); local.writeUInt16LE(name.length, 26); name.copy(local, 30); localParts.push(local, compressed); const central = Buffer.alloc(46 + name.length); central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(((entry.madeBySystem ?? 0) << 8) | 20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0x0800, 8); central.writeUInt16LE(method, 10); central.writeUInt32LE(checksum, 16); central.writeUInt32LE(compressed.length, 20); central.writeUInt32LE(declaredSize, 24); central.writeUInt16LE(name.length, 28); central.writeUInt32LE(entry.externalAttributes ?? 0, 38); central.writeUInt32LE(localOffset, 42); name.copy(central, 46); centralParts.push(central); localOffset += local.length + compressed.length; } const centralDirectory = Buffer.concat(centralParts); const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10); end.writeUInt32LE(centralDirectory.length, 12); end.writeUInt32LE(localOffset, 16); return Buffer.concat([...localParts, centralDirectory, end]); } function diagnosticCode(action: () => unknown): string | undefined { try { action(); } catch (error) { return error instanceof ImportError ? error.diagnostics[0]?.code : undefined; } return undefined; } describe('server-side GPO ZIP inventory', () => { it('inventories a synthetic GPO backup candidate without parsing or executing entries', () => { const root = '11111111-1111-4111-8111-111111111111'; const bytes = syntheticZip([ { path: `${root}/Backup.xml`, content: '' }, { path: `${root}/DomainSysvol/GPO/Machine/Registry.pol`, content: new Uint8Array([80, 82, 101, 103]) }, { path: `${root}/DomainSysvol/GPO/Machine/Scripts/startup.ps1`, content: 'Write-Output synthetic' }, { path: `${root}/DomainSysvol/GPO/gpt.ini`, content: '[General]' } ]); const result = inventoryGpoContainer(bytes); expect(result.recognition).toMatchObject({ status: 'gpo-backup-candidate', evidenceCodes: ['gpo-backup-metadata-path-observed', 'gpo-domain-sysvol-structure-observed'] }); expect(result.archive.entries.every(entry => entry.kind !== 'file' || /^[a-f0-9]{64}$/.test(entry.sha256 ?? ''))).toBe(true); expect(result.inventory.status).toBe('complete'); expect(result.inventory.entries).toEqual(expect.arrayContaining([ expect.objectContaining({ role: 'backup-metadata', scope: 'shared', disposition: 'inventory-only', secretIndicatorStatus: 'not-inspected' }), expect.objectContaining({ role: 'registry-policy', scope: 'computer', disposition: 'inventory-only' }), expect.objectContaining({ role: 'script', scope: 'computer', disposition: 'inventory-only' }), expect.objectContaining({ role: 'unknown', disposition: 'unsupported' }) ])); expect(result.inventory.diagnosticCodes).toEqual(expect.arrayContaining([ 'gpo-artifact-parser-pending', 'gpo-artifact-unsupported', 'gpo-script-inventory-only' ])); }); it('does not recognize filename-only ZIP content as a GPO backup', () => { const result = inventoryGpoContainer(syntheticZip([{ path: 'Backup.xml', content: '' }])); expect(result.recognition.status).toBe('unrecognized-zip'); expect(result.recognition.evidenceCodes).toContain('gpo-container-structure-unconfirmed'); }); it('rejects traversal, case collisions and file-as-directory aliases before inflation', () => { expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([{ path: '../escape.xml', content: 'x' }])))) .toBe('zip-entry-path-rejected'); expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([ { path: 'safe/Backup.xml', content: 'a' }, { path: 'SAFE/backup.XML', content: 'b' } ])))).toBe('zip-entry-path-collision'); expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([ { path: 'safe', content: 'file' }, { path: 'safe/child.xml', content: 'child' } ])))).toBe('zip-entry-path-collision'); }); it('rejects symbolic links and nested ZIP content', () => { const symlinkMode = ((0xa000 | 0o777) << 16) >>> 0; expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([{ path: 'link', content: 'target', madeBySystem: 3, externalAttributes: symlinkMode }])))).toBe('zip-link-rejected'); const nested = syntheticZip([{ path: 'inner.txt', content: 'synthetic' }]); expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([{ path: 'nested.bin', content: nested }])))) .toBe('zip-nested-archive-rejected'); }); it('verifies actual size, CRC and actual compression ratio', () => { expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([{ path: 'size.txt', content: 'actual', declaredUncompressedBytes: 5 }])))).toBe('zip-entry-size-mismatch'); expect(diagnosticCode(() => inventoryZipArchive(syntheticZip([{ path: 'crc.txt', content: 'actual', crcOverride: 0 }])))).toBe('zip-entry-crc-mismatch'); expect(diagnosticCode(() => inventoryZipArchive( syntheticZip([{ path: 'ratio.txt', content: 'A'.repeat(10_000) }]), { maximumZipCompressionRatio: 2 } ))).toBe('zip-compression-ratio-limit'); }); });