import { useEffect, useState } from 'react'; import { AssessorApiClient } from '../../api/assessor-api-client'; import { useApp } from '../../app/AppProvider'; import { useEnginePreferences } from '../../app/engine-preferences'; import type { FindingRecord } from '../../domain/finding'; import { gpoWorkSettingIdentity } from '../../features/results-browser/work-record-lookup'; import type { DynamicFacet } from '../../features/search-filter/dynamic-facets'; import type { RegulatoryControlFinding, RegulatoryControlOverviewResponse, RegulatoryControlTheme } from '../regulatory-mapping'; import { useRegulatoryDecisionRevision } from './decision-revision'; /** Work facets of the Compliance Engine; present only while the engine is on and its assignments are loaded. */ export const ISO_CONTROL_FACET_ID = 'compliance:iso-control'; export const ISO_THEME_FACET_ID = 'compliance:iso-theme'; export const BSI_REQUIREMENT_FACET_ID = 'compliance:bsi-requirement'; export const BSI_PRACTICE_FACET_ID = 'compliance:bsi-practice'; export const ISO_MAPPING_FACET_ID = 'compliance:iso-mapping'; /** `ISO mapping` facet values: the rule's latest mapping failed and nothing is accepted. */ export const ISO_MAPPING_FAILED = 'failed'; /** `ISO mapping` facet values: accepted ISO controls, but none reaches a BSI requirement through the BSI mapping. */ export const ISO_MAPPING_NO_BSI = 'no-bsi'; export type ComplianceMappingState = typeof ISO_MAPPING_FAILED | typeof ISO_MAPPING_NO_BSI; export const COMPLIANCE_FACET_IDS = [ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, ISO_MAPPING_FACET_ID, BSI_REQUIREMENT_FACET_ID, BSI_PRACTICE_FACET_ID] as const; const SOURCE_SCAN_FACET_ID = 'provenance:source-scan'; const THEMES: RegulatoryControlTheme[] = ['Organizational', 'People', 'Physical', 'Technological']; export interface ComplianceWorkFacetIndex { facets: DynamicFacet[]; } const indexes = new WeakMap(); /** One shared index per loaded response, so every Work consumer uses identical cache keys. */ export function complianceWorkFacetIndex(response: RegulatoryControlOverviewResponse): ComplianceWorkFacetIndex { const existing = indexes.get(response); if (existing) return existing; const byOccurrence = new Map>(); const bySetting = new Map>(); const add = (map: Map>, key: string, controlId: string) => map.set(key, (map.get(key) ?? new Set()).add(controlId)); const addFinding = (finding: RegulatoryControlFinding, value: string, occurrences: Map>, settings: Map>) => { if (finding.sourceKind === 'scanner-finding') add(occurrences, finding.sourceResultId, value); else if (finding.sourceRuleId) add(settings, `${finding.policyScope ?? ''}\u0000${finding.sourceRuleId}`, value); }; for (const control of response.controls) for (const finding of control.findings) addFinding(finding, control.controlId, byOccurrence, bySetting); // Scanner findings match by occurrence; GPO settings by scope and technical setting identity, like Result DB drill-downs. const valuesOf = (occurrences: Map>, settings: Map>) => (record: FindingRecord): string[] => { if (record.findingOccurrenceId) return [...occurrences.get(record.findingOccurrenceId) ?? []]; const identity = gpoWorkSettingIdentity(record.id); return identity ? [...settings.get(`${identity.scope ?? ''}\u0000${identity.settingId}`) ?? []] : []; }; const controlsFor = valuesOf(byOccurrence, bySetting); const failedOccurrences = new Map>(); const failedSettings = new Map>(); for (const finding of response.failedFindings ?? []) addFinding(finding, ISO_MAPPING_FAILED, failedOccurrences, failedSettings); const themeOf = new Map(response.controls.map(control => [control.controlId, control.theme])); // BSI requirements derived from the accepted ISO controls through the BSI mapping (Phase 13F, stage 1). const bsi = response.bsi; const requirementsByControl = new Map(); for (const requirement of bsi?.requirements ?? []) { for (const via of requirement.via) requirementsByControl.set(via.isoControlId, [...requirementsByControl.get(via.isoControlId) ?? [], requirement.requirementId]); } const practiceOf = new Map((bsi?.requirements ?? []).map(requirement => [requirement.requirementId, requirement.practice])); const requirementsFor = (record: FindingRecord): string[] => [...new Set(controlsFor(record).flatMap(controlId => requirementsByControl.get(controlId) ?? []))]; const controls = [...response.controls].sort((left, right) => left.controlId.localeCompare(right.controlId, 'en', { numeric: true })); const failedFor = valuesOf(failedOccurrences, failedSettings); // A finding has no BSI requirement when none of its accepted controls reaches one; offered only with hits. const hasNoBsi = [...byOccurrence.values(), ...bySetting.values()] .some(controlIds => [...controlIds].every(controlId => !requirementsByControl.has(controlId))); const mappingValues = [ ...(response.failedFindings?.length ? [{ value: ISO_MAPPING_FAILED, label: 'Mapping failed', count: 0 }] : []), ...(hasNoBsi ? [{ value: ISO_MAPPING_NO_BSI, label: 'ISO control, no BSI requirement', count: 0 }] : [])]; const index: ComplianceWorkFacetIndex = { facets: [{ id: ISO_CONTROL_FACET_ID, label: 'ISO control', source: 'derived', kind: 'category', description: 'Compliance Engine: findings and GPO settings with this accepted ISO/IEC 27002 control. It does not change results.', values: controls.map(control => ({ value: control.controlId, label: `${control.annexAControlId}${control.title ? ` · ${control.title}` : ''}`, count: 0 })), valuesFor: controlsFor }, { id: ISO_THEME_FACET_ID, label: 'ISO theme', source: 'derived', kind: 'category', description: 'Compliance Engine: findings and GPO settings with an accepted ISO/IEC 27002 control of this theme. It does not change results.', // Facet values are matched in lower case; the label keeps the ISO spelling. values: THEMES.filter(theme => response.controls.some(control => control.theme === theme)) .map(theme => ({ value: theme.toLowerCase(), label: theme, count: 0 })), valuesFor: record => [...new Set(controlsFor(record).flatMap(controlId => themeOf.get(controlId)?.toLowerCase() ?? []))] }, // Present only while one of its values has hits, so the Client Info coverage rows open exactly these in Work. ...(mappingValues.length ? [{ id: ISO_MAPPING_FACET_ID, label: 'ISO mapping', source: 'derived', kind: 'category', description: 'Compliance Engine: findings and GPO settings whose latest ISO mapping failed (map them again from the Finding Reader), or whose accepted ISO controls reach no BSI requirement. It does not change results.', values: mappingValues, valuesFor: record => [...failedFor(record), ...(controlsFor(record).length && !requirementsFor(record).length ? [ISO_MAPPING_NO_BSI] : [])] }] satisfies DynamicFacet[] : []), // The BSI facets appear only when a requirement is derived, so a client without one gets no empty controls. ...(bsi?.requirements.length ? [{ id: BSI_REQUIREMENT_FACET_ID, label: 'BSI requirement', source: 'derived', kind: 'category', description: 'Compliance Engine: findings and GPO settings with a BSI Grundschutz++ requirement derived from their accepted ISO controls through the BSI mapping. It does not change results.', // Values are matched in lower case, like the ISO theme; the label keeps the BSI spelling. values: bsi.requirements.map(requirement => ({ value: requirement.requirementId.toLowerCase(), label: `${requirement.requirementId} · ${requirement.title}`, count: 0 })), valuesFor: record => requirementsFor(record).map(requirementId => requirementId.toLowerCase()) }, { id: BSI_PRACTICE_FACET_ID, label: 'BSI practice', source: 'derived', kind: 'category', description: 'Compliance Engine: findings and GPO settings with a derived BSI Grundschutz++ requirement of this practice. It does not change results.', // Catalog order; values are matched in lower case, the label keeps the BSI spelling. values: bsi.practices.filter(practice => bsi.requirements.some(requirement => requirement.practice === practice.id)) .map(practice => ({ value: practice.id.toLowerCase(), label: `${practice.id} · ${practice.title}`, count: 0 })), valuesFor: (record: FindingRecord) => [...new Set(requirementsFor(record).flatMap(requirementId => practiceOf.get(requirementId)?.toLowerCase() ?? []))] }] satisfies DynamicFacet[] : [])] }; indexes.set(response, index); return index; } /** Adds the ISO and BSI facets directly before the permanent final `Source: Scan` control. */ export function withComplianceFacets(facets: DynamicFacet[], index: ComplianceWorkFacetIndex | undefined): DynamicFacet[] { if (!index) return facets; const sourceScan = facets.findIndex(facet => facet.id === SOURCE_SCAN_FACET_ID); return sourceScan < 0 ? [...facets, ...index.facets] : [...facets.slice(0, sourceScan), ...index.facets, ...facets.slice(sourceScan)]; } const assessorApi = new AssessorApiClient(); // One request per timeslot and decision revision, shared by every Work consumer. const requests = new Map>(); const ready = new Map(); const lastReadyByScope = new Map(); /** The ISO facet index for Work, or undefined while the engine is off or nothing is loaded yet. */ export function useComplianceWorkFacets(): ComplianceWorkFacetIndex | undefined { const app = useApp(); const available = useEnginePreferences().compliance; const revision = useRegulatoryDecisionRevision(); const assessmentIds = [...(app.activeAssessmentIds ?? [])].sort(); const scanRunIds = [...(app.activeScanRunIds ?? [])].sort(); const projectId = app.targetProjectId; const scope = JSON.stringify([projectId, assessmentIds, scanRunIds]); const key = `${scope}\u0000${revision}`; const enabled = available && app.backendStatus === 'connected' && Boolean(projectId) && (assessmentIds.length > 0 || scanRunIds.length > 0); const [, setLoaded] = useState(0); useEffect(() => { if (!enabled || !projectId || ready.has(key)) return; let active = true; let request = requests.get(key); if (!request) { request = assessorApi.getRegulatoryControlAssignments(projectId, { assessmentIds, scanRunIds }); requests.set(key, request); request.then(response => { ready.set(key, response); lastReadyByScope.set(scope, response); while (ready.size > 8) ready.delete(ready.keys().next().value!); while (lastReadyByScope.size > 4) lastReadyByScope.delete(lastReadyByScope.keys().next().value!); }, () => undefined).finally(() => requests.delete(key)); } request.then(() => { if (active) setLoaded(value => value + 1); }, () => undefined); return () => { active = false; }; }, [enabled, key]); // eslint-disable-line react-hooks/exhaustive-deps if (!enabled) return undefined; // While a new decision revision of the same timeslot loads, the last loaded state keeps an active filter valid. const response = ready.get(key) ?? lastReadyByScope.get(scope); return response ? complianceWorkFacetIndex(response) : undefined; } /** Test support. */ export function resetComplianceWorkFacetsForTests(): void { requests.clear(); ready.clear(); lastReadyByScope.clear(); }