Assessor_2.0/tests/features/cis-section-heading-interac...

111 lines
6.3 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', () => {
const useApp = vi.fn();
return { useApp, useOptionalApp: useApp };
});
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();
}
});