262 lines
18 KiB
TypeScript
262 lines
18 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 } 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('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('<script>');
|
|
expect(html).not.toContain('href="https://private.invalid');
|
|
expect(html).not.toContain('<script>');
|
|
});
|
|
});
|