import { Fragment, useState, type ChangeEvent, type CSSProperties } from 'react'; import type { DatasetSheet } from '../../domain/dataset'; import type { FindingRecord, RecordField } from '../../domain/finding'; import type { Review } from '../../domain/review'; import { defaultReview } from '../../domain/review'; import { riskAcceptanceState, type RiskAcceptanceCommand } from '../../domain/risk-acceptance'; import { RiskAcceptanceDialog } from './RiskAcceptanceDialog'; import { PolicySourceEvidence } from './PolicySourceEvidence'; import { PolicyBaselineComparison } from './PolicyBaselineComparison'; import { PolicyProblemSummary } from './PolicyProblemSummary'; import { FindingFirstRead } from './FindingFirstRead'; import { FindingContext, FindingContextBadges } from './FindingContext'; import { useContextClusterIndex } from '../context-layer/use-context-cluster-index'; import { numberedClusterLabel, publishedClusterColour } from '../sidebar/context-cluster-display'; import { FindingWorkPackage } from '../../remediation/FindingWorkPackage'; import { RelatedFindingRing, useRelatedFindings } from '../related-findings/RelatedFindings'; import { findingFirstRead, firstReadConsumedFields, firstReadFieldRank, firstReadImpactSplit, firstReadSectionSplit } from './first-read'; import { policyProblemSummary } from '../../profiles/group-policy/problem-summary'; import { normalizeSeverity, mitreBadgeLabel } from '../../profiles/purple-knight/presentation'; import { RichTextValue } from '../../components/RichTextValue/RichTextValue'; import { PathText } from '../../components/PathText/PathText'; import { normalizeNumericValue } from '../../shared/text'; import { sourceFileLabel } from '../../shared/source-file-label'; import { outcomeForStatusValue, toolIdForRecord } from '../../importers/status-normalization'; import { isFileReference } from '../../profiles/purple-knight/result-linker'; import { highlightText } from '../../components/HighlightText/highlight-text'; import { BookmarkIcon } from '../../components/BookmarkIcon'; import { TriangleIcon } from '../../components/TriangleIcon'; import { resultSectionFields, resultStatusFieldFor } from '../linked-results/result-message'; import { cisBenchmarkCategories } from '../../profiles/cis-cat/cis-benchmark-hierarchy'; import { cisCategoryRecordTitle } from '../../profiles/cis-cat/cis-benchmark-section-titles'; import { policyComparisonDisplayLines, policyValueName } from '../../profiles/group-policy/presentation'; import { policyDiagnosticBadges, policyFindingCategories } from '../../profiles/group-policy/categories'; import { findingWebSearch } from '../../profiles/public-search/search'; import { policyEvidenceResultSheets, POLICY_CONFLICT_DETAILS, POLICY_DUPLICATE_DETAILS } from '../../profiles/group-policy/conflict-presentation'; import { maesterExecutionErrorText, maesterFindingTitle, maesterTagText } from '../../profiles/maester/presentation'; import type { BackendStatus } from '../../app/app-state'; import { RegulatoryMappingPanel } from './RegulatoryMappingPanel'; import { regulatoryMappingResultId, useRegulatoryMapping } from './use-regulatory-mapping'; interface Props { record: FindingRecord; title: string; review?: Review; previousPolicyReviews?: Array<{ id: string; baseline: string; review: Review }>; linkedSheets: DatasetSheet[]; resultReferences: string[]; search: string; position: number; recordCount: number; /** Werkzeug-ID des Datasets; entscheidet ueber die werkzeuggebundene Statusnormalisierung. */ toolId?: string; onPrevious(): void; onNext(): void; onReview(patch: Partial): void; onRiskAcceptance?(command: RiskAcceptanceCommand): Promise; onResults?(): void; /** Ist das Ergebnisfenster zu diesem Fund gerade geoeffnet? */ resultsOpen?: boolean; /** Farbe des geöffneten Result-Fensters; entspricht der Kachelmarkierung. */ resultColor?: string; projectId?: string; backendStatus?: BackendStatus; } const statuses = ['Open', 'In Review', 'Clarification Needed', 'Actionable', 'Blocked', 'Done', 'Not Relevant']; const priorities = ['High', 'Medium', 'Low']; const normalizeLabel = (label: string) => label.trim().toLocaleLowerCase('en-US').replace(/[\s_-]+/g, ' '); const normalizeValue = (value: string) => value.trim().replace(/\s+/g, ' ').toLocaleLowerCase('en-US'); /** * Felder, die nicht in die Lesespalte gehoeren, sondern unter die * Zusatzinformationen. Herkunft (Datei, Blatt, Scan) und Kennungen sind ueber * die Filterleiste erreichbar beziehungsweise reine Nachschlagewerte - im * Finding-Abschnitt drueckten sie die eigentliche Aussage nach unten. */ const isAdditionalOnlyField = (label: string, toolId?: string) => { const normalized = normalizeLabel(label); // GPO technical provenance is reference data below the problem statement. Scope stays in the report (2026-09-06). if (toolId === 'group-policy' && /^(?:registry path|technical identity|imported|evaluated|diagnostics)$/.test(normalized)) return true; return /^(?:short\s*name|si version|target|runtime|points scale|rule id|source rule id|policy type|policy section|description source)$/.test(normalized) || /^(?:quelle|source):? /.test(normalized) || /^anssi \d+$/.test(normalized); }; /** * Ein Feld gilt als reine Ueberschrift/Badge und wird nicht als Fliesstext * gezeigt. "Result" gehoert immer dazu: bei Purple Knight ist es ein kurzes * Statuswort (Text separat in "Result Message"), bei Maester der vollstaendige * Befund - der gehoert dann aber in das Results-Fenster (siehe * `resultMessageFor`/`FloatingResultsWindow`), nicht in den Finding-Fliesstext. */ const isHeadingOrBadgeField = (label: string) => /^(?:name|title|finding|indicator|results?|status|evaluation|category|severity|criticality|priority|weight|score)$/.test(normalizeLabel(label)); /** Werkzeugdetails, die bei strukturierten Results nicht doppelt erscheinen duerfen. */ const isRedundantDetailField = (label: string) => /^(?:details?|result detail|resultdetail)$/.test(normalizeLabel(label)); const isPingCastleLinkedSectionField = (label: string) => normalizeLabel(label) === 'verwandte daten'; function legacyPurpleKnightFrameworkFields(record: FindingRecord, toolId?: string): { fields: RecordField[]; sourceIndexes: number[]; } { if (toolId !== 'purple-knight') return { fields: [], sourceIndexes: [] }; const source = record.fields.find(field => normalizeLabel(field.label) === 'framework' && field.value); if (!source || source.value.length > 64 * 1024) return { fields: [], sourceIndexes: [] }; let parsed: unknown; try { parsed = JSON.parse(source.value); } catch { return { fields: [], sourceIndexes: [] }; } if (!Array.isArray(parsed) || parsed.length > 32) return { fields: [], sourceIndexes: [] }; const fields: RecordField[] = []; const nextIndex = Math.max(-1, ...record.fields.map(field => field.index)) + 1; parsed.forEach(item => { if (!item || typeof item !== 'object' || Array.isArray(item)) return; const framework = item as { Name?: unknown; Tags?: unknown }; if (typeof framework.Name !== 'string' || !Array.isArray(framework.Tags)) return; const frameworkName = framework.Name; framework.Tags.slice(0, 32).forEach(tag => { if (typeof tag !== 'string' || !tag.trim()) return; const ordinal = fields.filter(field => normalizeLabel(field.label).startsWith(normalizeLabel(frameworkName))).length + 1; fields.push({ index: nextIndex + fields.length, label: `${frameworkName}_${ordinal}`, value: tag.trim() }); }); }); return fields.length ? { fields, sourceIndexes: [source.index] } : { fields: [], sourceIndexes: [] }; } const CIS_CAT_DETAILS_LABEL = 'cis cat details'; const RULE_RESULT_XML_LABEL = 'rule result xml'; const RULE_RESULT_XML_HEADING = /^##\s+Rule Result XML\s*$/im; /** * Das Rule Result XML bleibt bewusst hier: ein standardmaessig geschlossenes * Detail im Finding, das man gezielt aufklappt. Es ist nicht der Befund, * sondern dessen maschinenlesbare Rohfassung, und wuerde das Results-Fenster * vor den eigentlichen Belegtabellen fuellen. Aeltere Importe kennen statt des * eigenen Feldes nur das zusammengefasste "CIS-CAT Details"; daraus wird der * XML-Abschnitt herausgeloest. * * `resultSectionIndexes` sind die Felder, die bereits das Results-Fenster * traegt (siehe `resultSectionsFor`, bei CIS-CAT die Rahmenwerk-Zuordnungen). * Sie werden hier ausgeblendet, damit dieselbe Aussage nicht doppelt steht. */ function cisCatOptionalFields(record: FindingRecord, toolId?: string, resultSectionIndexes = new Set()) { const hiddenFieldIndexes = new Set(resultSectionIndexes); if (toolId !== 'cis-cat') return { optionalFields: [] as RecordField[], hiddenFieldIndexes }; const explicitXml = record.fields.find(field => normalizeLabel(field.label) === RULE_RESULT_XML_LABEL && field.value); const legacyDetails = record.fields.find(field => normalizeLabel(field.label) === CIS_CAT_DETAILS_LABEL && field.value); if (legacyDetails) hiddenFieldIndexes.add(legacyDetails.index); const cleanXmlValue = (val: string) => val.replace(/^```\w*\n?/, '').replace(/\n?```$/, '').trim(); if (explicitXml) { hiddenFieldIndexes.add(explicitXml.index); if (resultSectionIndexes.has(explicitXml.index)) return { optionalFields: [] as RecordField[], hiddenFieldIndexes }; return { optionalFields: [{ ...explicitXml, value: cleanXmlValue(explicitXml.value) }], hiddenFieldIndexes }; } if (!legacyDetails) return { optionalFields: [] as RecordField[], hiddenFieldIndexes }; const heading = RULE_RESULT_XML_HEADING.exec(legacyDetails.value); if (!heading?.length || heading.index === undefined) return { optionalFields: [] as RecordField[], hiddenFieldIndexes }; const remainder = legacyDetails.value.slice(heading.index + heading[0].length).trimStart(); const nextHeading = remainder.search(/^##\s+/m); const value = cleanXmlValue((nextHeading >= 0 ? remainder.slice(0, nextHeading) : remainder).trim()); return { optionalFields: value ? [{ ...legacyDetails, label: 'Rule Result XML', value }] : [], hiddenFieldIndexes }; } /** * Purple Knight legt seine Belegstellen als Abschnitt "References" in dieselbe * Zelle wie den vorangehenden Fliesstext. Beim Import kommt daraus ein einziges * Feld, in dem der Referenzblock ohne Trennung am Satzende klebt. * * Die Aufteilung geschieht bewusst hier in der Darstellung und nicht beim * Import: der kanonische Feldwert bleibt damit unveraendert die Quellzelle * (siehe AGENTS.md - Originalwerte und Provenienz erhalten), und bereits * importierte Berichte profitieren ohne erneuten Import. * * Die Verweisziele selbst sind an dieser Stelle bereits verloren: Purple Knight * hinterlegt sie als Hyperlinks der Excel-Zelle, der Importer liest nur den * Zelltext. Der Block wird deshalb als Liste der Bezeichnungen sichtbar, nicht * als anklickbare Verweise. */ const referencesSection = /\s*>?\s*References(?![a-z])\s*/; function splitReferenceSection(fields: RecordField[]): RecordField[] { return fields.flatMap(field => { const match = referencesSection.exec(field.value); if (!match?.index) return [field]; const body = field.value.slice(0, match.index).replace(/[\s>]+$/, ''); const references = field.value.slice(match.index + match[0].length).trim(); if (!body || !references) return [field]; return [{ ...field, value: body }, { ...field, label: 'References', value: references }]; }); } export function selectFindingFields(record: FindingRecord, title: string, hasStructuredResults = false, toolId?: string) { const derivedCategories = cisBenchmarkCategories(record, toolId); const legacyFramework = legacyPurpleKnightFrameworkFields(record, toolId); const resultSectionIndexes = new Set(resultSectionFields(record, toolId).map(field => field.index)); const { optionalFields: cisCatFields, hiddenFieldIndexes } = cisCatOptionalFields(record, toolId, resultSectionIndexes); const optionalFields = [...cisCatFields]; const sourceRuleId = record.fields.find(field => normalizeLabel(field.label) === 'source rule id' && field.value.trim()); if (sourceRuleId) hiddenFieldIndexes.add(sourceRuleId.index); legacyFramework.sourceIndexes.forEach(index => hiddenFieldIndexes.add(index)); // Die technische PingCastle-Sektions-ID bleibt im Datensatz, weil der // Results-Linker damit die aufgeloesten Tabellen eindeutig zuordnet. Im // Reader ist sie jedoch reine Implementierungsprovenienz ohne fachlichen // Lesewert und wird deshalb weder im Finding noch unter Additional // Information ausgegeben. if (toolId === 'pingcastle') { record.fields .filter(field => isPingCastleLinkedSectionField(field.label)) .forEach(field => hiddenFieldIndexes.add(field.index)); } // Die Defender-Rohbeobachtungen bleiben als maschinenlesbare Provenienz im // Datensatz (die Defender-Sonderansicht rekonstruiert daraus ihr Modell), // gehoeren aber nicht in die Lesespalte: der fachliche Beleg steht als // "Affected Devices"-Tabelle im Ergebnisfenster. if (toolId === 'defender-for-iot-vulnerabilities') { record.fields .filter(field => normalizeLabel(field.label) === 'source observations') .forEach(field => hiddenFieldIndexes.add(field.index)); } if (hasStructuredResults) { record.fields .filter(field => isRedundantDetailField(field.label)) .forEach(field => hiddenFieldIndexes.add(field.index)); } if (toolId === 'purple-knight' && (legacyFramework.fields.length || record.fields.some(field => isRedundantDetailField(field.label)))) { record.fields .filter(field => /^(?:grade|exposure type)$/.test(normalizeLabel(field.label))) .forEach(field => hiddenFieldIndexes.add(field.index)); } const statusField = resultStatusFieldFor(record, toolId); const maesterPresentationFields: RecordField[] = []; if (toolId === 'maester') { let presentationIndex = Math.max(-1, ...record.fields.map(field => field.index)) + 1; const displayField = (source: RecordField, label: string, value = source.value): RecordField => ({ ...source, index: presentationIndex++, label, value }); const byLabel = (label: string) => record.fields.find(field => normalizeLabel(field.label) === label && field.value); const id = byLabel('id'); const script = byLabel('testscript'); const category = byLabel('block'); const tags = byLabel('tag'); const source = byLabel('testscriptfile'); const error = byLabel('errormessage'); record.fields.filter(field => /^(?:id|result|resultdetail|severity|tag|block|duration|errormessage|name|helpurl|testscriptfile|testscript|index)$/.test(normalizeLabel(field.label))) .forEach(field => hiddenFieldIndexes.add(field.index)); if (!sourceRuleId && id) maesterPresentationFields.push(displayField(id, 'ID')); if (script) optionalFields.push(displayField(script, 'Test details', script.value)); if (category) maesterPresentationFields.push(displayField(category, 'Category')); if (tags) maesterPresentationFields.push(displayField(tags, 'Tags', maesterTagText(tags.value))); if (source) maesterPresentationFields.push(displayField(source, 'Source')); if (error) optionalFields.push({ ...error, label: 'Technical execution detail', value: maesterExecutionErrorText(error.value) }); } const hasPolicyEvidenceResults = toolId === 'group-policy' && hasStructuredResults && policyEvidenceResultSheets(record).length > 0; const hasPolicySourceEvidence = toolId === 'group-policy' && Boolean(record.policySourceEvidence?.length); if (hasPolicySourceEvidence) record.fields.filter(field => ['Sources', 'Configured Value'].includes(field.label)) .forEach(field => hiddenFieldIndexes.add(field.index)); // Structured variant comparisons replace the repeated per-package text in the reader; // the unchanged fields remain the export and search source. const hasPolicyBaselineComparisons = toolId === 'group-policy' && Boolean(record.policyBaselineComparisons?.length); if (hasPolicyBaselineComparisons) record.fields // Conflict and Duplicates summaries are stated in the problem summary. .filter(field => ['Description', 'Baseline', 'Comparison', 'Baseline value', 'Remediation', 'Conflict', 'Duplicates'].includes(field.label)) .forEach(field => hiddenFieldIndexes.add(field.index)); if (hasPolicyEvidenceResults) record.fields.filter(field => [POLICY_CONFLICT_DETAILS, POLICY_DUPLICATE_DETAILS, 'Sources', 'Configured Value'].includes(field.label)) .forEach(field => hiddenFieldIndexes.add(field.index)); const policyComparisonFields = toolId === 'group-policy' && (!hasPolicyEvidenceResults || hasPolicySourceEvidence) ? record.fields.filter(field => { const label = normalizeLabel(field.label); // The grouped baseline comparison states expected values once per distinct value. return field.value && ((!hasPolicySourceEvidence && label === 'configured value') || (!hasPolicyBaselineComparisons && label === 'baseline value')); }) : []; const policyReadiness = toolId === 'group-policy' ? record.fields.find(field => normalizeLabel(field.label) === 'evaluation readiness' && field.value) : undefined; if (policyReadiness) hiddenFieldIndexes.add(policyReadiness.index); policyComparisonFields.forEach(field => hiddenFieldIndexes.add(field.index)); const badges = record.fields.filter(field => { if (!field.value) return false; const label = normalizeLabel(field.label); return /^(?:category|severity|weight|score|category score|domain risk level)$/.test(label) || (/^points?$/.test(label) && normalizeNumericValue(field.value) !== null); }); const mitre = [...record.fields, ...legacyFramework.fields].filter(field => mitreBadgeLabel(field.label) && field.value); // Scanner first read (user decision 2026-09-25): problem, impact and next step are shown above, // so their fields are not repeated; background precedes references. if (toolId !== 'group-policy') firstReadConsumedFields(record, toolId).forEach(field => hiddenFieldIndexes.add(field.index)); // A native remediation subsection moved to the next step is not repeated; the text before it stays as background. // References a scanner appended to its impact text move to the references position. const sectionSplit = toolId !== 'group-policy' ? firstReadSectionSplit(record, toolId) : undefined; const impactSplit = toolId !== 'group-policy' ? firstReadImpactSplit(record, toolId) : undefined; const sectionBackground: RecordField[] = []; let firstReadIndex = Math.max(-1, ...record.fields.map(field => field.index), ...maesterPresentationFields.map(field => field.index)) + 1; if (sectionSplit) { hiddenFieldIndexes.add(sectionSplit.field.index); if (sectionSplit.background) sectionBackground.push({ ...sectionSplit.field, value: sectionSplit.background, index: firstReadIndex++ }); } if (impactSplit?.references) sectionBackground.push({ ...impactSplit.field, label: 'References', value: impactSplit.references, index: firstReadIndex++ }); const summaryPriority = (label: string) => { const normalized = normalizeLabel(label); const firstReadRank = toolId !== 'group-policy' ? firstReadFieldRank(label, toolId) : undefined; if (firstReadRank !== undefined) return firstReadRank; if (normalized === 'conflict' || normalized === 'duplicates') return -1; if (toolId === 'maester' && normalized === 'id') return -1; if (normalized === 'result message') return 0; if (toolId === 'maester' && normalized === 'description') return 1; if (toolId === 'maester' && normalized === 'test details') return 2; if (toolId === 'maester' && normalized === 'category') return 3; if (toolId === 'maester' && normalized === 'tags') return 4; if (toolId === 'maester' && normalized === 'source') return 5; if (/^number of results?$/.test(normalized)) return 1; if (normalized === 'description') return 2; if (toolId === 'pingcastle' && normalized === 'technical explanation') return 3; if (toolId === 'pingcastle' && normalized === 'advised solution') return 4; if (toolId === 'pingcastle' && normalized === 'documentation') return 5; if (/^remediation(?: guidance| recommendation)?$/.test(normalized)) return 3; if (/^(source|quelle)(?:\s*:\s*.*)?$/.test(normalized)) return 4; return 10; }; const summaryValues = new Set(); const presentationFields = [...record.fields, ...maesterPresentationFields, ...sectionBackground]; const maesterPresentationIndexes = new Set(maesterPresentationFields.map(field => field.index)); const summaryFields = presentationFields .filter(field => { if (hiddenFieldIndexes.has(field.index)) return false; if (!field.value || field.value === title || badges.some(badge => badge.index === field.index) || mitreBadgeLabel(field.label) || isAdditionalOnlyField(field.label, toolId) || (!maesterPresentationIndexes.has(field.index) && isHeadingOrBadgeField(field.label))) return false; if (hasStructuredResults && isRedundantDetailField(field.label)) return false; const value = normalizeValue(field.value); if (summaryValues.has(value)) return false; summaryValues.add(value); return true; }) .sort((left, right) => summaryPriority(left.label) - summaryPriority(right.label) || left.index - right.index); const summary = splitReferenceSection(summaryFields).slice(0, 12); const displayedIndexes = new Set([ ...hiddenFieldIndexes, ...summary.map(field => field.index), ...badges.map(field => field.index), ...mitre.map(field => field.index), ...(statusField ? [statusField.index] : []) ]); const displayedTextValues = new Set([title, ...summary.map(field => field.value)].map(normalizeValue)); const remainingFields = [...presentationFields, ...legacyFramework.fields.filter(field => !mitreBadgeLabel(field.label))].filter(field => { if (!field.value || displayedIndexes.has(field.index)) return false; if (isAdditionalOnlyField(field.label, toolId)) return true; if (isHeadingOrBadgeField(field.label)) return false; return !displayedTextValues.has(normalizeValue(field.value)); }); // "Check title" ist die Pruefbeschreibung der Regel und dient als Ueberschrift // des Finding-Abschnitts. Die Aufteilung erfolgt bewusst erst hier, damit das // Feld weiterhin als angezeigt gilt und nicht zusaetzlich unter den // Zusatzinformationen erscheint. const checkTitleIndex = summary.findIndex(field => normalizeLabel(field.label) === 'check title'); const checkTitle = checkTitleIndex >= 0 ? summary.splice(checkTitleIndex, 1)[0] : undefined; return { statusField, badges, derivedCategories, mitre, summary, checkTitle, sourceRuleId, optionalFields, remainingFields, policyComparisonFields, policyReadiness }; } /** * Short technical token that can be pasted into Policy Analyzer. This is a * presentation aid only: evaluation and finding identity continue to use the * complete technical policy identity. */ export function policyAnalyzerSearchKeys(record: FindingRecord, title: string, toolId?: string): string[] { if (toolId !== 'group-policy') return []; const field = (label: string) => record.fields.find(item => normalizeLabel(item.label) === label)?.value.trim() ?? ''; const preference = field('policy type') === 'group-policy-preference'; const technicalTitle = title.split(/\s*\u00b7\s*/).at(-1)?.trim() ?? title.trim(); const titleParts = technicalTitle.split(/[\\/]/).map(part => part.trim()).filter(Boolean); const keys: string[] = []; const add = (value?: string) => { const trimmed = value?.trim(); if (trimmed && !keys.some(item => item.toLocaleLowerCase('en-US') === trimmed.toLocaleLowerCase('en-US'))) keys.push(trimmed); }; for (const item of record.fields) { if (!/^(?:configured value|baseline value)$/.test(normalizeLabel(item.label))) continue; if (preference) { try { const values = JSON.parse(item.value) as unknown; const visit = (value: unknown): void => { if (Array.isArray(value)) return value.forEach(visit); if (!value || typeof value !== 'object') return; const entry = value as Record; if (entry.kind === 'registry-preference' && typeof entry.key === 'string') add(entry.key.split(/[\\/]/).at(-1)); Object.values(entry).forEach(visit); }; visit(values); } catch { /* The unchanged technical title remains the safe fallback. */ } } add(policyValueName(item.value)); } if (!keys.length && preference && titleParts.length > 1) add(titleParts.at(-2)); if (preference || !keys.length) add(titleParts.at(-1)); return keys; } function MitreBadgeDisclosure({ fields, search }: { fields: RecordField[]; search: string }) { const [expanded, setExpanded] = useState(false); return
{expanded &&
{fields.map(field => {mitreBadgeLabel(field.label)}: {highlightText(field.value, search)})}
}
; } export function FindingReader({ record, title, review: storedReview, previousPolicyReviews, linkedSheets, resultReferences, search, position, recordCount, toolId, onPrevious, onNext, onReview, onRiskAcceptance, onResults, resultsOpen, resultColor, projectId = '', backendStatus = 'unavailable' }: Props) { const relatedSelection = useRelatedFindings()?.selection; const contextClusters = useContextClusterIndex(); const resultButtonStyle = resultsOpen && resultColor ? ({ '--result-color': resultColor } as CSSProperties) : undefined; const [openLinkedResultsFor, setOpenLinkedResultsFor] = useState(''); const [riskAcceptanceFor, setRiskAcceptanceFor] = useState(''); const linkedResultsOpen = openLinkedResultsFor === record.id; const review = { ...defaultReview(), ...storedReview }; const severityField = record.fields.find(field => /^(severity|criticality|priority)$/i.test(field.label) && field.value); const severity = normalizeSeverity(severityField?.value); const hasStructuredResults = linkedSheets.length > 0; const resolvedToolId = toolIdForRecord(record, toolId); const webSearch = findingWebSearch(record, resolvedToolId); const { statusField, badges, derivedCategories, mitre, summary, checkTitle, sourceRuleId, optionalFields, remainingFields, policyComparisonFields, policyReadiness } = selectFindingFields(record, title, hasStructuredResults, resolvedToolId); const comparisonOutcome = statusField?.value ? outcomeForStatusValue(resolvedToolId, statusField.value) : 'unknown'; const problem = resolvedToolId === 'group-policy' && !policyReadiness ? policyProblemSummary(record) : undefined; const firstRead = resolvedToolId !== 'group-policy' ? findingFirstRead(record, resolvedToolId, linkedSheets, comparisonOutcome) : undefined; const policyHeading = policyComparisonFields.map(field => policyValueName(field.value)).find(Boolean); // Nur echte Dateiverweise gehoeren in den Hinweis. Werkzeuge, die ihr // Ergebnis als Fliesstext oder Tabelle in dasselbe Feld schreiben, haetten // sonst ihren gesamten Inhalt ein zweites Mal als "Referenz" dort stehen. const fileReferences = [...new Set(resultReferences.filter(isFileReference))]; const hasResolvableResultReference = fileReferences.length > 0 || resultReferences.some(reference => /source-table:\d+/i.test(reference)); const updateText = (field: keyof Review) => (event: ChangeEvent) => onReview({ [field]: event.target.value }); const riskState = review.riskAcceptance ? riskAcceptanceState(review.riskAcceptance, record.findingOccurrenceId) : undefined; const regulatoryMapping = useRegulatoryMapping(projectId, regulatoryMappingResultId(record), backendStatus); return (
{riskAcceptanceFor === record.id && setRiskAcceptanceFor('')} />}
{sourceRuleId &&

Source Rule ID

{highlightText(sourceRuleId.value, search)}

} [cluster.id, numberedClusterLabel(cluster.id, cluster.label, contextClusters?.publication.clusters)]))} clusterSeverities={Object.fromEntries((contextClusters?.publication.clusters ?? []).map(cluster => [cluster.id, publishedClusterColour(cluster)]))}/>
{Boolean(previousPolicyReviews?.length) &&
Previous baseline-specific reviews ({previousPolicyReviews!.length})

These earlier notes remain attached to their original comparisons. Different decisions and risk acceptances are not automatically applied to this combined setting.

{previousPolicyReviews!.map(item =>

{item.baseline}

{Object.entries(item.review).filter(([key, value]) => value !== undefined && JSON.stringify(value) !== JSON.stringify(defaultReview()[key as keyof Review])).map(([key, value]) =>
{key.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, character => character.toUpperCase())}
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
)}
)}
}

