Assessor_2.0/tests/features/policy-guidance.test.tsx

304 lines
21 KiB
TypeScript

import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import type { FindingRecord } from '../../src/domain/finding';
import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
import type { StoredMicrosoftSecurityBaselineAnalysis } from '../../src/domain/microsoft-security-baseline-analysis';
import { baselineRemediation, buildPolicyGuidance, formatBaselineVariants } from '../../src/profiles/group-policy/guidance';
import { policyWebSearch } from '../../src/profiles/group-policy/web-search';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
import { loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace';
function result(): PolicyEvaluationResult {
return { id: 'result', baselineId: 'baseline', baselineAnalysisId: 'analysis', baselineLabel: 'Synthetic security baseline',
baselineState: 'explicit-recommendation', customerState: 'missing', classification: 'confirmed_configured_deviation',
comparator: { comparator: { kind: 'equality' }, relation: 'different', baseline: { kind: 'integer', value: '1' } },
identity: { id: 'identity', scope: 'computer', target: { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic', valueName: 'Mode' },
admx: { status: 'matched', catalogId: 'catalog', reference: { technicalId: 'namespace#Mode' } } },
observationIds: [], backupInstanceIds: ['backup'], diagnosticCodes: [] } as unknown as PolicyEvaluationResult;
}
function evaluation(item = result()): StoredPolicyEvaluation {
return { results: [item], run: { id: 'run', completedAt: '2026-09-06T00:00:00Z', evaluator: POLICY_EVALUATION_ENGINE, baselineAnalysisIds: ['analysis'] } } as StoredPolicyEvaluation;
}
function analysis(): StoredMicrosoftSecurityBaselineAnalysis {
return { analysis: { id: 'analysis', baselineId: 'baseline', admxCatalog: { id: 'catalog',
policies: [{ technicalId: 'namespace#Mode', sourcePath: 'Templates/Synthetic.admx', explainTextResourceId: 'Explain' }],
languageResources: [
{ admxFileName: 'Other.admx', locale: 'en-US', strings: { Explain: 'Wrong policy text' } },
{ admxFileName: 'Synthetic.admx', locale: 'de-DE', strings: { Explain: 'Synthetischer Text' } },
{ admxFileName: 'Synthetic.admx', locale: 'en-US', strings: { Explain: 'Original synthetic policy explanation.' } }
] } } } as unknown as StoredMicrosoftSecurityBaselineAnalysis;
}
function record(values: Record<string, string>, title = 'Synthetic setting'): FindingRecord {
return { id: 'synthetic', index: 0, title, filled: [], meta: [], fields: Object.entries(values).map(([label, value], index) => ({ index, label, value })) };
}
function render(item: FindingRecord) {
return renderToStaticMarkup(createElement(FindingReader, { record: item, title: item.title, linkedSheets: [], resultReferences: [], search: '', position: 0, recordCount: 1, toolId: 'group-policy', onPrevious() {}, onNext() {}, onReview() {} }));
}
describe('GPO baseline guidance', () => {
it('distinguishes equal values from different templates and shows which template was selected', () => {
const item = result();
item.identity.admx = { status: 'not-found' };
item.baselineSettingIds = ['dc'];
item.baselineTemplateSelection = { status: 'selected', candidateSettingIds: ['dc', 'member'], contextIds: ['explicit'] };
const catalog = analysis();
catalog.analysis.settings = ['dc', 'member'].map(id => ({
id, baselineId: 'baseline', backupId: id,
template: { names: [{ value: `Synthetic ${id}`, sourcePath: `Synthetic/${id}/Backup.xml`, sourceSha256: 'a'.repeat(64) }] },
identity: { status: 'resolved', identity: item.identity },
observation: { values: [{ kind: 'principal-set', principals: [] }] }
})) as unknown as StoredMicrosoftSecurityBaselineAnalysis['analysis']['settings'];
const text = buildPolicyGuidance([evaluation(item)], [catalog]).get(item.id)!.description;
expect(text).toContain('Synthetic security baseline — Synthetic dc: Empty account list (selected)');
expect(text).toContain('Synthetic security baseline — Synthetic member: Empty account list');
expect(text.match(/Empty account list/g)).toHaveLength(2);
expect(text).not.toContain('Source:');
expect(text).not.toContain('Backup:');
expect(text).not.toContain('could not be loaded');
});
it('shows distinct baseline values without source clutter or changing the outcome', () => {
const item = result();
item.identity.admx = { status: 'not-found' };
item.diagnosticCodes = ['policy-evaluation-baseline-consensus-conflict'];
item.baselineSettingIds = ['one', 'two', 'three'];
const catalog = analysis();
catalog.analysis.settings = ['one', 'two', 'three', 'unrelated'].map((id, index) => ({
id, baselineId: 'baseline', backupId: `backup-${id}`,
identity: { status: 'resolved', identity: item.identity },
observation: { sourcePath: `Synthetic/${id}/GptTmpl.inf`,
values: [{ kind: 'principal-set', principals: index === 0 ? ['*S-1-5-32-544'] : [] }] }
})) as StoredMicrosoftSecurityBaselineAnalysis['analysis']['settings'];
const before = JSON.stringify([item, catalog]);
const guidance = buildPolicyGuidance([evaluation(item)], [catalog]);
const description = guidance.get(item.id)!.description;
expect(description).toContain('Synthetic security baseline: Values: *S-1-5-32-544');
expect(description).toContain('Synthetic security baseline: Empty account list');
expect(description).not.toContain('Source:');
expect(description).not.toContain('Backup:');
expect(description.match(/Empty account list/g)).toHaveLength(1);
expect(description).not.toContain('could not be loaded');
expect(description).not.toContain('unrelated');
const dataset = appendPolicyObservationsToDataset(null, 'Synthetic', [], [evaluation(item)], guidance)!;
const [headers, row] = dataset.sheets[0].rows;
expect(row[headers.indexOf('Description')]).toBe(description);
expect(row[headers.indexOf('Status')]).toBe('Conflicts');
expect(render(record({ Description: description }))).toContain('Empty account list');
expect(JSON.stringify([item, catalog])).toBe(before);
item.baselineSettingIds.push('missing');
expect(buildPolicyGuidance([evaluation(item)], [catalog]).get(item.id)!.description)
.toContain('Some referenced baseline recommendations could not be loaded.');
catalog.analysis.id = 'different-revision';
const unavailable = buildPolicyGuidance([evaluation(item)], [catalog]).get(item.id)!.description;
expect(unavailable).toContain('could not be loaded');
expect(unavailable).not.toContain('S-1-5-32-544');
});
it('highlights remediation without coloring description or sources', () => {
const html = render(record({ Description: 'Synthetic explanation', Remediation: 'Expected: 1', Sources: 'Synthetic source' }));
expect(html).toContain('<div class="reader-remediation"><h4>Remediation</h4>');
expect(html.match(/class="reader-remediation"/g)).toHaveLength(1);
expect(html).toContain('Expected: 1');
});
it('resolves English original text from the exact catalog and matching ADMX file', () => {
const guidance = buildPolicyGuidance([evaluation()], [analysis()]).get('result')!;
expect(guidance.description).toBe('Original synthetic policy explanation.');
expect(guidance.source).toBe('Imported baseline ADML · en-US · Synthetic.admx');
expect(guidance.remediation).toContain('HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic\\Mode');
expect(guidance.remediation).toContain('Expected: 1');
});
it('never borrows descriptions from another version, baseline or ambiguous catalog', () => {
for (const patch of [{ id: 'newer' }, { baselineId: 'different' }]) {
const candidate = analysis();
Object.assign(candidate.analysis, patch);
expect(buildPolicyGuidance([evaluation()], [candidate]).get('result')?.description).toBe('');
}
const item = result();
item.identity.admx = { status: 'not-found' };
expect(buildPolicyGuidance([evaluation(item)], [analysis()]).get('result')?.description).toBe('');
const duplicate = analysis();
duplicate.analysis.admxCatalog.languageResources.push({ ...duplicate.analysis.admxCatalog.languageResources[2], strings: { Explain: 'Conflicting synthetic text' } });
expect(buildPolicyGuidance([evaluation()], [duplicate]).get('result')?.description).toBe('');
});
it('preserves zero and explicit audit flags, and does not invent absent recommendations', () => {
const item = result();
item.comparator.baseline = { kind: 'integer', value: '0' };
expect(baselineRemediation(item)).toContain('Expected: 0');
item.comparator.baseline = { kind: 'audit-flags', success: true, failure: false };
expect(baselineRemediation(item)).toContain('Success: true');
expect(baselineRemediation(item)).toContain('Failure: false');
item.comparator.baseline = undefined;
expect(baselineRemediation(item)).toBe('');
item.comparator.baseline = { kind: 'none' };
expect(baselineRemediation(item)).toBe('');
item.comparator.baseline = { kind: 'integer', value: '1' };
item.diagnosticCodes = ['policy-evaluation-baseline-consensus-conflict'];
expect(baselineRemediation(item)).toBe('');
});
it('shows a missing customer state as Not configured and registry states in plain language', () => {
const item = result();
item.identity.admx = { status: 'not-found' };
item.identity.target = { kind: 'registry-key', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic\\Blocklist' } as never;
item.classification = 'configured_match';
const keyEmpty = { kind: 'registry-state', state: 'key-empty' } as const;
const synthesizedAbsent = { kind: 'registry-state', state: 'absent' } as const;
item.comparator = { version: 1, comparator: { kind: 'equality' }, relation: 'match', diagnosticCodes: [],
actual: synthesizedAbsent, baseline: keyEmpty } as PolicyEvaluationResult['comparator'];
item.baselineVariantAssessment = { version: 1, scope: 'backup-configuration', decision: 'all-pass', diagnosticCodes: [], actualValues: [],
variants: [{ settingId: 'setting', comparator: item.comparator,
baselineValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic\\Blocklist', operation: 'delete-all-values' }] }]
} as unknown as PolicyEvaluationResult['baselineVariantAssessment'];
const before = JSON.stringify(item);
const guidance = buildPolicyGuidance([evaluation(item)], []);
const [headers, row] = appendPolicyObservationsToDataset(null, 'Synthetic', [], [evaluation(item)], guidance)!.sheets[0].rows;
const fields = Object.fromEntries(headers.map((header, index) => [String(header), String(row[index])]));
expect(fields.Status).toBe('Pass');
expect(fields['Configured Value']).toBe('Not configured');
expect(fields.Description).toContain('Matches every assessed baseline variant');
expect(fields.Description).not.toContain('Registry type');
expect(fields.Description).not.toContain('State:');
const html = render(record(fields));
expect(html).toMatch(/Configured customer value<\/h4><p[^>]*>Not configured</);
expect(html).toMatch(/Expected baseline value<\/h4><p[^>]*>No values under this key</);
expect(html).not.toContain('State:');
expect(JSON.stringify(item)).toBe(before);
const required = result();
required.identity.target = { kind: 'registry-key', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic\\Dll' } as never;
required.comparator.baseline = { kind: 'registry-state', state: 'key-present' };
expect(baselineRemediation(required)).toBe('Computer policy: HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic\\Dll\nExpected: Registry key exists');
required.comparator.baseline = { kind: 'registry-state', state: 'absent' };
expect(baselineRemediation(required)).toContain('Expected: Value not set');
required.baselineVariantAssessment = { version: 1, scope: 'backup-configuration', decision: 'all-fail', diagnosticCodes: [], actualValues: [],
variants: [{ settingId: 'dll', comparator: { ...required.comparator, relation: 'different', baseline: { kind: 'registry-state', state: 'key-present' } },
baselineValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic\\Dll', operation: 'create-key', typeCode: 0, data: { kind: 'none' } }] }]
} as unknown as PolicyEvaluationResult['baselineVariantAssessment'];
expect(formatBaselineVariants(required)).toBe('Variant 1: Registry key exists · Missing');
expect(buildPolicyGuidance([evaluation(required)], []).get(required.id)!.description).not.toMatch(/Registry type|Setting reference/);
});
it('retains minimum/maximum comparison semantics in remediation', () => {
const item = result();
item.comparator.comparator = { kind: 'minimum' };
expect(baselineRemediation(item)).toContain('Minimum: 1');
item.comparator.comparator = { kind: 'maximum' };
expect(baselineRemediation(item)).toContain('Maximum: 1');
});
it('projects guidance without modifying source records or outcomes', () => {
const data = evaluation();
const before = JSON.stringify(data);
const guidance = buildPolicyGuidance([data], [analysis()]);
const dataset = appendPolicyObservationsToDataset(null, 'Synthetic', [], [data], guidance)!;
const [headers, row] = dataset.sheets[0].rows;
expect(row[headers.indexOf('Description')]).toBe('Original synthetic policy explanation.');
expect(row[headers.indexOf('Remediation')]).toContain('Expected: 1');
expect(row[headers.indexOf('Status')]).toBe('Not configured');
expect(JSON.stringify(data)).toBe(before);
const missing = appendPolicyObservationsToDataset(null, 'Synthetic', [], [data])!;
expect(missing.sheets[0].rows[1][headers.indexOf('Description')]).toBe('Description unavailable in the referenced baseline metadata.');
});
it('separates historical outcomes, current readiness and absent descriptions without modifying evidence', () => {
const data = evaluation();
data.run.evaluator = { id: POLICY_EVALUATION_ENGINE.id, version: '1.3.3' };
const before = JSON.stringify(data);
const [headers, row] = appendPolicyObservationsToDataset(null, 'Synthetic', [], [data])!.sheets[0].rows;
const fields = Object.fromEntries(headers.map((header, index) => [String(header), String(row[index])]));
expect(fields.Status).toBe('Review required');
expect(fields.Comparison).toBe('Historical comparison — re-evaluation required');
expect(fields['Historical result']).toBe('Not configured · Engine 1.3.3');
expect(fields['Evaluation readiness']).toContain(POLICY_EVALUATION_ENGINE.version);
expect(fields.Description).not.toContain('Re-evaluation');
const html = render(record(fields));
expect(html).toContain('data-outcome="historical"');
expect(html).toContain('Saved baseline value');
expect(html).toContain('role="status"');
expect(JSON.stringify(data)).toBe(before);
const failedGuidance = buildPolicyGuidance([data]);
failedGuidance.get('result')!.descriptionLoadFailed = true;
const failed = appendPolicyObservationsToDataset(null, 'Synthetic', [], [data], failedGuidance)!.sheets[0].rows;
expect(failed[1][headers.indexOf('Description')]).toContain('could not be loaded');
});
it.each([false, true])('loads documentation once per referenced baseline; graceful failure=%s', async fails => {
const stamp = '2026-09-06T00:00:00Z';
const model = { schemaVersion: 1, project: { id: 'project', name: 'Synthetic' }, assessment: { id: 'slot', projectId: 'project', name: 'Synthetic' },
scanRuns: [], importFiles: [], sourceTables: [], findingDefinitions: [], findingOccurrences: [], evidence: [], reviews: [], diagnostics: [] } as unknown as CanonicalAssessmentModel;
const item = result();
item.observationIds = ['observation'];
item.customerState = 'configured';
item.comparator.actual = { kind: 'integer', value: '0' };
const list = vi.fn();
if (fails) list.mockRejectedValue(new Error('synthetic unavailable'));
else list.mockResolvedValue({ records: [analysis()] });
const reader = { listProjectAssessments: vi.fn().mockResolvedValue([model.assessment]), loadAssessmentModel: vi.fn().mockResolvedValue(model),
listPolicyEvaluations: vi.fn().mockResolvedValue({ records: [evaluation(item)] }), listMicrosoftSecurityBaselineAnalyses: list,
queryUnifiedResults: vi.fn().mockResolvedValue({ total: 1, limit: 500, offset: 0, items: [{ sourceKind: 'policy-observation', sourceResultId: 'observation',
projectId: 'project', assessmentId: 'slot', title: 'Mode', importedAt: stamp, policyScope: 'computer', policyDeliveryMechanism: 'registry-policy',
policyValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic', valueName: 'Mode', operation: 'set-value', typeCode: 4, data: { kind: 'uint32', value: 0 } }] }] }) };
const workspace = await loadCanonicalProjectWorkspace(reader, 'project');
expect(list).toHaveBeenCalledExactlyOnceWith('baseline');
const [headers, row] = workspace.dataset!.sheets[0].rows;
expect(row[headers.indexOf('Description')]).toBe(fails ? 'Baseline description could not be loaded. The saved comparison is retained.' : 'Original synthetic policy explanation.');
expect(row[headers.indexOf('Remediation')]).toContain('Expected: 1');
});
});
describe('privacy-safe GPO web search', () => {
it.each(['Domain', 'Private', 'Public'])('asks for the specific firewall setting and %s profile', profile => {
const item = record({ 'Registry path': `HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\WindowsFirewall\\${profile}Profile`, Baseline: 'Windows Server 2022 Security Baseline - Synthetic Secret', 'Configured Value': 'private-value' }, 'AllowLocalPolicyMerge');
const search = policyWebSearch(item, 'group-policy')!;
expect(search.query).toContain(`Windows Firewall AllowLocalPolicyMerge (${profile} profile)`);
expect(search.query).toContain('Benchmark products: Windows Server 2022.');
expect(search.query).not.toContain('Synthetic Secret');
expect(search.query).not.toContain('HKEY_LOCAL_MACHINE');
expect(search.query).not.toContain('private-value');
expect(new URL(search.url).searchParams.get('q')).toBe(search.query);
expect(new URL(search.url).searchParams.get('udm')).toBe('50');
});
it('does not guess a firewall setting from a partial match or unrelated registry path', () => {
const item = record({ 'Registry path': 'SOFTWARE\\Policies\\Microsoft\\WindowsFirewall\\DomainProfile' }, 'AllowLocalPolicyMerge private-name');
expect(policyWebSearch(item, 'group-policy')?.query).not.toContain('AllowLocalPolicyMerge');
expect(policyWebSearch(item, 'group-policy')?.query).not.toContain('Windows Windows');
expect(policyWebSearch(record({ 'Registry path': 'SOFTWARE\\Synthetic' }, 'EnableFirewall'), 'group-policy')?.query).not.toContain('EnableFirewall');
});
it('excludes customer identifiers, file names, values and injected queries', () => {
const sensitive = 'Synthetic Person synthetic.customer.invalid \\private\\file user@example.invalid';
const item = record({ Baseline: sensitive, Description: sensitive, Remediation: sensitive, 'Configured Value': sensitive, 'Registry path': sensitive,
'Search URL': 'https://evil.invalid/?q=' + sensitive, 'Source: File': sensitive }, sensitive);
const search = policyWebSearch(item, 'group-policy')!;
expect(search.query).toContain('Explain the Group Policy setting: Microsoft Group Policy.');
expect(new URL(search.url).searchParams.get('udm')).toBe('50');
expect(decodeURIComponent(search.url)).not.toContain('synthetic');
expect(policyWebSearch(item, 'cis-cat')).toBeUndefined();
});
it('uses only literal public topics even when uploaded metadata contains appended private text', () => {
const item = record({ Baseline: 'Office Security Baseline - Synthetic Secret', 'Policy type': 'advanced-audit-policy' });
expect(policyWebSearch(item, 'group-policy')?.query).toContain('Microsoft Office / advanced audit policy.');
expect(policyWebSearch(item, 'group-policy')?.query).not.toContain('Synthetic Secret');
expect(policyWebSearch(record({}, 'ADPasswordEncryptionEnabled'), 'group-policy')?.query).toContain('Windows LAPS ADPasswordEncryptionEnabled');
expect(policyWebSearch(record({}, 'ADPasswordEncryptionEnabled Synthetic Secret'), 'group-policy')?.query).not.toContain('ADPasswordEncryptionEnabled');
expect(policyWebSearch(record({}, '__proto__'), 'group-policy')?.query).toContain('Explain the Group Policy setting: Microsoft Group Policy.');
});
it('renders an explicit no-referrer search link and inert original documentation', () => {
const html = render(record({ Description: '<script>alert(1)</script> https://private.invalid/secret', Remediation: '[click](https://private.invalid)' }));
expect(html).toContain('>AI search</a>');
expect(html).toContain('rel="noopener noreferrer"');
expect(html).toContain('referrerPolicy="no-referrer"');
expect(html).toContain('&lt;script&gt;');
expect(html).not.toContain('href="https://private.invalid');
expect(html).not.toContain('<script>');
});
});