import { createHash } from 'node:crypto'; import { describe, expect, it } from 'vitest'; import { parseSecurityTemplateInf, reduceSecurityTemplate } from '../../server/gpo/security-template-parser'; import type { VerifiedZipFile } from '../../server/gpo/zip-inventory'; import { ImportError } from '../../src/shared/errors'; function verifiedFile(content: string | Uint8Array): VerifiedZipFile { const bytes = typeof content === 'string' ? new TextEncoder().encode(content) : content; const sha256 = createHash('sha256').update(bytes).digest('hex'); return { entry: { index: 1, relativePath: 'synthetic/DomainSysvol/GPO/Machine/Microsoft/Windows NT/SecEdit/GptTmpl.inf', kind: 'file', compressionMethod: 8, compressedBytes: bytes.byteLength, uncompressedBytes: bytes.byteLength, crc32: '00000000', sha256 }, bytes }; } function context() { return { backupInstanceId: 'backup-synthetic', sourceArtifactId: 'source-synthetic', artifactId: 'entry-synthetic' }; } describe('Security Template parser and reducer', () => { it('keeps section semantics separate and emits typed observations with line provenance', () => { const document = parseSecurityTemplateInf(verifiedFile([ '[Unicode]', 'Unicode=yes', '[System Access]', 'MinimumPasswordLength = 14', '[Privilege Rights]', 'SeDenyNetworkLogonRight = *S-1-5-32-546,*S-1-5-32-544', '[Registry Values]', 'MACHINE\\Software\\Synthetic\\Setting=4,1', '[Version]', 'signature="$CHICAGO$"' ].join('\r\n')), 'computer'); expect(document).toMatchObject({ status: 'complete', scope: 'computer' }); expect(reduceSecurityTemplate(document, context())).toEqual(expect.arrayContaining([ expect.objectContaining({ identity: expect.objectContaining({ section: 'system access', key: 'MinimumPasswordLength' }), values: [{ kind: 'scalar', raw: '14' }], sourceLines: [4], effectiveState: 'unknown' }), expect.objectContaining({ identity: expect.objectContaining({ section: 'privilege rights', key: 'SeDenyNetworkLogonRight' }), values: [{ kind: 'principal-set', principals: ['*S-1-5-32-546', '*S-1-5-32-544'] }] }), expect.objectContaining({ identity: expect.objectContaining({ section: 'registry values' }), values: [{ kind: 'registry', typeCode: 4, rawData: '1' }] }) ])); }); it('marks differing duplicate instructions as conflicted instead of choosing a winner', () => { const document = parseSecurityTemplateInf(verifiedFile([ '[System Access]', 'PasswordComplexity=1', 'passwordcomplexity=0' ].join('\n')), 'computer'); expect(reduceSecurityTemplate(document, context())).toEqual([ expect.objectContaining({ state: 'conflicted', values: [{ kind: 'scalar', raw: '1' }, { kind: 'scalar', raw: '0' }], sourceLines: [2, 3], diagnosticCodes: ['gpo-security-template-value-conflict'] }) ]); }); it('retains supported observations but marks documents with unknown sections partial', () => { const document = parseSecurityTemplateInf(verifiedFile([ '[System Access]', 'MinimumPasswordLength=14', '[File Security]', 'synthetic-path=synthetic-acl' ].join('\n')), 'computer'); expect(document).toMatchObject({ status: 'partial', diagnosticCodes: ['gpo-security-template-section-unsupported'] }); expect(reduceSecurityTemplate(document, context())).toHaveLength(1); }); it('fails closed for malformed supported entries and never emits partial instructions', () => { const document = parseSecurityTemplateInf(verifiedFile('[Registry Values]\nMACHINE\\Synthetic=not-a-type'), 'computer'); expect(document).toMatchObject({ status: 'invalid', instructions: [], diagnosticCodes: ['gpo-security-template-registry-value-invalid'] }); expect(reduceSecurityTemplate(document, context())).toEqual([]); }); it('accepts BOM-marked UTF-16LE and rejects undecodable bytes', () => { const encoded = Buffer.from('\ufeff[System Access]\r\nMinimumPasswordLength=14', 'utf16le'); expect(parseSecurityTemplateInf(verifiedFile(encoded), 'computer').status).toBe('complete'); expect(() => parseSecurityTemplateInf(verifiedFile(new Uint8Array([0xff, 0xfe, 0x00])), 'computer')) .toThrowError(ImportError); }); });