{policyReadiness &&

Historical comparison

{policyReadiness.value}

} {problem && } {resolvedToolId === 'maester' && !firstRead?.rows.some(row => row.label === 'Result') &&

No result statement is available in this imported finding.

} {firstRead && } {resolvedToolId === 'group-policy' && Boolean(record.policySourceEvidence?.length) && } {policyComparisonFields.length > 0 &&
{policyComparisonFields.map(field => { const expected = normalizeLabel(field.label) === 'baseline value'; return

{policyReadiness ? (expected ? 'Saved baseline value' : 'Saved customer value') : expected ? 'Expected baseline value' : 'Configured customer value'}

{policyComparisonDisplayLines(field.value).map((line, index) => {index > 0 && '\n'}{line.emphasized ? : })}

; })}
} {resolvedToolId === 'group-policy' && Boolean(record.policyBaselineComparisons?.length) && } {/* Context layer (Phase 13E): for every finding after the technical evidence, only while switched on; it never changes the result above. */} {summary.map(field =>

{highlightText(field.label, search)}

{resolvedToolId === 'maester' && normalizeLabel(field.label) === 'id' ?

{highlightText(field.value, search)}

: resolvedToolId === 'maester' && normalizeLabel(field.label) === 'test details' ?
{highlightText(field.value, search)}
: resolvedToolId === 'group-policy' && /^(?:description|remediation|sources)$/.test(normalizeLabel(field.label)) ?

: }
)} {optionalFields.map(field =>
{highlightText(field.label, search)} {resolvedToolId === 'maester' && normalizeLabel(field.label) === 'test details' ?
{highlightText(field.value, search)}
: }
)}
{regulatoryMapping.open && void regulatoryMapping.start()} onDecision={(candidate, decision, comment) => void regulatoryMapping.decide(candidate, decision, comment)} />} {/* Work tracking is opt-in below the finding so the first screen states the problem. */}
Work tracking {[`Status: ${review.status}`, `Priority: ${review.priority}`, review.owner && `Owner: ${review.owner}`, review.due && `Due: ${review.due}`].filter(Boolean).join(' · ')}
{([['prerequisites', 'Prerequisites'], ['access', 'Required Access / Roles'], ['nextStep', 'Next Step'], ['evidence', 'Evidence / Acceptance Criteria'], ['comment', 'Comment']] as Array<[keyof Review, string]>).map(([field, label]) => (