import { resetEnginePreferencesForTests, useEnginePreferences } from '../../src/app/engine-preferences'; import { EnginesControl } from '../../src/components/EnginesControl'; // @vitest-environment jsdom import { act, useState } from 'react'; import { FindingReader } from '../../src/features/finding-reader/FindingReader'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { RELEASED_ASSESSMENT_CONTEXT_CATALOG } from '../../src/assessment-context/context-catalog'; import { buildAssessmentContext } from '../../src/assessment-context/context-engine'; import type { StoredGpoPolicyAnalysis } from '../../src/domain/gpo-analysis-persistence'; import type { UnifiedResultProjection } from '../../src/domain/unified-result'; import { CONTEXT_CLUSTER_FACET_ID, contextClusterIndex, withContextClusterFacet } from '../../src/features/context-layer/context-cluster-facet'; import { resetAssessmentContextRequestsForTests } from '../../src/features/context-layer/use-assessment-context'; import { resetContextClusterIndexForTests } from '../../src/features/context-layer/use-context-cluster-index'; import type { DynamicFacet } from '../../src/features/search-filter/dynamic-facets'; import { filterAndSortRecords } from '../../src/features/search-filter/filter-records'; import { SearchFilterBar } from '../../src/features/search-filter/SearchFilterBar'; import { selectWorkFacets } from '../../src/features/search-filter/work-facets'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; }); const LSA = 'MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\LmCompatibilityLevel'; function syntheticContext() { const observations = [{ resultProjectionId: 'policy-observation:obs-lm', sourceKind: 'policy-observation', sourceResultId: 'obs-lm', policyScope: 'computer', policyState: 'configured', policyIdentityStatus: 'resolved', policyIdentityKey: 'dummy-identity-lm', policyValues: [{ kind: 'registry', typeCode: 4, rawData: '2' }], policyBackupInstanceId: 'backup-a' }] as unknown as UnifiedResultProjection[]; const analyses = [{ run: { id: 'dummy' }, resolutions: [{ observationId: 'obs-lm', resolution: { status: 'resolved', identity: { id: 'dummy-identity-lm', target: { kind: 'security-template', section: 'Registry Values', key: LSA } } } }] }] as unknown as StoredGpoPolicyAnalysis[]; const finding = (occurrence: string, tool: string, ruleId: string, outcome: string) => ({ resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence, sourceTool: tool, sourceRuleId: ruleId, outcomeNormalized: outcome, title: ruleId }) as unknown as UnifiedResultProjection; return buildAssessmentContext({ projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1, observations, analyses, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG, scannerFindings: [finding('occ-rbcd', 'purple-knight', 'SI000079', 'fail'), finding('occ-pc', 'pingcastle', 'S-OldNtlm', 'fail')] }); } function workSheet() { const sheet = makeSheet('Dummy', [ ['Title', 'Status', 'Scope', 'Technical Identity'], ['Dummy LAN Manager setting', 'Failed', 'computer', 'dummy-identity-lm'], ['Dummy RBCD finding', 'Failed', '', ''], ['Dummy NTLM finding', 'Passed', '', ''], ['Dummy unrelated finding', 'Failed', '', ''] ], '', 'dummy.csv', { isProjectSheet: true }); const [, rbcd, ntlm, unrelated] = sheet.analysis.records; rbcd.findingOccurrenceId = 'occ-rbcd'; ntlm.findingOccurrenceId = 'occ-pc'; unrelated.findingOccurrenceId = 'occ-unrelated'; return sheet; } const baseOptions = (sheet = workSheet()) => ({ sheet, options: { activeProfile: genericProfile, search: '', sortColumn: '', sortDirection: 'asc' as const, statusFilter: 'all', facetFilters: {}, dataset: { fileName: 'Dummy', sheets: [sheet], scanTool: 'Generic' } as never, actionOnly: false, reviews: {} } }); describe('Work facet Context Cluster', () => { it('permanent acceptance: without a context filter Work records and every other facet count are identical with the layer on and off', () => { const { sheet, options } = baseOptions(); const index = contextClusterIndex(syntheticContext()); for (const statusFilter of ['all', 'deviation']) { const off = filterAndSortRecords(sheet, { ...options, statusFilter }); const on = filterAndSortRecords(sheet, { ...options, statusFilter, contextClusters: index }); expect(on.map(record => record.id)).toEqual(off.map(record => record.id)); const facetsOff = selectWorkFacets(sheet, { ...options, statusFilter }); const facetsOn = selectWorkFacets(sheet, { ...options, statusFilter, contextClusters: index }); expect(facetsOn.filter(facet => facet.id !== CONTEXT_CLUSTER_FACET_ID && facet.id !== 'report:business-impact') .map(({ valuesFor: _v, ...facet }) => facet)).toEqual(facetsOff.map(({ valuesFor: _v, ...facet }) => facet)); expect(facetsOn.length).toBe(facetsOff.length + 2); } }); it('offers reachable clusters and selects exactly the GPO settings and findings that provide evidence', () => { const { sheet, options } = baseOptions(); const index = contextClusterIndex(syntheticContext()); const facet = selectWorkFacets(sheet, { ...options, contextClusters: index }).find(item => item.id === CONTEXT_CLUSTER_FACET_ID)!; expect(facet.label).toBe('Context Cluster'); expect(facet.values.map(value => value.value)).toEqual(['credential-relay-exposure', 'kerberos-delegation-abuse']); const count = (id: string) => facet.values.find(value => value.value === id)!.count; expect(count('credential-relay-exposure')).toBe(2); expect(count('kerberos-delegation-abuse')).toBe(1); expect(facet.values.some(value => value.value === 'adcs-escalation')).toBe(false); const relay = filterAndSortRecords(sheet, { ...options, contextClusters: index, facetFilters: { [CONTEXT_CLUSTER_FACET_ID]: { values: ['credential-relay-exposure'] } } }); expect(relay.map(record => record.title)).toEqual(['Dummy LAN Manager setting', 'Dummy NTLM finding']); const delegation = filterAndSortRecords(sheet, { ...options, contextClusters: index, facetFilters: { [CONTEXT_CLUSTER_FACET_ID]: { values: ['kerberos-delegation-abuse'] } } }); expect(delegation.map(record => record.title)).toEqual(['Dummy RBCD finding']); }); it('stays directly before the permanent final Source: Scan control', () => { const facet = (id: string) => ({ id, label: id, source: 'derived', kind: 'category', values: [] }) as DynamicFacet; const index = contextClusterIndex(syntheticContext()); expect(withContextClusterFacet([facet('status'), facet('provenance:source-scan')], index).map(item => item.id)) .toEqual(['status', CONTEXT_CLUSTER_FACET_ID, 'provenance:source-scan']); expect(withContextClusterFacet([facet('status')], undefined).map(item => item.id)).toEqual(['status']); }); it('counts All once per record with overlapping clusters and respects search and status', () => { const { sheet, options } = baseOptions(); const original = contextClusterIndex(syntheticContext()); const index = { ...original, facet: { ...original.facet, valuesFor: () => ['credential-relay-exposure', 'kerberos-delegation-abuse'] } }; const count = (search: string, statusFilter: string) => selectWorkFacets(sheet, { ...options, search, statusFilter, contextClusters: index, facetFilters: { [CONTEXT_CLUSTER_FACET_ID]: { values: ['credential-relay-exposure'] } } }).find(f => f.id === CONTEXT_CLUSTER_FACET_ID)!; expect(count('', 'all').allCount).toBe(4); expect(count('RBCD', 'all').allCount).toBe(1); expect(count('not-present', 'all').allCount).toBe(0); expect(count('', 'passed').allCount).toBe(1); }); }); describe('Work facet Context Cluster in the filter bar', () => { let host: HTMLDivElement; let root: Root; const setFacetFilter = vi.fn(); function mockApp(facetFilters: Record) { const { sheet } = baseOptions(); vi.mocked(useApp).mockReturnValue({ activeSheet: sheet, activeProfile: genericProfile, dataset: { fileName: 'Dummy', sheets: [sheet], scanTool: 'Generic' }, reviews: {}, search: '', statusFilter: 'all', setStatusFilter: vi.fn(), setActiveSheet: vi.fn(), facetFilters, pathFindingIds: [], setFacetFilter, resetFilters: vi.fn(), actionOnly: false, setActionOnly: vi.fn(), targetProjectId: 'dummy-project', activeAssessmentIds: ['dummy-assessment'], activeScanRunIds: [] } as unknown as ReturnType); } beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetAssessmentContextRequestsForTests(); resetContextClusterIndexForTests(); setFacetFilter.mockReset(); host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); afterEach(async () => { await act(async () => { root.unmount(); }); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it.each(['dark', 'light'])('selects the badge cluster in the visible Work filter and filters the record set in %s', async theme => { document.documentElement.dataset.theme = theme; const context = syntheticContext(); vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(context); mockApp({}); const initial = useApp(); const { sheet, options } = baseOptions(); function WorkFixture() { const engines = useEnginePreferences(); const [facets, setFacets] = useState>({}); vi.mocked(useApp).mockReturnValue({ ...initial, activeSheet: sheet, facetFilters: facets, setFacetFilter: (id, filter) => setFacets(current => { const next = { ...current }; if (filter?.values?.length) next[id] = { values: filter.values }; else delete next[id]; return next; }) }); const filtered = filterAndSortRecords(sheet, { ...options, facetFilters: facets, contextClusters: engines.context ? contextClusterIndex(context) : undefined }); return <>{filtered.length} ; } await act(async () => { root.render(); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(host.querySelector('[data-record-count]')?.textContent).toBe('4'); await act(async () => { host.querySelector('[data-context-badges] [data-cluster-id="credential-relay-exposure"]')!.click(); }); const select = host.querySelector('select[aria-label="Context Cluster"]')!; expect(select.value).toBe('credential-relay-exposure'); expect(select.options[0].textContent).toBe('Context Cluster: All (4)'); expect(select.classList.contains('is-filtered')).toBe(true); expect(host.querySelector('[data-record-count]')?.textContent).toBe('2'); expect(host.querySelector('details.finding-context')!.open).toBe(false); await act(async () => { select.value = ''; select.dispatchEvent(new Event('change', { bubbles: true })); }); expect(host.querySelector('[data-record-count]')?.textContent).toBe('4'); expect(host.querySelector('details.finding-context')).not.toBeNull(); expect(select.options[0].textContent).toBe('Context Cluster: All (4)'); await act(async () => { host.querySelector('[data-context-badges] [data-cluster-id="credential-relay-exposure"]')!.click(); }); await act(async () => { host.querySelector('.engines-button')!.click(); }); const contextToggle = [...document.querySelectorAll('.engines-option')] .find(label => label.textContent === 'Context Engine')!.querySelector('input')!; await act(async () => { contextToggle.click(); }); expect(host.querySelector('details.finding-context')).toBeNull(); expect(host.querySelector('select[aria-label="Context Cluster"]')).toBeNull(); expect(useApp().facetFilters[CONTEXT_CLUSTER_FACET_ID]).toBeUndefined(); expect(host.querySelector('[data-record-count]')?.textContent).toBe('4'); await act(async () => { contextToggle.click(); }); expect(host.querySelector('select[aria-label="Context Cluster"]')).not.toBeNull(); expect(host.querySelector('details.finding-context')!.open).toBe(false); delete document.documentElement.dataset.theme; }); it('always shows the facet and keeps the selected cluster even with an obsolete off preference', async () => { vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(syntheticContext()); localStorage.setItem('ASSESSOR.contextLayer', 'off'); mockApp({ [CONTEXT_CLUSTER_FACET_ID]: { values: ['credential-relay-exposure'] } }); await act(async () => { root.render(); await new Promise(resolve => setTimeout(resolve, 0)); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); expect(host.textContent).toContain('Context Cluster'); expect(setFacetFilter).not.toHaveBeenCalledWith(CONTEXT_CLUSTER_FACET_ID, null); }); it('offers only reachable clusters as search changes, keeps an empty selection visible and permits All', async () => { vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(syntheticContext()); mockApp({}); await act(async () => { root.render(); await new Promise(resolve => setTimeout(resolve, 0)); }); const select = () => host.querySelector('select[aria-label="Context Cluster"]')!; expect([...select().options].map(option => option.value)).toEqual(['', 'credential-relay-exposure', 'kerberos-delegation-abuse']); await act(async () => { select().value = 'credential-relay-exposure'; select().dispatchEvent(new Event('change', { bubbles: true })); }); expect(setFacetFilter).toHaveBeenLastCalledWith(CONTEXT_CLUSTER_FACET_ID, { values: ['credential-relay-exposure'] }); mockApp({ [CONTEXT_CLUSTER_FACET_ID]: { values: ['credential-relay-exposure'] } }); vi.mocked(useApp).mockReturnValue({ ...useApp(), search: 'RBCD' }); await act(async () => root.render()); expect(select().value).toBe('credential-relay-exposure'); expect([...select().options].filter(option => !option.disabled).map(option => option.value)).toEqual(['', 'kerberos-delegation-abuse']); expect(select().selectedOptions[0].disabled).toBe(true); expect(select().options[0].textContent).toBe('Context Cluster: All (1)'); expect(setFacetFilter).not.toHaveBeenCalledWith(CONTEXT_CLUSTER_FACET_ID, null); await act(async () => { select().value = ''; select().dispatchEvent(new Event('change', { bubbles: true })); }); expect(setFacetFilter).toHaveBeenLastCalledWith(CONTEXT_CLUSTER_FACET_ID, null); mockApp({}); vi.mocked(useApp).mockReturnValue({ ...useApp(), search: 'not-present' }); await act(async () => root.render()); expect([...select().options].map(option => option.textContent)).toEqual(['Context Cluster: All (0)']); }); });