Assessor_2.0/tests/server/security-template-parser.te...

113 lines
4.4 KiB
TypeScript

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);
});
});