Assessor_2.0/tests/server/admx-policy-catalog.test.ts

91 lines
5.2 KiB
TypeScript

import { createHash } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import { buildAdmxPolicyCatalog, resolveRegistryPolicyIdentity } from '../../server/gpo/admx-policy-catalog';
import { parseRegistryPolicyFile, reduceRegistryPolicy } from '../../server/gpo/registry-policy-parser';
import type { VerifiedZipFile } from '../../server/gpo/zip-inventory';
function verified(path: string, content: Uint8Array | string): VerifiedZipFile {
const bytes = typeof content === 'string' ? Buffer.from(content) : content;
return {
entry: {
index: 0,
relativePath: path,
kind: 'file',
compressionMethod: 0,
compressedBytes: bytes.byteLength,
uncompressedBytes: bytes.byteLength,
crc32: '00000000',
sha256: createHash('sha256').update(bytes).digest('hex')
},
bytes
};
}
function registryPolicy(key: string, name: string, value: number): Uint8Array {
const unicode = (text: string) => Buffer.from(text, 'utf16le');
const header = Buffer.alloc(8); header.write('PReg'); header.writeUInt32LE(1, 4);
const type = Buffer.alloc(4); type.writeUInt32LE(4);
const size = Buffer.alloc(4); size.writeUInt32LE(4);
const data = Buffer.alloc(4); data.writeUInt32LE(value);
return Buffer.concat([
header, unicode('['), unicode(key), Buffer.alloc(2), unicode(';'), unicode(name), Buffer.alloc(2), unicode(';'),
type, unicode(';'), size, unicode(';'), data, unicode(']')
]);
}
const namespace = 'http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions';
describe('ADMX technical policy identity catalog', () => {
it('keeps technical identity stable while attaching locale-independent ADMX metadata', () => {
const admx = verified('Templates/Synthetic.admx', `<?xml version="1.0" encoding="utf-8"?>
<policyDefinitions xmlns="${namespace}" revision="1.0" schemaVersion="1.0">
<policyNamespaces><target prefix="synthetic" namespace="Contoso.Policies.Synthetic" /></policyNamespaces>
<policies><policy name="HardenMode" class="Machine" displayName="$(string.HardenMode)"
explainText="$(string.HardenMode_Help)" key="Software\\Policies\\Synthetic" valueName="Mode" /></policies>
</policyDefinitions>`);
const adml = verified('Templates/en-US/Synthetic.adml', `<?xml version="1.0" encoding="utf-8"?>
<policyDefinitionResources xmlns="${namespace}" revision="1.0" schemaVersion="1.0">
<resources><stringTable><string id="HardenMode">Harden mode</string><string id="HardenMode_Help">Synthetic help</string></stringTable></resources>
</policyDefinitionResources>`);
const catalog = buildAdmxPolicyCatalog([adml, admx], {
kind: 'microsoft-security-baseline', baselineId: 'baseline-1', packageSha256: 'a'.repeat(64)
});
expect(catalog).toMatchObject({
status: 'complete',
policies: [{ technicalId: 'Contoso.Policies.Synthetic#HardenMode', displayNameResourceId: 'HardenMode' }],
languageResources: [{ locale: 'en-US', strings: { HardenMode: 'Harden mode' } }]
});
const source = verified('GPOs/one/DomainSysvol/GPO/Machine/Registry.pol', registryPolicy('Software\\Policies\\Synthetic', 'Mode', 1));
const document = parseRegistryPolicyFile(source, 'computer');
const observation = reduceRegistryPolicy(document, { backupInstanceId: 'backup-1', sourceArtifactId: 'artifact-1', artifactId: 'entry-1' })[0];
const withoutCatalog = resolveRegistryPolicyIdentity(observation);
const withCatalog = resolveRegistryPolicyIdentity(observation, catalog);
expect(withoutCatalog).toMatchObject({ status: 'resolved', identity: { admx: { status: 'not-found' } } });
expect(withCatalog).toMatchObject({
status: 'resolved',
identity: {
id: withoutCatalog.status === 'resolved' ? withoutCatalog.identity.id : undefined,
target: { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic', valueName: 'Mode' },
admx: { status: 'matched', reference: { technicalId: 'Contoso.Policies.Synthetic#HardenMode', settingId: '$policy' } }
}
});
});
it('fails closed on duplicate namespaces and external XML constructs', () => {
const source = (path: string) => verified(path, `<policyDefinitions xmlns="${namespace}"><policyNamespaces><target prefix="x" namespace="Duplicate.Namespace" /></policyNamespaces><policies><policy name="One" class="User" key="Software\\X" valueName="Y" /></policies></policyDefinitions>`);
expect(() => buildAdmxPolicyCatalog([source('one.admx'), source('two.admx')], {
kind: 'microsoft-security-baseline', baselineId: 'baseline-1', packageSha256: 'b'.repeat(64)
})).toThrow(/Zielnamespace/);
expect(() => buildAdmxPolicyCatalog([verified('bad.admx', `<!DOCTYPE x [<!ENTITY y SYSTEM "file:///x">]><policyDefinitions xmlns="${namespace}"/>`)], {
kind: 'microsoft-security-baseline', baselineId: 'baseline-1', packageSha256: 'c'.repeat(64)
})).toThrow(/Externe/);
});
it('does not pretend that multi-target delete instructions have one identity', () => {
const source = verified('GPOs/one/DomainSysvol/GPO/User/Registry.pol', registryPolicy('Software\\Policies\\Synthetic', '**DeleteValues', 1));
const document = parseRegistryPolicyFile(source, 'user');
expect(document.status).toBe('invalid');
});
});