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('skips unsupported section syntax before validating supported key/value entries', () => { const document = parseSecurityTemplateInf(verifiedFile([ '[System Access]', 'MinimumPasswordLength=14', '[Service General Setting]', '"SyntheticService",2,""', '"SyntheticDisabledService",4,""', '[Privilege Rights]', 'SeNetworkLogonRight=*S-1-5-32-544', 'SeEnableDelegationPrivilege=', '[Registry Values]', 'MACHINE\\Software\\Synthetic\\Mode=4,1', '[Kerberos Policy]', 'MaxTicketAge=10' ].join('\r\n')), 'computer'); expect(document.status).toBe('partial'); expect(document.diagnosticCodes).toEqual(['gpo-security-template-section-unsupported']); const observations = reduceSecurityTemplate(document, context()); expect(observations).toHaveLength(5); expect(observations.map(item => item.sourceLines)).toEqual([[2], [7], [8], [10], [12]]); expect(observations[2].values).toEqual([{ kind: 'principal-set', principals: [] }]); expect(observations.every(item => item.sourceSha256 === document.sourceSha256)).toBe(true); }); it.each([ ['missing separator', '[System Access]\nmalformed entry', 'gpo-security-template-entry-invalid'], ['empty key', '[System Access]\n =1', 'gpo-security-template-entry-invalid'], ['oversized key', `[System Access]\n${'x'.repeat(1025)}=1`, 'gpo-security-template-entry-invalid'], ['invalid registry type', '[Registry Values]\nMACHINE\\Synthetic=not-a-type', 'gpo-security-template-registry-value-invalid'] ])('retains valid instructions around a defective entry: %s', (_name, malformed, diagnostic) => { const document = parseSecurityTemplateInf(verifiedFile([ '[System Access]', 'MinimumPasswordLength=14', malformed, '[Privilege Rights]', 'SeNetworkLogonRight=*S-1-5-32-544' ].join('\n')), 'computer'); expect(document.status).toBe('partial'); expect(document.diagnosticCodes).toEqual([diagnostic]); expect(document.instructions).toHaveLength(2); expect(reduceSecurityTemplate(document, context()).map(item => item.sourceLines)).toEqual([[2], [6]]); }); it('reports partial with no observations when the only supported entry is malformed', () => { const document = parseSecurityTemplateInf(verifiedFile('[Registry Values]\nMACHINE\\Synthetic=not-a-type'), 'computer'); expect(document).toMatchObject({ status: 'partial', instructions: [], diagnosticCodes: ['gpo-security-template-registry-value-invalid'] }); expect(reduceSecurityTemplate(document, context())).toEqual([]); }); it.each([ ['unclosed section', '[System Access'], ['empty section', '[]'], ['oversized section', `[${'x'.repeat(257)}]`], ['field limit', `Mode=${'x'.repeat(4096)}`] ])('keeps structural ambiguity and resource violations invalid: %s', (_name, malformed) => { const document = parseSecurityTemplateInf(verifiedFile([ '[System Access]', 'MinimumPasswordLength=14', malformed, 'OtherSetting=1' ].join('\n')), 'computer'); expect(document.status).toBe('invalid'); expect(document.instructions).toEqual([]); 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); }); });