Assessor_2.0/tests/compliance/iso-work-facets.test.tsx

269 lines
18 KiB
TypeScript

// @vitest-environment jsdom
import { act, type ReactNode } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import { AppProvider, useApp } from '../../src/app/AppProvider';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import type { RegulatoryControlOverviewResponse } from '../../src/compliance/regulatory-mapping';
import {
BSI_PRACTICE_FACET_ID,
BSI_REQUIREMENT_FACET_ID,
complianceWorkFacetIndex,
ISO_CONTROL_FACET_ID,
ISO_MAPPING_FACET_ID,
ISO_MAPPING_FAILED,
ISO_MAPPING_NO_BSI,
ISO_THEME_FACET_ID,
resetComplianceWorkFacetsForTests,
useComplianceWorkFacets
} from '../../src/compliance/ui/compliance-work-facets';
import { markRegulatoryDecisionSaved, markRegulatoryMappingsChanged } from '../../src/compliance/ui/decision-revision';
import { filterAndSortRecords } from '../../src/features/search-filter/filter-records';
import { selectWorkFacets } from '../../src/features/search-filter/work-facets';
import { makeSheet } from '../../src/importers/sheet-factory';
import { genericProfile } from '../../src/profiles/generic/generic-profile';
// Work `ISO control` and `ISO theme` (Compliance Engine): a second reading of accepted controls that
// filters Work like the Context Cluster facet, sits before the final `Source: Scan`, and is absent while
// the engine is off.
const gpoKey = `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy', 'baseline', 'computer', 'registry:dummy:setting']))}`;
const assignments: RegulatoryControlOverviewResponse = {
version: 1, projectId: 'dummy-project', scope: { assessmentIds: ['dummy-assessment'], scanRunIds: [] }, findingCount: 3,
controls: [
{ controlId: '8.20', annexAControlId: 'A.8.20', title: 'Networks security', theme: 'Technological', findings: [
{ resultProjectionId: 'scanner-finding:dummy-a', sourceKind: 'scanner-finding', sourceResultId: 'dummy-a' },
{ resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' },
{ resultProjectionId: 'policy-evaluation-comparison:dummy', sourceKind: 'policy-evaluation', sourceResultId: 'dummy-comparison',
sourceRuleId: 'registry:dummy:setting', policyScope: 'computer' }] },
{ controlId: '5.15', annexAControlId: 'A.5.15', theme: 'Organizational', findings: [
{ resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }] }
]
};
function workSheet() {
const sheet = makeSheet('Dummy', [
['Title', 'Status', 'Source: Scan'],
['Dummy check A', 'Failed', 'Purple Knight'],
['Dummy check B', 'Passed', 'Purple Knight'],
['Dummy GPO setting', 'Failed', 'GPOs'],
['Dummy unmapped check', 'Failed', 'Purple Knight']
], '', 'dummy.csv', { isProjectSheet: true });
const [a, b, gpo] = sheet.analysis.records;
a.findingOccurrenceId = 'dummy-a';
b.findingOccurrenceId = 'dummy-b';
(gpo as { id: string }).id = gpoKey;
return sheet;
}
const options = (statusFilter = 'all', facetFilters = {}) => ({ dataset: null, activeProfile: genericProfile, reviews: {}, search: '',
actionOnly: false, statusFilter, facetFilters, complianceFacets: complianceWorkFacetIndex(assignments) });
describe('Work ISO control and ISO theme facets', () => {
it('offers accepted controls and themes before the final Source: Scan control', () => {
const facets = selectWorkFacets(workSheet(), options());
const ids = facets.map(facet => facet.id);
expect(ids.at(-1)).toBe('provenance:source-scan');
// Nothing reaches BSI in this fixture, so ISO mapping offers `ISO control, no BSI requirement` after ISO theme.
expect(ids.slice(-4, -1)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, ISO_MAPPING_FACET_ID]);
const control = facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!;
expect(control.label).toBe('ISO control');
expect(control.values.map(value => [value.label, value.count])).toEqual([['A.5.15', 1], ['A.8.20 · Networks security', 3]]);
const theme = facets.find(facet => facet.id === ISO_THEME_FACET_ID)!;
expect(theme.values.map(value => [value.value, value.label, value.count])).toEqual([['organizational', 'Organizational', 1], ['technological', 'Technological', 3]]);
});
it('offers ISO mapping: Mapping failed only while a mapping failed, and filters to exactly those records', () => {
const sheet = workSheet();
sheet.analysis.records[3].findingOccurrenceId = 'dummy-unmapped';
const failed = { ...assignments, failedFindings: [
{ resultProjectionId: 'scanner-finding:dummy-unmapped', sourceKind: 'scanner-finding' as const, sourceResultId: 'dummy-unmapped' }] };
const withFailed = { ...options(), complianceFacets: complianceWorkFacetIndex(failed) };
const ids = selectWorkFacets(sheet, withFailed).map(facet => facet.id);
expect(ids.slice(-4)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, ISO_MAPPING_FACET_ID, 'provenance:source-scan']);
const facet = selectWorkFacets(sheet, withFailed).find(item => item.id === ISO_MAPPING_FACET_ID)!;
expect(facet.label).toBe('ISO mapping');
expect(facet.values.map(value => [value.value, value.label, value.count])).toEqual([[ISO_MAPPING_FAILED, 'Mapping failed', 1],
[ISO_MAPPING_NO_BSI, 'ISO control, no BSI requirement', 3]]);
const titles = filterAndSortRecords(sheet, { ...withFailed, statusFilter: 'deviation',
facetFilters: { [ISO_MAPPING_FACET_ID]: { values: [ISO_MAPPING_FAILED] } }, sortColumn: '', sortDirection: 'asc' }).map(record => record.title);
expect(titles).toEqual(['Dummy unmapped check']);
// Without failed mappings and without a BSI derivation gap there is no such filter.
const allReachBsi = { ...assignments, bsi: { catalogVersion: '2026-01-01', crosswalkVersion: '0.1-synthetic', license: 'Synthetic.',
practices: [{ id: 'KONF', title: 'Konfiguration' }], missingTargets: [], requirements: [
{ requirementId: 'KONF.1.1', title: 'Synthetisch', practice: 'KONF', statement: 'Synthetisch.', via: [{ isoControlId: '8.20', relationship: 'subset-of' as const },
{ isoControlId: '5.15', relationship: 'subset-of' as const }] }] } };
expect(selectWorkFacets(sheet, { ...options(), complianceFacets: complianceWorkFacetIndex(allReachBsi) }).some(item => item.id === ISO_MAPPING_FACET_ID)).toBe(false);
});
it('offers ISO mapping: ISO control, no BSI requirement for records whose accepted controls reach no BSI requirement', () => {
const sheet = workSheet();
// Only 8.20 reaches a BSI requirement; Dummy check B also has 5.15 and therefore one.
const partial = { ...assignments, bsi: { catalogVersion: '2026-01-01', crosswalkVersion: '0.1-synthetic', license: 'Synthetic.',
practices: [{ id: 'KONF', title: 'Konfiguration' }], missingTargets: [], requirements: [
{ requirementId: 'KONF.1.1', title: 'Synthetisch', practice: 'KONF', statement: 'Synthetisch.', via: [{ isoControlId: '5.15', relationship: 'subset-of' as const }] }] } };
const withPartial = { ...options(), complianceFacets: complianceWorkFacetIndex(partial) };
const facet = selectWorkFacets(sheet, withPartial).find(item => item.id === ISO_MAPPING_FACET_ID)!;
expect(facet.values.map(value => [value.value, value.label, value.count])).toEqual([[ISO_MAPPING_NO_BSI, 'ISO control, no BSI requirement', 2]]);
const titles = filterAndSortRecords(sheet, { ...withPartial, facetFilters: { [ISO_MAPPING_FACET_ID]: { values: [ISO_MAPPING_NO_BSI] } },
sortColumn: '', sortDirection: 'asc' }).map(record => record.title);
expect(titles).toEqual(['Dummy check A', 'Dummy GPO setting']);
});
it('filters Work records by control and theme, matching scanner occurrences and GPO settings', () => {
const sheet = workSheet();
const titles = (filters: object, status = 'all') => filterAndSortRecords(sheet, { ...options(status, filters), sortColumn: '', sortDirection: 'asc' })
.map(record => record.title);
expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['5.15'] } })).toEqual(['Dummy check B']);
expect(titles({ [ISO_THEME_FACET_ID]: { values: ['technological'] } })).toEqual(['Dummy check A', 'Dummy check B', 'Dummy GPO setting']);
// Other filters still apply: the status filter narrows the ISO selection.
expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } }, 'deviation')).toEqual(['Dummy check A', 'Dummy GPO setting']);
});
it('counts each control against the other active filters', () => {
const facets = selectWorkFacets(workSheet(), options('deviation'));
expect(facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!.values.map(value => [value.value, value.count])).toEqual([['8.20', 2]]);
});
it('adds no ISO facet without loaded assignments', () => {
const facets = selectWorkFacets(workSheet(), { ...options(), complianceFacets: undefined });
expect(facets.some(facet => facet.id.startsWith('compliance:'))).toBe(false);
});
});
// Work `BSI requirement` and `BSI practice` (Phase 13F, stage 1): requirements derived from the accepted ISO
// controls through the BSI mapping, after the ISO facets and before the final `Source: Scan`. Synthetic ids.
describe('Work BSI requirement and BSI practice facets', () => {
const withBsi: RegulatoryControlOverviewResponse = { ...assignments, bsi: {
catalogVersion: '2026-01-01', crosswalkVersion: '0.1-synthetic', license: 'Synthetic attribution.',
practices: [{ id: 'BER', title: 'Berechtigung' }, { id: 'ARCH', title: 'Architektur' }, { id: 'DET', title: 'Detektion' }],
requirements: [
{ requirementId: 'ARCH.1.1', title: 'Synthetische Architektur', practice: 'ARCH', statement: 'Synthetisch.', via: [{ isoControlId: '8.20', relationship: 'subset-of' }] },
{ requirementId: 'BER.1.1', title: 'Synthetische Berechtigung', practice: 'BER', statement: 'Synthetisch.', via: [{ isoControlId: '5.15', relationship: 'equivalent-to' }] }
],
missingTargets: []
} };
const bsiOptions = (statusFilter = 'all', facetFilters = {}) => ({ ...options(statusFilter, facetFilters), complianceFacets: complianceWorkFacetIndex(withBsi) });
it('offers derived requirements and practices after the ISO facets, before the final Source: Scan control', () => {
const facets = selectWorkFacets(workSheet(), bsiOptions());
expect(facets.map(facet => facet.id).slice(-5)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, BSI_REQUIREMENT_FACET_ID, BSI_PRACTICE_FACET_ID, 'provenance:source-scan']);
const requirement = facets.find(facet => facet.id === BSI_REQUIREMENT_FACET_ID)!;
expect(requirement.label).toBe('BSI requirement');
expect(requirement.values.map(value => [value.value, value.label, value.count])).toEqual([
['arch.1.1', 'ARCH.1.1 · Synthetische Architektur', 3], ['ber.1.1', 'BER.1.1 · Synthetische Berechtigung', 1]]);
// Catalog order, only practices with a derived requirement.
expect(facets.find(facet => facet.id === BSI_PRACTICE_FACET_ID)!.values.map(value => [value.value, value.label, value.count]))
.toEqual([['ber', 'BER · Berechtigung', 1], ['arch', 'ARCH · Architektur', 3]]);
});
it('filters Work records by requirement and practice through their ISO controls, with the other filters still applying', () => {
const sheet = workSheet();
const titles = (filters: object, status = 'all') => filterAndSortRecords(sheet, { ...bsiOptions(status, filters), sortColumn: '', sortDirection: 'asc' })
.map(record => record.title);
expect(titles({ [BSI_REQUIREMENT_FACET_ID]: { values: ['ber.1.1'] } })).toEqual(['Dummy check B']);
expect(titles({ [BSI_PRACTICE_FACET_ID]: { values: ['arch'] } })).toEqual(['Dummy check A', 'Dummy check B', 'Dummy GPO setting']);
expect(titles({ [BSI_PRACTICE_FACET_ID]: { values: ['arch'] } }, 'deviation')).toEqual(['Dummy check A', 'Dummy GPO setting']);
});
it('adds no BSI facet when no requirement is derived', () => {
expect(selectWorkFacets(workSheet(), options()).some(facet => facet.id.startsWith('compliance:bsi-'))).toBe(false);
});
});
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
let host: HTMLDivElement;
let root: Root;
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
localStorage.clear();
resetEnginePreferencesForTests();
resetComplianceWorkFacetsForTests();
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
host = document.createElement('div');
document.body.append(host);
root = createRoot(host);
});
afterEach(async () => {
await act(async () => { root.unmount(); });
host.remove();
vi.restoreAllMocks();
vi.unstubAllGlobals();
resetEnginePreferencesForTests();
});
function Consumer(): ReactNode {
const index = useComplianceWorkFacets();
return <output data-iso-facets>{index ? index.facets.map(facet => facet.values.length).join(',') : 'none'}</output>;
}
function Harness({ withScope }: { withScope: boolean }): ReactNode {
const app = useApp();
return <>
<button type="button" onClick={() => void app.openProject('dummy-project', [], ['dummy-assessment'])}>Open</button>
<button type="button" onClick={() => app.setFacetFilter(ISO_CONTROL_FACET_ID, { values: ['8.20'] })}>Select</button>
<output data-filters>{JSON.stringify(app.facetFilters)}</output>
{withScope && <><Consumer/><Consumer/></>}
</>;
}
describe('Work ISO facet loading and the engine switch', () => {
it('shares one request for every Work consumer and reloads after a control decision', async () => {
const { loadCanonicalProjectWorkspace } = await import('../../src/features/project-import/load-canonical-project-workspace');
vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ assessmentIds: ['dummy-assessment'], models: [],
dataset: { fileName: 'Dummy', scanTool: 'Generic', sheets: [workSheet()] } } as never);
vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({} as never);
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments);
await act(async () => root.render(<AppProvider><Harness withScope/></AppProvider>));
await act(async () => { host.querySelector('button')!.click(); await new Promise(resolve => setTimeout(resolve, 0)); });
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(1);
expect([...host.querySelectorAll('[data-iso-facets]')].map(node => node.textContent)).toEqual(['2,2,1', '2,2,1']);
await act(async () => { markRegulatoryDecisionSaved(); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(2);
});
// Regression 2026-10-09: `Mapping failed` in the Compliance tab opened Work without filtering when the mapping failed
// after Work had loaded its assignments, because a failed mapping is no decision and Work kept the outdated state.
it('reloads the assignments after a mapping change and offers no outdated state meanwhile, so Mapping failed stays selected', async () => {
const { loadCanonicalProjectWorkspace } = await import('../../src/features/project-import/load-canonical-project-workspace');
vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ assessmentIds: ['dummy-assessment'], models: [],
dataset: { fileName: 'Dummy', scanTool: 'Generic', sheets: [workSheet()] } } as never);
vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({} as never);
let resolveFresh!: (value: RegulatoryControlOverviewResponse) => void;
const withFailure: RegulatoryControlOverviewResponse = { ...assignments, failedFindings: [
{ resultProjectionId: 'scanner-finding:dummy-a', sourceKind: 'scanner-finding', sourceResultId: 'dummy-a' }] };
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments')
.mockResolvedValueOnce(assignments)
.mockImplementationOnce(() => new Promise(resolve => { resolveFresh = resolve; }));
await act(async () => root.render(<AppProvider><Harness withScope/></AppProvider>));
await act(async () => { host.querySelector('button')!.click(); await new Promise(resolve => setTimeout(resolve, 0)); });
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('2,2,1');
// The Compliance tab saw a mapping fail: Work drops the outdated state while it loads the fresh one ...
await act(async () => { markRegulatoryMappingsChanged(); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(2);
expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('none');
// ... and then offers ISO mapping with Mapping failed, matching exactly that finding.
await act(async () => { resolveFresh(withFailure); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('2,2,2');
const index = complianceWorkFacetIndex(withFailure);
const mapping = index.facets.find(facet => facet.id === ISO_MAPPING_FACET_ID)!;
expect(mapping.values.map(value => value.value)).toEqual([ISO_MAPPING_FAILED, ISO_MAPPING_NO_BSI]);
expect(filterAndSortRecords(workSheet(), { ...options('all', { [ISO_MAPPING_FACET_ID]: { values: [ISO_MAPPING_FAILED] } }), complianceFacets: index,
sortColumn: '', sortDirection: 'asc' })
.map(record => record.findingOccurrenceId)).toEqual(['dummy-a']);
});
it('loads nothing and clears an active ISO filter when the Compliance Engine is switched off', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments);
await act(async () => root.render(<AppProvider><Harness withScope={false}/></AppProvider>));
await act(async () => host.querySelectorAll('button')[1].click());
expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } });
await act(async () => { setEngineEnabled('compliance', false); });
expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({});
await act(async () => root.render(<AppProvider><Harness withScope/></AppProvider>));
expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('none');
expect(request).not.toHaveBeenCalled();
});
});