108 lines
6.2 KiB
TypeScript
108 lines
6.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { useApp } from '../../src/app/AppProvider';
|
|
import type { ImportedDataset } from '../../src/domain/dataset';
|
|
import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { DatasetViewer } from '../../src/features/dataset-viewer/DatasetViewer';
|
|
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
|
|
import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
|
|
vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
|
|
vi.mock('../../src/features/search-filter/use-filtered-records', () => ({ useFilteredRecords: vi.fn() }));
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
Reflect.deleteProperty(HTMLElement.prototype, 'scrollIntoView');
|
|
});
|
|
|
|
/** Mixed project sheet as the canonical projection returns it: the CIS record knows its run's section titles. */
|
|
function scannerDataset(): ImportedDataset {
|
|
const sheet = makeSheet('Synthetic · All Records', [
|
|
['Source Rule ID', 'Title', 'Status', 'Source: Scan'],
|
|
['2.2.2', '2.2.2 Ensure a synthetic user right is restricted', 'Fail', 'CIS-CAT Pro · 2026-01-01T00:00:00.000Z']
|
|
], '', 'Synthetic', { isProjectSheet: true });
|
|
const record = sheet.analysis.records[0];
|
|
record.scanRunId = 'run-synthetic';
|
|
record.benchmarkSectionTitles = new Map([['2', 'Synthetic Local Policies'], ['2.2', 'Synthetic User Rights Assignment']]);
|
|
return { sheets: [sheet], rawText: '', profileId: 'generic', scanTool: 'Multiple Tools' } as unknown as ImportedDataset;
|
|
}
|
|
|
|
/** A timeslot that also holds GPO results: Work rebuilds the project sheet to append them. */
|
|
function withPolicyObservations(dataset: ImportedDataset): ImportedDataset {
|
|
const observation = {
|
|
sourceKind: 'policy-observation', sourceResultId: 'synthetic-observation', projectId: 'synthetic', assessmentId: 'slot',
|
|
policyBackupInstanceId: 'backup-1', title: 'Synthetic setting', importedAt: '2026-01-01T00:00:00Z',
|
|
policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyIdentityStatus: 'resolved',
|
|
policyIdentityKey: 'synthetic-setting', policyValues: [{ kind: 'registry-policy', operation: 'set-value',
|
|
key: 'Software\\Policies\\Synthetic', valueName: 'Mode', typeCode: 4, data: { kind: 'uint32', value: 0 } }]
|
|
} as unknown as UnifiedResultProjection;
|
|
const comparison = {
|
|
id: 'synthetic-comparison', observationIds: [observation.sourceResultId], baselineId: 'baseline', baselineLabel: 'Synthetic baseline',
|
|
identity: { id: 'setting-synthetic', scope: 'computer', target: { kind: 'registry-value', key: 'Synthetic', valueName: 'Mode' } },
|
|
baselineState: 'explicit-recommendation', customerState: 'configured', classification: 'confirmed_configured_deviation', diagnosticCodes: [],
|
|
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different',
|
|
baseline: { kind: 'integer', value: '1' }, actual: { kind: 'integer', value: '0' }, diagnosticCodes: [] }
|
|
} as unknown as PolicyEvaluationResult;
|
|
const evaluation = { run: { id: 'evaluation', projectId: 'synthetic', aggregationRunId: 'aggregation',
|
|
completedAt: '2026-01-01T00:00:00Z', evaluator: POLICY_EVALUATION_ENGINE }, results: [comparison] } as StoredPolicyEvaluation;
|
|
return appendPolicyObservationsToDataset(dataset, 'Synthetic', [observation], [evaluation])!;
|
|
}
|
|
|
|
// User correction 2026-09-26: Work headings name the CIS benchmark section instead of the rule number;
|
|
// the exact number stays visible as Source Rule ID in the report. Regression 2026-09-26: with GPO results
|
|
// in the timeslot, Work rebuilt the project sheet and dropped the titles, so the headings kept the number.
|
|
it.each([
|
|
['without GPO results', (dataset: ImportedDataset) => dataset],
|
|
['with GPO results appended to the project sheet', withPolicyObservations]
|
|
])('opens a CIS finding in mixed Work %s with the section title in the list and report headings', async (_case, project) => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
|
|
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { value: vi.fn(), configurable: true });
|
|
|
|
const dataset = project(scannerDataset());
|
|
const sheet = dataset.sheets[0];
|
|
const record = sheet.analysis.records.find(item => item.fields.some(field => field.label === 'Source Rule ID' && field.value === '2.2.2'))!;
|
|
expect(record.scanRunId).toBe('run-synthetic');
|
|
const state = {
|
|
dataset,
|
|
activeSheet: sheet,
|
|
view: 'records',
|
|
search: '',
|
|
titleColumn: 'Title',
|
|
selectedRecordId: record.id,
|
|
reviews: {},
|
|
facetFilters: {},
|
|
statusFilter: 'all',
|
|
actionOnly: false,
|
|
setView: vi.fn(),
|
|
setSelectedRecord: vi.fn(),
|
|
updateReview: vi.fn()
|
|
} as unknown as ReturnType<typeof useApp>;
|
|
vi.mocked(useApp).mockReturnValue(state);
|
|
vi.mocked(useFilteredRecords).mockReturnValue([record]);
|
|
|
|
const container = document.createElement('div');
|
|
document.body.append(container);
|
|
const root = createRoot(container);
|
|
try {
|
|
await act(async () => root.render(<DatasetViewer />));
|
|
const heading = 'Synthetic User Rights Assignment: Ensure a synthetic user right is restricted';
|
|
expect(container.querySelector('.finding-reader h2')?.textContent).toBe(heading);
|
|
expect(container.textContent!.split(heading).length).toBeGreaterThan(2);
|
|
expect(container.textContent).not.toContain('2.2.2 Ensure');
|
|
expect(container.querySelector('[data-source-rule-id] code')?.textContent).toBe('2.2.2');
|
|
expect([...container.querySelectorAll('.finding-reader [data-derived-category="cis"]')].map(badge => badge.getAttribute('title')))
|
|
.toEqual(['Synthetic Local Policies', 'Synthetic User Rights Assignment']);
|
|
expect(record.fields.find(field => field.label === 'Title')?.value).toBe('2.2.2 Ensure a synthetic user right is restricted');
|
|
} finally {
|
|
await act(async () => root.unmount());
|
|
container.remove();
|
|
}
|
|
});
|