91 lines
5.2 KiB
TypeScript
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');
|
|
});
|
|
});
|