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

167 lines
7.1 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('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);
});
});