229 lines
16 KiB
TypeScript
229 lines
16 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { regulatoryMappingResultId } from '../../src/compliance/ui/use-regulatory-mapping';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { readFileSync } from 'node:fs';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
|
|
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
|
|
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
|
|
import { wellKnownPrincipalNames } from '../../src/profiles/group-policy/well-known-principals';
|
|
|
|
/** Synthetic privilege-right setting compared against several baseline packages. */
|
|
function fixture(packages: number[], configured = ['*S-1-5-32-544', '*S-1-5-32-549'],
|
|
expectedFor: (index: number) => string[] = () => ['*S-1-5-32-544']) {
|
|
const actual = { kind: 'principal-set', section: 'Privilege Rights', key: 'SeSyntheticPrivilege', principals: configured } as const;
|
|
const observation = { sourceKind: 'policy-observation', sourceResultId: 'synthetic-observation', projectId: 'synthetic',
|
|
assessmentId: 'slot', policyBackupInstanceId: 'synthetic-backup', policyIdentityKey: 'synthetic-right',
|
|
policyIdentityStatus: 'resolved', policyScope: 'computer', policyState: 'configured', policyDeliveryMechanism: 'security-template',
|
|
importedAt: '2026-09-01T00:00:00Z', title: 'Privilege Rights · SeSyntheticPrivilege', policyValues: [actual],
|
|
sourceLocator: { objectPath: 'Synthetic/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf#lines=1' }
|
|
} as unknown as UnifiedResultProjection;
|
|
const comparator = (baseline: string[]) => ({ version: 1, comparator: { kind: 'set', mode: 'exact' },
|
|
relation: configured.length === baseline.length && configured.every(item => baseline.includes(item)) ? 'match' : 'different',
|
|
diagnosticCodes: [], actual: { kind: 'string-set', values: configured }, baseline: { kind: 'string-set', values: baseline } });
|
|
const results = packages.map((variants, index) => {
|
|
const expected = { ...actual, principals: expectedFor(index) };
|
|
const variantList = Array.from({ length: variants }, (_, variant) => ({
|
|
settingId: `setting-${index}-${variant}`, baselineValues: [expected], comparator: comparator(expected.principals)
|
|
}));
|
|
const decision = variantList.every(item => item.comparator.relation === 'match') ? 'all-pass' : 'all-fail';
|
|
return {
|
|
id: `comparison-${index}`, baselineId: `baseline-${index}`, baselineLabel: `Synthetic baseline ${index}`,
|
|
baselineAnalysisId: `analysis-${index}`, observationIds: [observation.sourceResultId],
|
|
baselineState: 'explicit-recommendation', customerState: 'configured',
|
|
classification: decision === 'all-pass' ? 'configured_match' : 'confirmed_configured_deviation',
|
|
identity: { id: 'synthetic-right', scope: 'computer', target: { kind: 'security-template', section: 'Privilege Rights', key: 'SeSyntheticPrivilege' } },
|
|
diagnosticCodes: [], comparator: comparator(expected.principals),
|
|
baselineVariantAssessment: { version: 1, scope: 'backup-configuration', decision, diagnosticCodes: [],
|
|
actualValues: [actual], variants: variantList }
|
|
};
|
|
}) as unknown as PolicyEvaluationResult[];
|
|
const evaluation = { run: { id: 'evaluation', projectId: 'synthetic', aggregationRunId: 'aggregation',
|
|
evaluator: POLICY_EVALUATION_ENGINE, completedAt: '2026-09-02T00:00:00Z' }, results } as StoredPolicyEvaluation;
|
|
return { observation, evaluation };
|
|
}
|
|
|
|
function read(observation: UnifiedResultProjection, evaluations: StoredPolicyEvaluation[]) {
|
|
const dataset = appendPolicyObservationsToDataset(null, 'Synthetic', [observation], evaluations)!;
|
|
const record = dataset.sheets[0].analysis.records[0];
|
|
const html = renderToStaticMarkup(<FindingReader record={record} title={record.title} linkedSheets={[]} resultReferences={[]}
|
|
search="" position={0} recordCount={1} toolId="group-policy" onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>);
|
|
const container = document.createElement('div');
|
|
container.innerHTML = html;
|
|
return { record, container };
|
|
}
|
|
|
|
const count = (text: string, part: string) => text.split(part).length - 1;
|
|
|
|
describe('GPO Finding Reader baseline comparison', () => {
|
|
it('keeps source, baseline and following field spacing when opening and closing comparisons', () => {
|
|
const { observation, evaluation } = fixture([1, 2]);
|
|
const { container } = read(observation, [evaluation]);
|
|
const tokens = readFileSync('src/styles/ui-tokens.css', 'utf8');
|
|
const spacing = tokens.match(/--space-4:\s*([^;]+);/)![1];
|
|
const style = document.createElement('style');
|
|
// jsdom does not resolve custom properties; retain the actual production rules.
|
|
style.textContent = readFileSync('src/styles/components.css', 'utf8')
|
|
.replaceAll('var(--space-4)', spacing).replaceAll('var(--border-hair)', '1px')
|
|
.replaceAll('var(--line)', 'gray').replaceAll('var(--line-strong)', 'gray');
|
|
document.head.append(style);
|
|
document.body.append(container);
|
|
try {
|
|
const sources = container.querySelector('.policy-source-evidence')!;
|
|
const baseline = container.querySelector('.policy-baseline-comparison')!;
|
|
const details = baseline.querySelector('details')!;
|
|
const checkSpacing = () => {
|
|
for (const section of [sources, baseline]) {
|
|
expect(getComputedStyle(section).paddingBottom).toBe(spacing);
|
|
expect(getComputedStyle(section).marginBottom).toBe(spacing);
|
|
}
|
|
expect(getComputedStyle(sources.querySelector('.policy-source-group:last-child')!).borderBottomWidth).toBe('0px');
|
|
const last = container.querySelector('.reader-summary > :last-child')!;
|
|
expect(getComputedStyle(last).borderBottomWidth).toBe('0px');
|
|
};
|
|
checkSpacing();
|
|
(details.querySelector('summary') as HTMLElement).click();
|
|
expect(details.open).toBe(true);
|
|
checkSpacing();
|
|
(details.querySelector('summary') as HTMLElement).click();
|
|
expect(details.open).toBe(false);
|
|
checkSpacing();
|
|
} finally {
|
|
container.remove();
|
|
style.remove();
|
|
}
|
|
});
|
|
it('states identical variant outcomes once and keeps every stored comparison in a closed disclosure', () => {
|
|
const { observation, evaluation } = fixture([1, 2, 3]);
|
|
const { record, container } = read(observation, [evaluation]);
|
|
const section = container.querySelector('.policy-baseline-comparison')!;
|
|
expect(section.getAttribute('data-consistent')).toBe('true');
|
|
// The first block states the problem; the comparison section does not repeat it.
|
|
const problem = container.querySelector('.policy-problem')!;
|
|
expect(problem.querySelector('h4')?.textContent).toBe('Problem');
|
|
const values = Object.fromEntries([...problem.querySelectorAll('.policy-problem__values > div')]
|
|
.map(row => [row.querySelector('dt')!.textContent, row.querySelector('dd')!.textContent]));
|
|
// Red marks the member absent from every variant, green the member every variant expects;
|
|
// the separate Additional entries row is then redundant.
|
|
expect(values).toEqual({ Configured: '*S-1-5-32-544 (Administrators)*S-1-5-32-549 (Server Operators)',
|
|
Expected: 'Values: *S-1-5-32-544 (Administrators)' });
|
|
expect([...problem.querySelectorAll('.policy-problem__entry[data-state]')].map(entry => [entry.textContent, entry.getAttribute('data-state')]))
|
|
.toEqual([['*S-1-5-32-544 (Administrators)', 'aligned'], ['*S-1-5-32-549 (Server Operators)', 'deviating']]);
|
|
// The source section carries the same proof: green for the expected member, red for the extra one.
|
|
expect(container.querySelector('.policy-source-aligned-value')?.textContent?.trim()).toBe('*S-1-5-32-544 (Administrators)');
|
|
expect(container.querySelector('.policy-source-deviation-value')?.textContent?.trim()).toBe('*S-1-5-32-549 (Server Operators)');
|
|
expect(problem.querySelector('.policy-problem__context')?.textContent)
|
|
.toBe('1 GPO backup source · differs from all 6 assessed baseline variants in 3 baseline packages');
|
|
expect(problem.nextElementSibling?.matches('.policy-next-step')).toBe(true);
|
|
expect(container.querySelector('.policy-next-step')?.textContent).toContain('Expected: Values: *S-1-5-32-544');
|
|
expect(problem.compareDocumentPosition(container.querySelector('.policy-source-evidence')!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
expect(section.querySelector('.policy-baseline-comparison__verdict')).toBeNull();
|
|
expect(section.querySelector('.policy-baseline-comparison__facts')).toBeNull();
|
|
const text = container.querySelector('.finding-reader')!.textContent!;
|
|
expect(count(text, 'target role is not established')).toBe(1);
|
|
expect(count(text, 'not effective device security')).toBe(1);
|
|
const details = section.querySelector('details')!;
|
|
expect(details.hasAttribute('open')).toBe(false);
|
|
expect(details.querySelector('summary')?.textContent).toBe('All baseline comparisons (3 packages, 6 variants)');
|
|
expect(details.querySelectorAll('.policy-baseline-package')).toHaveLength(3);
|
|
expect(details.querySelectorAll('.policy-baseline-variant')).toHaveLength(6);
|
|
// User correction 2026-09-25: every variant shows its own expected value and result, not only the package verdict.
|
|
const firstVariant = details.querySelector('.policy-baseline-variant')!;
|
|
expect(firstVariant.querySelector('.policy-baseline-variant__values dd')?.textContent).toBe('Values: *S-1-5-32-544 (Administrators)');
|
|
expect(firstVariant.querySelector('.policy-baseline-variant__result')?.textContent).toBe('Different');
|
|
expect(firstVariant.querySelector('.policy-baseline-variant__result')?.getAttribute('data-outcome')).toBe('fail');
|
|
expect(details.querySelectorAll('.policy-baseline-variant__values')).toHaveLength(6);
|
|
expect(details.textContent).not.toContain('Every variant below has the outcome shown above.');
|
|
// Expected value and remediation appear once instead of once per package.
|
|
expect(container.querySelector('.policy-value-block[data-value="expected"]')).toBeNull();
|
|
expect(container.querySelectorAll('.policy-problem__step')).toHaveLength(1);
|
|
expect(container.querySelector('.reader-remediation')).toBeNull();
|
|
expect(details.querySelector('table')).toBeNull();
|
|
for (const id of ['setting-0-0', 'setting-1-1', 'setting-2-2']) expect(details.textContent).toContain(`Setting reference: ${id}`);
|
|
// The repeated per-package fields are not rendered a second time in the reader.
|
|
expect([...container.querySelectorAll('.reader-summary h4')].map(item => item.textContent)).not.toContain('Comparison');
|
|
// Exports and search keep the complete original text.
|
|
const description = record.fields.find(field => field.label === 'Description')!.value;
|
|
expect(count(description, 'target role is not established')).toBe(3);
|
|
expect(count(description, 'Setting reference:')).toBe(6);
|
|
});
|
|
|
|
it('opens the disclosure and does not claim a shared outcome when package verdicts differ', () => {
|
|
const { observation, evaluation } = fixture([1, 1]);
|
|
const pass = evaluation.results[1];
|
|
const variant = pass.baselineVariantAssessment!.variants[0];
|
|
variant.baselineValues = observation.policyValues as never;
|
|
variant.comparator = { ...variant.comparator, relation: 'match',
|
|
baseline: { kind: 'string-set', values: ['*S-1-5-32-544', '*S-1-5-32-549'] } } as never;
|
|
pass.comparator = variant.comparator;
|
|
pass.classification = 'configured_match';
|
|
pass.baselineVariantAssessment!.decision = 'all-pass';
|
|
const { container } = read(observation, [evaluation]);
|
|
const section = container.querySelector('.policy-baseline-comparison')!;
|
|
expect(section.getAttribute('data-consistent')).toBe('false');
|
|
expect(section.querySelector('.policy-baseline-comparison__verdict')?.textContent).toMatch(/^Outcomes differ between 2 assessed baseline variants/);
|
|
expect(section.querySelector('.policy-baseline-comparison__statuses')?.textContent).toBe('Fail: 1 · Pass: 1');
|
|
expect(section.querySelectorAll('.policy-baseline-value-group')).toHaveLength(2);
|
|
expect(section.querySelector('details')?.hasAttribute('open')).toBe(true);
|
|
expect(section.querySelectorAll('.policy-baseline-variant__values')).toHaveLength(2);
|
|
expect([...section.querySelectorAll('.policy-baseline-package__header .outcome-mark')].map(item => item.getAttribute('data-outcome'))).toEqual(['fail', 'pass']);
|
|
});
|
|
|
|
it('groups differing expected values of a uniform Fail and names only the exception packages', () => {
|
|
const { observation, evaluation } = fixture([1, 1, 1, 1], ['*S-1-5-32-544', '*S-1-5-32-549'],
|
|
index => index === 3 ? ['*S-1-5-32-551'] : ['*S-1-5-32-544']);
|
|
const { record, container } = read(observation, [evaluation]);
|
|
const section = container.querySelector('.policy-baseline-comparison')!;
|
|
expect(section.getAttribute('data-uniform-outcome')).toBe('true');
|
|
expect(section.getAttribute('data-consistent')).toBe('false');
|
|
const problem = container.querySelector('.policy-problem')!;
|
|
expect(problem.querySelector('.policy-problem__context')?.textContent)
|
|
.toBe('1 GPO backup source · differs from all 4 assessed baseline variants in 4 baseline packages');
|
|
const expected = [...problem.querySelectorAll('.policy-problem__values > div')].find(row => row.querySelector('dt')?.textContent === 'Expected');
|
|
expect([...expected!.querySelectorAll('.policy-problem__entry')].map(item => item.textContent)).toEqual([
|
|
'Values: *S-1-5-32-544 (Administrators) — 3 baseline packages',
|
|
'Values: *S-1-5-32-551 (Backup Operators) — Synthetic baseline 3'
|
|
]);
|
|
// Package-specific differences have no single shared entry list.
|
|
expect(problem.textContent).not.toContain('Additional entries');
|
|
expect(section.querySelector('.policy-baseline-comparison__statuses')).toBeNull();
|
|
expect(section.querySelector('.policy-baseline-value-group')).toBeNull();
|
|
// A uniform result keeps the per-package list closed; its values stay available per variant.
|
|
expect(section.querySelector('details')?.hasAttribute('open')).toBe(false);
|
|
expect(section.querySelectorAll('.policy-baseline-variant__values')).toHaveLength(4);
|
|
expect(container.querySelector('.policy-value-block[data-value="expected"]')).toBeNull();
|
|
const next = container.querySelector('.policy-next-step')!;
|
|
expect(next.textContent).toContain('Establish the applicable baseline template');
|
|
expect([...next.querySelectorAll('.policy-problem__packages')].map(item => item.textContent))
|
|
.toEqual(['3 baseline packages', 'Synthetic baseline 3']);
|
|
expect(next.querySelector('.policy-problem__sources')?.textContent).toBe('In: GPO backup synthetic-backup');
|
|
expect(record.fields.find(field => field.label === 'Remediation')!.value.split('Computer policy:').length - 1).toBe(4);
|
|
});
|
|
|
|
it('names a current stored comparison of the setting for ISO control mapping', () => {
|
|
const { observation, evaluation } = fixture([1, 1]);
|
|
const { record } = read(observation, [evaluation]);
|
|
expect(record.findingOccurrenceId).toBeUndefined();
|
|
expect(evaluation.results.map(result => `policy-evaluation-comparison:${result.id}`)).toContain(regulatoryMappingResultId(record));
|
|
});
|
|
|
|
it('keeps the complete stored text for a historical comparison', () => {
|
|
const { observation, evaluation } = fixture([1, 1]);
|
|
evaluation.run.evaluator = { ...POLICY_EVALUATION_ENGINE, version: '1.8.0' };
|
|
const { record, container } = read(observation, [evaluation]);
|
|
expect(record.policyBaselineComparisons).toBeUndefined();
|
|
// A historical comparison is not a current Result DB row, so ISO mapping has nothing to name.
|
|
expect(regulatoryMappingResultId(record)).toBe('');
|
|
expect(container.querySelector('.policy-baseline-comparison')).toBeNull();
|
|
});
|
|
|
|
it('names only documented well-known SIDs next to the unchanged original value', () => {
|
|
expect(wellKnownPrincipalNames('Values: *S-1-5-32-544, *S-1-5-32-549')).toEqual(['Administrators', 'Server Operators']);
|
|
// Standardized domain RIDs are named; the domain part itself stays unresolved.
|
|
expect(wellKnownPrincipalNames('*S-1-5-21-1000-2000-3000-512')).toEqual(['Domain Admins']);
|
|
expect(wellKnownPrincipalNames('*S-1-5-21-1000-2000-3000-1105')).toEqual([]);
|
|
expect(wellKnownPrincipalNames('*S-1-5-32-5440')).toEqual([]);
|
|
});
|
|
});
|