import type { CanonicalOutcome, JsonValue } from '../domain/canonical-model'; import type { StoredGpoPolicyAnalysis } from '../domain/gpo-analysis-persistence'; import type { UnifiedResultProjection } from '../domain/unified-result'; import type { ReferenceRecognitionRun } from '../domain/reference-recognition'; import type { ScannerReportSection } from '../domain/environment-context'; import { cisBenchmarkSectionTitles } from '../profiles/cis-cat/cis-benchmark-section-titles'; import { assessmentContextCatalogErrors, primaryClusterOfCondition, type AssessmentContextCatalog, type ContextCondition, type ContextRelationship, type ContextGpoVerdictTarget, type ContextScannerRule, type ContextSemanticDefinition, type ContextSettingTarget, type ContextSource, type ContextText } from './context-catalog'; import { CONTEXT_RECOGNITION_TERMS, CONTEXT_RECOGNITION_VOCABULARY, type RecognitionTerm } from './context-recognition'; import { assignRemainingDeviations, type ContextAssignmentMethod } from './context-assignment'; import { contextClusterSeverities } from './context-cluster-severity'; /** * Context engine (Phase 13E): a derived, switchable second reading of stored ASSESSOR results. * It never produces a pass/fail verdict, a priority or an effective host state, and it never writes. * GPO backups contribute what they declare; scanners contribute what they observed. Both stay * separately attributed evidence of the same catalogued conditions. */ export const ASSESSMENT_CONTEXT_ENGINE = { id: 'assessment-context-engine', version: '1.11.0' } as const; export type DeclaredValue = | { state: 'configured'; value: number } | { state: 'deleted' } | { state: 'unknown'; reason: 'multiple-instructions' | 'conflicted-observation' | 'unsupported-value' }; /** What one GPO backup declares for one catalogued setting. Backup configuration only. */ export interface DeclaredFact { id: string; semanticId: string; backupInstanceId: string; gpoId?: string; linkStatus?: 'linked' | 'not-linked'; /** Same technical identity and scope as the Work row, so the layer can attach to it. */ identityKey: string; scope: 'computer' | 'user'; mechanism: ContextSettingTarget['mechanism']; value: DeclaredValue; observationIds: string[]; assertion: 'declared-in-gpo-backup'; } /** * What one scanner result observed for one catalogued condition: via its exact rule ID, for CIS-CAT via * the benchmark section of its own report, otherwise as a visible uncatalogued deviation. */ export interface ObservedFact { id: string; conditionId: string; tool: ContextScannerRule['tool']; sourceRuleId: string; ruleText: string; matchedBy: 'rule-id' | 'report-section' | 'uncatalogued-deviation'; /** Report section title that assigned the finding (`report-section` only). */ sectionTitle?: string; resultProjectionId: string; /** Same finding occurrence as the Work record, so the layer can attach to it. */ occurrenceId: string; outcome: CanonicalOutcome; observedAt?: string; assertion: 'observed-by-scanner'; } /** * ASSESSOR's authoritative, rolled-up baseline verdict for one GPO setting (user decision 2026-09-30). * Read as it is, never re-derived; used where no catalogued value semantic covers the setting. */ export interface EvaluatedFact { id: string; conditionId: string; identityKey: string; scope: 'computer' | 'user'; settingName: string; verdict: 'deviation' | 'passed'; matchedBy: 'setting' | 'uncatalogued-deviation' | 'assigned-by-recognition'; resultProjectionIds: string[]; assertion: 'evaluated-by-assessor'; } export type ConditionState = 'satisfied' | 'not-satisfied' | 'unknown'; export interface ContextConditionResult { conditionId: string; /** The GPO setting behind the condition; absent for conditions only scanners observe. */ semanticId?: string; label: ContextText; /** Aggregate over all evidence: satisfied as soon as one GPO declares or one scanner observes it. */ state: ConditionState; /** Declared and observed fact IDs; look them up in `facts` or `observations`. */ satisfiedBy: string[]; notSatisfiedBy: string[]; unknownBy: string[]; /** Different GPO backups declare contradicting values; GPO precedence is never assumed. */ conflictingDeclarations: boolean; /** A GPO declaration and a scanner observation point in opposite directions. */ declaredVersusObserved: boolean; /** Every satisfying piece of evidence is a declaration of a GPO backup without an enabled link. */ onlyUnlinkedSources: boolean; relationships: ContextRelationship[]; } export interface ContextClusterResult { clusterId: string; label: ContextText; description: ContextText; /** `present`: at least `minimumSatisfied` conditions are met; never a proof of an attack. */ state: 'present' | 'not-present' | 'undetermined'; minimumSatisfied: number; satisfied: number; notSatisfied: number; unknown: number; conditions: ContextConditionResult[]; sources: ContextSource[]; } /** Attach point for a GPO Work row: its technical identity and scope. */ export interface ContextSettingAttachment { identityKey: string; scope: 'computer' | 'user'; /** Catalogued value semantic; absent when only ASSESSOR's baseline verdict links the setting. */ semanticId?: string; settingName: string; factIds: string[]; evaluationIds: string[]; conditionIds: string[]; /** Exactly one primary cluster per deviation (user decision 2026-10-04); empty when no cluster applies. */ clusterIds: string[]; /** Further clusters the evidence also names; traceable, but never counted (user decision 2026-10-04). */ secondaryClusterIds?: string[]; /** Confidence of each cluster that is not catalogued (user decision 2026-10-01); absent means catalogued. */ clusterConfidence?: Record; } /** Attach point for a scanner Work record: its finding occurrence. */ export interface ContextFindingAttachment { occurrenceId: string; resultProjectionId: string; tool: string; sourceRuleId: string; observationIds: string[]; conditionIds: string[]; /** Exactly one primary cluster per finding (user decision 2026-10-04); empty when no cluster applies. */ clusterIds: string[]; /** Further clusters the evidence also names; traceable, but never counted (user decision 2026-10-04). */ secondaryClusterIds?: string[]; /** ATT&CK and D3FEND IDs the scanner itself declares for this finding (user decisions 2026-09-30). */ declaredReferenceIds: string[]; /** CIS benchmark sections containing this rule in its own scan report, top level first. */ cisControlAreaIds: string[]; /** Confidence of each cluster that is not catalogued (user decision 2026-10-01); absent means catalogued. */ clusterConfidence?: Record; } /** * Tier 1 (user decision 2026-10-01): a deviation whose scanner itself declares an active ATT&CK technique that the * catalogue's sourced relationships name for exactly one cluster. Counts like a catalogued rule for that cluster. */ export interface DeclaredTechniqueAssignment { id: string; occurrenceId: string; resultProjectionId: string; tool: string; sourceRuleId: string; ruleText: string; techniqueId: string; clusterId: string; outcome: CanonicalOutcome; assertion: 'declared-technique'; } /** * Tiers 2 to 5 (user decision 2026-10-01): a deviation no rule, section or declared technique assigns receives a cluster * from its rule text, description, technical anchors, the most similar assigned finding or its scanner category. * It counts for the cluster and always carries its confidence, method and evidence. */ export interface ContextRecognition { id: string; subject: 'scanner-finding' | 'gpo-setting'; occurrenceId?: string; identityKey?: string; scope?: 'computer' | 'user'; resultProjectionIds: string[]; tool: string; sourceRuleId?: string; text: string; clusterId: string; confidence: 'recognized' | 'estimated'; method: ContextAssignmentMethod; termId?: string; /** The words, neighbour text or category that led to the assignment. */ matchedText: string; assertion: 'automatically-recognized' | 'automatically-estimated'; } /** * Findings whose scanner declares the same ATT&CK tactic or technique, or the same D3FEND * countermeasure. Only references the source declares count; mentioned ones never do. States a * shared declared topic or countermeasure, never a common attack path or cause. */ export interface DeclaredReferenceGroup { framework: 'mitre-attack' | 'mitre-d3fend'; id: string; title?: string; kind?: 'technique' | 'tactic'; findings: Array<{ occurrenceId: string; resultProjectionId: string; tool: string; sourceRuleId?: string; outcome: CanonicalOutcome }>; } /** * One CIS benchmark section of one scan report, derived from the exact rule-number hierarchy * (9.3.6 lies in 9.3 and 9). Numbers are compared only inside the same scan run, because * different benchmarks reuse the same numbering. Same section, never a proven attack chain. */ export interface CisControlArea { id: string; scanRunId: string; sectionId: string; parentSectionId?: string; /** Section title as the scan report's section-score table lists it; absent when missing or ambiguous. */ title?: string; findings: number; fail: number; pass: number; other: number; /** Pass/Fail counts the scan report itself states for this section. */ reported?: { pass: number; fail: number }; } /** Remediation lever: a plain count, never a rank, effort or risk score. */ export interface ContextLever { semanticId: string; settingName: string; satisfiedConditions: number; clusters: number; backupInstanceIds: string[]; } export interface AssessmentContext { version: 1; engine: typeof ASSESSMENT_CONTEXT_ENGINE; catalog: AssessmentContextCatalog['catalog']; projectId: string; assessmentIds: string[]; scanRunIds?: string[]; generatedAt: string; repositoryRevision: number; facts: DeclaredFact[]; observations: ObservedFact[]; evaluations: EvaluatedFact[]; settings: ContextSettingAttachment[]; findings: ContextFindingAttachment[]; clusters: ContextClusterResult[]; declaredReferences: DeclaredReferenceGroup[]; cisControlAreas: CisControlArea[]; levers: ContextLever[]; /** Tier 1: deviations assigned through a technique their scanner declares. */ techniqueAssignments: DeclaredTechniqueAssignment[]; /** Tier 2: suggestions from the recognition vocabulary; kept apart from every catalogued statement. */ recognitions: ContextRecognition[]; recognitionVocabulary: typeof CONTEXT_RECOGNITION_VOCABULARY; /** Whether the backend run read everything Raw Data shows; absent when no deviation needed it. */ rawDataRead?: boolean; /** Gap list with vocabulary suggestions; added by the backend run (context-run.ts), read by the UI. */ gapReport?: import('./context-gaps').ContextGapReport; semantics: Array>; scannerRules: Array>; coverage: { observations: number; semanticallyMapped: number; notSemanticallyMapped: number; unresolvedIdentity: number; scannerFindings: number; scannerMapped: number; scannerNotInCatalog: number; /** Rule ID known, but the finding does not name the exact setting: not mapped (benchmark renumbering guard). */ scannerGuardRejected: number; /** GPO settings with an authoritative baseline verdict, and how many of them the context assigns. */ gpoEvaluatedSettings: number; gpoMappedByVerdict: number; gpoUncatalogued: number; /** CIS-CAT findings assigned through the section title of their own report. */ scannerMappedBySection: number; /** Deviations no rule or section assigns yet; they are kept in the uncatalogued cluster. */ scannerUncatalogued: number; /** Scanner findings with at least one ATT&CK ID their scanner declares. */ scannerWithDeclaredReferences: number; /** CIS-CAT findings placed in at least one benchmark section of their scan report. */ scannerWithCisControlArea: number; /** Tier 1: deviations assigned through a declared technique; they are no longer uncatalogued. */ scannerMappedByDeclaredTechnique: number; /** Tiers 2 to 5: deviations assigned as recognized or estimated, and how many stay without context. */ scannerRecognized: number; scannerEstimated: number; gpoRecognized: number; gpoEstimated: number; uncataloguedWithoutRecognition: number; }; } export interface AssessmentContextInput { projectId: string; assessmentIds: string[]; scanRunIds?: string[]; generatedAt: string; repositoryRevision: number; /** Current `policy-observation` results of the scope, as Work reads them. */ observations: UnifiedResultProjection[]; analyses: StoredGpoPolicyAnalysis[]; /** Current `policy-evaluation` results of the scope, as Work reads them (authoritative GPO verdicts). */ evaluations?: UnifiedResultProjection[]; /** Resolved technical targets by identity ID and scope (from the stored policy evaluations). */ identityTargets?: Array<{ id: string; scope: string; target: unknown }>; /** Current `scanner-finding` results of the scope, as Work reads them. */ scannerFindings?: UnifiedResultProjection[]; /** Stored reference recognition runs; only the latest run per scan run counts. */ referenceRuns?: ReferenceRecognitionRun[]; /** Stored report sections of the CIS-CAT scan runs in scope; only the section-score table is read. */ cisReportSections?: Array>; catalog: AssessmentContextCatalog; /** Tier 2 vocabulary; defaults to the released one. */ recognitionTerms?: RecognitionTerm[]; /** Template names of Microsoft baseline settings by technical identity (for example "MSFT Windows 11 - BitLocker"). */ baselineTemplates?: Array<{ identityKey: string; names: string[] }>; /** Every field Raw Data shows for each scanner record, by finding occurrence (user decision 2026-10-01). */ recordFields?: Array<{ occurrenceId: string; fields: Array<{ label: string; value: string }> }>; /** Every field Raw Data shows for each GPO setting record, by scope and technical identity. */ settingRecordFields?: Array<{ identityKey: string; scope: string; fields: Array<{ label: string; value: string }> }>; } const CIS_RULE_ID = /^\d+(?:\.\d+)+$/; const CIS_SECTION_ROW = /^((?:\d+\.)*\d+)\s+\S/; const WHOLE_NUMBER = /^\d+$/; /** Pass/Fail counts per section exactly as the section-score table states them; ambiguous rows are left out. */ function cisReportedCounts(sections: AssessmentContextInput['cisReportSections']): Map> { const result = new Map>(); for (const section of sections ?? []) { if (section.sectionKey !== 'cis-cat-section-scores') continue; const run = result.get(section.scanRunId) ?? new Map(); const conflicts = new Set(); for (const table of section.tables) { const column = (name: RegExp) => table.headers.findIndex(header => name.test(header.trim())); const [description, pass, fail] = [column(/^description$/i), column(/^pass$/i), column(/^fail$/i)]; if (description < 0 || pass < 0 || fail < 0) continue; for (const row of table.rows) { const id = CIS_SECTION_ROW.exec(String(row[description] ?? '').trim())?.[1]; const [passText, failText] = [String(row[pass] ?? '').trim(), String(row[fail] ?? '').trim()]; if (!id || !WHOLE_NUMBER.test(passText) || !WHOLE_NUMBER.test(failText)) continue; const counts = { pass: Number(passText), fail: Number(failText) }; const known = run.get(id); if (known && (known.pass !== counts.pass || known.fail !== counts.fail)) conflicts.add(id); else run.set(id, counts); } } conflicts.forEach(id => run.delete(id)); result.set(section.scanRunId, run); } return result; } const fold = (value: string) => value.normalize('NFC').replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase(); type ResolvedTarget = | { kind: 'security-template' | 'advanced-audit-policy'; section: string; key: string } | { kind: 'registry-key' | 'registry-value'; hive: string; key: string; valueName?: string }; function targetMatches(target: ContextSettingTarget, resolved: ResolvedTarget, scope: string): boolean { if (target.scope !== scope) return false; if (target.mechanism === 'security-template') { return resolved.kind === 'security-template' && fold(resolved.section) === fold(target.section) && fold(resolved.key) === fold(target.key); } const hive = target.scope === 'computer' ? 'HKEY_LOCAL_MACHINE' : 'HKEY_CURRENT_USER'; return resolved.kind === 'registry-value' && resolved.hive === hive && fold(resolved.key) === fold(target.key) && fold(resolved.valueName ?? '') === fold(target.valueName); } /** Registry verdict rules match their key and every subkey; the longer key is the more specific rule. */ function verdictTargetMatches(target: ContextGpoVerdictTarget, resolved: ResolvedTarget, scope: string): boolean { if (target.scope !== scope) return false; if (target.mechanism === 'advanced-audit-policy') return resolved.kind === 'advanced-audit-policy'; if (target.mechanism === 'security-template') { return resolved.kind === 'security-template' && fold(resolved.section) === fold(target.section) && fold(resolved.key) === fold(target.key); } const hive = target.scope === 'computer' ? 'HKEY_LOCAL_MACHINE' : 'HKEY_CURRENT_USER'; if ((resolved.kind !== 'registry-key' && resolved.kind !== 'registry-value') || resolved.hive !== hive) return false; const key = fold(resolved.key); const ruleKey = fold(target.key); if (key !== ruleKey && !key.startsWith(`${ruleKey}\\`)) return false; return target.valueName === undefined || fold(resolved.valueName ?? '') === fold(target.valueName); } const verdictSpecificity = (target: ContextGpoVerdictTarget) => target.mechanism === 'registry-key' ? target.key.length + (target.valueName ? 1000 : 0) : target.mechanism === 'security-template' ? 2000 : 0; function record(value: JsonValue): Record | undefined { return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined; } /** Decodes only an exact integer instruction; everything else stays explicitly unknown. */ export function declaredValue(observation: UnifiedResultProjection): DeclaredValue { if (observation.policyState === 'conflicted') return { state: 'unknown', reason: 'conflicted-observation' }; const values = observation.policyValues ?? []; if (values.length !== 1) return { state: 'unknown', reason: 'multiple-instructions' }; const value = record(values[0]); if (value?.kind === 'registry' && value.typeCode === 4 && typeof value.rawData === 'string' && /^\d{1,10}$/.test(value.rawData.trim())) { return { state: 'configured', value: Number(value.rawData.trim()) }; } // Account policies ([System Access]) carry the instruction as a plain scalar; only an exact integer counts. if (value?.kind === 'scalar' && typeof value.raw === 'string' && /^\d{1,10}$/.test(value.raw.trim())) { return { state: 'configured', value: Number(value.raw.trim()) }; } if (value?.kind === 'registry-policy') { const data = record(value.data ?? null); if (value.operation === 'set-value' && value.typeCode === 4 && data?.kind === 'uint32' && typeof data.value === 'number') { return { state: 'configured', value: data.value }; } if (value.operation === 'delete-value') return { state: 'deleted' }; } return { state: 'unknown', reason: 'unsupported-value' }; } function declaredState(condition: ContextCondition, value: DeclaredValue): ConditionState { if (value.state !== 'configured') return 'unknown'; if (condition.satisfiedValues.includes(value.value)) return 'satisfied'; if (condition.notSatisfiedValues.includes(value.value)) return 'not-satisfied'; return 'unknown'; } /** * `fail-means-met`: every failing or deviating native result (`fail`, `warning`) is a finding and meets the * condition (user decision 2026-09-30); a pass refutes it; everything else stays unknown. */ const isDeviation = (outcome: CanonicalOutcome) => outcome === 'fail' || outcome === 'warning'; const clustersOfConditionId = (catalog: AssessmentContextCatalog, conditionId: string) => catalog.clusters.filter(cluster => cluster.conditionIds.includes(conditionId)).map(cluster => cluster.id); /** Subjects whose strongest automatic assignment has this confidence (recognized wins over estimated). */ function countSubjects(recognitions: ContextRecognition[], subject: ContextRecognition['subject'], confidence: ContextRecognition['confidence']) { const best = new Map(); for (const item of recognitions.filter(entry => entry.subject === subject)) { const key = item.occurrenceId ?? `${item.scope}\u0000${item.identityKey}`; if (best.get(key) !== 'recognized') best.set(key, item.confidence); } return [...best.values()].filter(value => value === confidence).length; } function observedState(outcome: CanonicalOutcome): ConditionState { if (isDeviation(outcome)) return 'satisfied'; if (outcome === 'pass') return 'not-satisfied'; return 'unknown'; } export function buildAssessmentContext(input: AssessmentContextInput): AssessmentContext { const errors = assessmentContextCatalogErrors(input.catalog); if (errors.length) throw new Error(`Context catalog is invalid: ${errors.join(' ')}`); // The unified result already carries the identity ASSESSOR selected; only that resolution counts. const resolutions = new Map(); for (const analysis of input.analyses) { for (const item of analysis.resolutions) { if (item.resolution.status === 'resolved') resolutions.set(`${item.observationId}\u0000${item.resolution.identity.id}`, item.resolution.identity.target as ResolvedTarget); } } const facts: DeclaredFact[] = []; let unresolvedIdentity = 0; let notMapped = 0; for (const observation of input.observations) { const scope = observation.policyScope; const target = observation.policyIdentityStatus === 'resolved' && observation.policyIdentityKey ? resolutions.get(`${observation.sourceResultId}\u0000${observation.policyIdentityKey}`) : undefined; if (!target || (scope !== 'computer' && scope !== 'user') || !observation.policyBackupInstanceId) { unresolvedIdentity += 1; continue; } const semantic = input.catalog.semantics.find(item => item.targets.some(candidate => targetMatches(candidate, target, scope))); if (!semantic) { notMapped += 1; continue; } const mechanism = semantic.targets.find(candidate => targetMatches(candidate, target, scope))!.mechanism; facts.push({ id: `context-fact:${observation.policyBackupInstanceId}:${semantic.id}:${observation.sourceResultId}`, semanticId: semantic.id, backupInstanceId: observation.policyBackupInstanceId, gpoId: observation.policyGpoId, linkStatus: observation.policyBackupLinkStatus, identityKey: observation.policyIdentityKey!, scope, mechanism, value: declaredValue(observation), observationIds: [observation.sourceResultId], assertion: 'declared-in-gpo-backup' }); } facts.sort((left, right) => left.id.localeCompare(right.id)); // GPO verdicts: every setting whose rolled-up baseline verdict ASSESSOR reports as a deviation enters the context. const identityKeyOf = (scope: string, identityKey: string) => `${scope}\u0000${identityKey}`; const targetByIdentity = new Map(); for (const observation of input.observations) { const target = observation.policyIdentityStatus === 'resolved' && observation.policyIdentityKey ? resolutions.get(`${observation.sourceResultId}\u0000${observation.policyIdentityKey}`) : undefined; if (target && observation.policyScope) targetByIdentity.set(identityKeyOf(observation.policyScope, observation.policyIdentityKey!), target); } for (const item of input.identityTargets ?? []) { const key = identityKeyOf(item.scope, item.id); if (!targetByIdentity.has(key) && item.target && typeof item.target === 'object') targetByIdentity.set(key, item.target as ResolvedTarget); } const declaredIdentities = new Set(facts.map(fact => identityKeyOf(fact.scope, fact.identityKey))); const verdicts = new Map(); for (const evaluation of input.evaluations ?? []) { const scope = evaluation.policyScope; if ((scope !== 'computer' && scope !== 'user') || !evaluation.policyIdentityKey) continue; const key = identityKeyOf(scope, evaluation.policyIdentityKey); const entry = verdicts.get(key) ?? { scope, identityKey: evaluation.policyIdentityKey, settingName: evaluation.title, deviation: false, passed: false, ids: [] }; entry.deviation ||= evaluation.isDeviation; entry.passed ||= evaluation.outcomeNormalized === 'pass'; entry.ids.push(evaluation.resultProjectionId); verdicts.set(key, entry); } const verdictRuleFor = (target: ResolvedTarget, scope: string) => input.catalog.gpoVerdictRules .filter(rule => verdictTargetMatches(rule.target, target, scope)) .sort((left, right) => verdictSpecificity(right.target) - verdictSpecificity(left.target))[0]; const evaluations: EvaluatedFact[] = []; let gpoMappedByVerdict = 0; let gpoUncatalogued = 0; for (const [key, verdict] of verdicts) { if (declaredIdentities.has(key)) continue; const state = verdict.deviation ? 'deviation' : verdict.passed ? 'passed' : undefined; if (!state) continue; const target = targetByIdentity.get(key); const rule = target ? verdictRuleFor(target, verdict.scope) : undefined; const conditionId = rule?.conditionId ?? (state === 'deviation' ? input.catalog.uncataloguedDeviationConditions.gpo : undefined); if (!conditionId) continue; if (rule) gpoMappedByVerdict += 1; else gpoUncatalogued += 1; evaluations.push({ id: `context-evaluation:${verdict.scope}:${verdict.identityKey}`, conditionId, identityKey: verdict.identityKey, scope: verdict.scope, settingName: verdict.settingName, verdict: state, matchedBy: rule ? 'setting' : 'uncatalogued-deviation', resultProjectionIds: verdict.ids.sort(), assertion: 'evaluated-by-assessor' }); } evaluations.sort((left, right) => left.id.localeCompare(right.id)); // Scanner observations: exact tool and rule ID only; the title only guards renumbered benchmarks. const rulesByKey = new Map(input.catalog.scannerRules.map(rule => [`${rule.tool}\u0000${rule.ruleId}`, rule])); const foldTitle = (value: string) => value.trim().toLowerCase().replace(/\s+/g, ' '); const sectionRules = new Map(input.catalog.cisSectionRules.map(rule => [foldTitle(rule.sectionTitle), rule])); const sectionTitles = cisBenchmarkSectionTitles(input.cisReportSections); const sectionOf = (finding: UnifiedResultProjection) => { const ruleId = finding.sourceRuleId?.trim() ?? ''; if (finding.sourceTool !== 'cis-cat' || !finding.scanRunId || !CIS_RULE_ID.test(ruleId)) return undefined; const segments = ruleId.split('.'); for (let length = segments.length - 1; length > 0; length -= 1) { const title = sectionTitles.get(finding.scanRunId)?.get(segments.slice(0, length).join('.')); const rule = title ? sectionRules.get(foldTitle(title)) : undefined; if (rule) return { rule, title: title! }; } return undefined; }; // Latest reference recognition run per scan run; read by tier 1 and by the declared reference groups below. const latestRuns = new Map(); for (const run of input.referenceRuns ?? []) { const previous = latestRuns.get(run.scanRunId); if (!previous || run.createdAt > previous.createdAt || (run.createdAt === previous.createdAt && run.id > previous.id)) latestRuns.set(run.scanRunId, run); } // Tier 1: active ATT&CK techniques the scanner itself declares, by finding occurrence (exact IDs, never tactics). const declaredTechniques = new Map(); for (const run of latestRuns.values()) { for (const result of run.results) { const ids = result.references .filter(reference => reference.framework === 'mitre-attack' && reference.evidence === 'declared' && reference.status === 'active' && reference.kind === 'technique' && /^T\d{4}(?:\.\d{3})?$/.test(reference.id)) .map(reference => reference.id); if (ids.length) declaredTechniques.set(result.occurrenceId, [...new Set([...(declaredTechniques.get(result.occurrenceId) ?? []), ...ids])]); } } // A technique assigns only when the catalogue's sourced relationships name it for exactly one cluster, and for that // cluster in at least two relationships; a single incidental mention is not enough to stand for the attack path. const clustersByTechnique = new Map>(); for (const relationship of input.catalog.relationships) { for (const techniqueId of relationship.techniqueIds) { for (const cluster of input.catalog.clusters.filter(item => item.conditionIds.includes(relationship.conditionId))) { if (Object.values(input.catalog.uncataloguedDeviationConditions).some(id => cluster.conditionIds.includes(id))) continue; const counts = clustersByTechnique.get(techniqueId) ?? new Map(); counts.set(cluster.id, (counts.get(cluster.id) ?? 0) + 1); clustersByTechnique.set(techniqueId, counts); } } } const techniqueCluster = (techniqueId: string) => { const counts = clustersByTechnique.get(techniqueId); const [only] = counts?.size === 1 ? [...counts] : []; return only && only[1] >= 2 ? only[0] : undefined; }; const techniqueClusters = (occurrenceId: string) => (declaredTechniques.get(occurrenceId) ?? []) .flatMap(techniqueId => { const clusterId = techniqueCluster(techniqueId); return clusterId ? [{ techniqueId, clusterId }] : []; }); const techniqueAssignments: DeclaredTechniqueAssignment[] = []; const recognitionTerms = input.recognitionTerms ?? CONTEXT_RECOGNITION_TERMS; const recognitions: ContextRecognition[] = []; const observations: ObservedFact[] = []; let scannerNotInCatalog = 0; let scannerGuardRejected = 0; let scannerMappedBySection = 0; let scannerUncatalogued = 0; const pendingFindings: Array<{ finding: UnifiedResultProjection; tool: string; uncatalogued?: string; observe: (conditionId: string, tool: ContextScannerRule['tool'], sourceRuleId: string, ruleText: string, matchedBy: ObservedFact['matchedBy']) => void }> = []; // A fixed processing order makes every assignment independent of the order the repository returns findings in. const scannerFindings = [...(input.scannerFindings ?? [])].sort((left, right) => left.resultProjectionId.localeCompare(right.resultProjectionId)); for (const finding of scannerFindings) { const observe = (conditionId: string, tool: ContextScannerRule['tool'], sourceRuleId: string, ruleText: string, matchedBy: ObservedFact['matchedBy'], sectionTitle?: string) => observations.push({ id: `context-observation:${finding.resultProjectionId}:${conditionId}`, conditionId, tool, sourceRuleId, ruleText, matchedBy, ...(sectionTitle ? { sectionTitle } : {}), resultProjectionId: finding.resultProjectionId, occurrenceId: finding.sourceResultId, outcome: finding.outcomeNormalized, observedAt: finding.sourceObservedAt, assertion: 'observed-by-scanner' }); const rule = rulesByKey.get(`${finding.sourceTool ?? ''}\u0000${(finding.sourceRuleId ?? '').trim()}`); if (rule && (!rule.requiredTitleFragment || (finding.title ?? '').includes(rule.requiredTitleFragment))) { observe(rule.conditionId, rule.tool, rule.ruleId, rule.ruleText, 'rule-id'); continue; } if (rule) scannerGuardRejected += 1; const section = sectionOf(finding); if (section) { scannerMappedBySection += 1; observe(section.rule.conditionId, 'cis-cat', finding.sourceRuleId!.trim(), finding.title ?? '', 'report-section', section.title); continue; } if (isDeviation(finding.outcomeNormalized)) { const assigned = techniqueClusters(finding.sourceResultId); if (assigned.length) { for (const item of assigned) { techniqueAssignments.push({ id: `context-technique:${finding.resultProjectionId}:${item.techniqueId}:${item.clusterId}`, occurrenceId: finding.sourceResultId, resultProjectionId: finding.resultProjectionId, tool: finding.sourceTool ?? 'unknown', sourceRuleId: (finding.sourceRuleId ?? '').trim(), ruleText: finding.title ?? '', techniqueId: item.techniqueId, clusterId: item.clusterId, outcome: finding.outcomeNormalized, assertion: 'declared-technique' }); } continue; } } const tool = finding.sourceTool as ContextScannerRule['tool'] | undefined; if (isDeviation(finding.outcomeNormalized) && finding.sourceTool) { // Tiers 2 to 5 run after every finding of the scope is known (user decision 2026-10-01). pendingFindings.push({ finding, tool: finding.sourceTool, uncatalogued: tool ? input.catalog.uncataloguedDeviationConditions[tool] : undefined, observe }); continue; } scannerNotInCatalog += 1; } // Tiers 2 to 5 (user decision 2026-10-01): every remaining deviation receives a cluster in a fixed, deterministic order. const assigned = assignRemainingDeviations({ catalog: input.catalog, recognitionTerms, resolvedFindings: (input.scannerFindings ?? []).flatMap(finding => { const clusterIds = [...new Set([ ...observations.filter(item => item.occurrenceId === finding.sourceResultId).flatMap(item => clustersOfConditionId(input.catalog, item.conditionId)), ...techniqueAssignments.filter(item => item.occurrenceId === finding.sourceResultId).map(item => item.clusterId) ])]; return clusterIds.length ? [{ finding, clusterIds }] : []; }), pendingFindings: pendingFindings.map(item => item.finding), recordFields: new Map((input.recordFields ?? []).map(record => [record.occurrenceId, record.fields])), settingRecordFields: new Map((input.settingRecordFields ?? []).map(record => [`${record.scope}\u0000${record.identityKey}`, record.fields])), pendingSettings: evaluations.filter(item => item.matchedBy === 'uncatalogued-deviation').map(item => { // Registry path and baseline template names carry the topic when the setting name alone does not. const target = targetByIdentity.get(identityKeyOf(item.scope, item.identityKey)) as { key?: string; valueName?: string } | undefined; const templates = [...new Set((input.baselineTemplates ?? []).filter(entry => entry.identityKey === item.identityKey).flatMap(entry => entry.names))].sort(); return { ...item, technicalText: [target?.key && [target.key, target.valueName].filter(Boolean).join('\\'), ...templates].filter(Boolean).join('\n') }; }) }); for (const item of pendingFindings) { const matches = assigned.findings.get(item.finding.sourceResultId) ?? []; for (const match of matches) { recognitions.push({ id: `context-recognition:${item.finding.resultProjectionId}:${match.clusterId}`, subject: 'scanner-finding', occurrenceId: item.finding.sourceResultId, resultProjectionIds: [item.finding.resultProjectionId], tool: item.tool, sourceRuleId: (item.finding.sourceRuleId ?? '').trim(), text: item.finding.title ?? '', clusterId: match.clusterId, confidence: match.confidence, method: match.method, termId: match.termId, matchedText: match.evidence, assertion: match.confidence === 'estimated' ? 'automatically-estimated' : 'automatically-recognized' }); } if (matches.length) continue; if (item.uncatalogued) { scannerUncatalogued += 1; item.observe(item.uncatalogued, item.tool as ContextScannerRule['tool'], (item.finding.sourceRuleId ?? '').trim(), item.finding.title ?? '', 'uncatalogued-deviation'); } else { scannerNotInCatalog += 1; } } observations.sort((left, right) => left.id.localeCompare(right.id)); for (const evaluation of evaluations.filter(item => item.matchedBy === 'uncatalogued-deviation')) { for (const match of assigned.settings.get(`${evaluation.scope}\u0000${evaluation.identityKey}`) ?? []) { recognitions.push({ id: `context-recognition:${evaluation.scope}:${evaluation.identityKey}:${match.clusterId}`, subject: 'gpo-setting', identityKey: evaluation.identityKey, scope: evaluation.scope, resultProjectionIds: evaluation.resultProjectionIds, tool: 'gpo', text: evaluation.settingName, clusterId: match.clusterId, confidence: match.confidence, method: match.method, termId: match.termId, matchedText: match.evidence, assertion: match.confidence === 'estimated' ? 'automatically-estimated' : 'automatically-recognized' }); } } // An assigned GPO setting leaves the uncatalogued cluster; it keeps its authoritative verdict record. const assignedSettingKeys = new Set(recognitions.filter(item => item.subject === 'gpo-setting').map(item => `${item.scope}\u0000${item.identityKey}`)); for (const evaluation of evaluations) { if (evaluation.matchedBy === 'uncatalogued-deviation' && assignedSettingKeys.has(`${evaluation.scope}\u0000${evaluation.identityKey}`)) { evaluation.matchedBy = 'assigned-by-recognition'; gpoUncatalogued -= 1; } } recognitions.sort((left, right) => left.id.localeCompare(right.id)); techniqueAssignments.sort((left, right) => left.id.localeCompare(right.id)); const semanticById = new Map(input.catalog.semantics.map(semantic => [semantic.id, semantic])); const conditionResults = new Map(); const conditionSources: Array<{ semantic?: ContextSemanticDefinition; condition: { id: string; label: ContextText } & Partial }> = [ ...input.catalog.semantics.flatMap(semantic => semantic.conditions.map(condition => ({ semantic, condition }))), ...input.catalog.standaloneConditions.map(condition => ({ semantic: undefined, condition })) ]; for (const { semantic, condition } of conditionSources) { const declaring = semantic ? facts.filter(fact => fact.semanticId === semantic.id) : []; const observing = observations.filter(item => item.conditionId === condition.id); const evaluating = evaluations.filter(item => item.conditionId === condition.id && item.matchedBy !== 'assigned-by-recognition'); const evidence = [ ...declaring.map(fact => ({ id: fact.id, state: declaredState(condition as ContextCondition, fact.value), declared: fact })), ...observing.map(item => ({ id: item.id, state: observedState(item.outcome), declared: undefined })), ...evaluating.map(item => ({ id: item.id, state: (item.verdict === 'deviation' ? 'satisfied' : 'not-satisfied') as ConditionState, declared: undefined })) ]; const ids = (state: ConditionState) => evidence.filter(item => item.state === state).map(item => item.id); const satisfied = evidence.filter(item => item.state === 'satisfied'); const notSatisfied = evidence.filter(item => item.state === 'not-satisfied'); const declaredStates = new Set(evidence.filter(item => item.declared && item.state !== 'unknown').map(item => item.state)); const observedStates = new Set(evidence.filter(item => !item.declared && item.state !== 'unknown').map(item => item.state)); conditionResults.set(condition.id, { conditionId: condition.id, ...(semantic ? { semanticId: semantic.id } : {}), label: condition.label, state: satisfied.length ? 'satisfied' : notSatisfied.length && notSatisfied.length === evidence.length ? 'not-satisfied' : 'unknown', satisfiedBy: ids('satisfied'), notSatisfiedBy: ids('not-satisfied'), unknownBy: ids('unknown'), conflictingDeclarations: declaredStates.size > 1, declaredVersusObserved: [...declaredStates].some(state => observedStates.size && !observedStates.has(state)) || [...observedStates].some(state => declaredStates.size && !declaredStates.has(state)), onlyUnlinkedSources: satisfied.length > 0 && satisfied.every(item => item.declared?.linkStatus === 'not-linked'), relationships: input.catalog.relationships.filter(item => item.conditionId === condition.id) }); } const clusters: ContextClusterResult[] = input.catalog.clusters.map(cluster => { const conditions = cluster.conditionIds.map(id => conditionResults.get(id)!); const count = (state: ConditionState) => conditions.filter(item => item.state === state).length; const satisfied = count('satisfied'); const unknown = count('unknown'); return { clusterId: cluster.id, label: cluster.label, description: cluster.description, state: satisfied >= cluster.minimumSatisfied ? 'present' : satisfied + unknown < cluster.minimumSatisfied ? 'not-present' : 'undetermined', minimumSatisfied: cluster.minimumSatisfied, satisfied, notSatisfied: count('not-satisfied'), unknown, conditions, sources: cluster.sources }; }); const clustersOfCondition = (conditionId: string) => input.catalog.clusters.filter(cluster => cluster.conditionIds.includes(conditionId)).map(cluster => cluster.id); const settings = new Map(); for (const fact of facts) { const key = `${fact.scope}\u0000${fact.identityKey}`; const semantic = semanticById.get(fact.semanticId)!; const conditionIds = semantic.conditions.map(condition => condition.id); const entry = settings.get(key) ?? { identityKey: fact.identityKey, scope: fact.scope, semanticId: semantic.id, settingName: semantic.settingName, factIds: [], evaluationIds: [], conditionIds, clusterIds: [...new Set(conditionIds.flatMap(clustersOfCondition))] }; entry.factIds.push(fact.id); settings.set(key, entry); } for (const evaluation of evaluations) { const key = `${evaluation.scope}\u0000${evaluation.identityKey}`; if (evaluation.matchedBy === 'assigned-by-recognition') { const assignedEntry = settings.get(key) ?? { identityKey: evaluation.identityKey, scope: evaluation.scope, settingName: evaluation.settingName, factIds: [], evaluationIds: [], conditionIds: [], clusterIds: [] }; assignedEntry.evaluationIds.push(evaluation.id); settings.set(key, assignedEntry); continue; } const entry = settings.get(key) ?? { identityKey: evaluation.identityKey, scope: evaluation.scope, settingName: evaluation.settingName, factIds: [], evaluationIds: [], conditionIds: [], clusterIds: [] }; entry.evaluationIds.push(evaluation.id); if (!entry.conditionIds.includes(evaluation.conditionId)) entry.conditionIds.push(evaluation.conditionId); entry.clusterIds = [...new Set([...entry.clusterIds, ...clustersOfCondition(evaluation.conditionId)])]; settings.set(key, entry); } const findings = new Map(); for (const item of observations) { const entry = findings.get(item.occurrenceId) ?? { occurrenceId: item.occurrenceId, resultProjectionId: item.resultProjectionId, tool: item.tool, sourceRuleId: item.sourceRuleId, observationIds: [], conditionIds: [], clusterIds: [], declaredReferenceIds: [], cisControlAreaIds: [] }; entry.observationIds.push(item.id); if (!entry.conditionIds.includes(item.conditionId)) entry.conditionIds.push(item.conditionId); entry.clusterIds = [...new Set([...entry.clusterIds, ...clustersOfCondition(item.conditionId)])]; findings.set(item.occurrenceId, entry); } // Tier 1 counts like a catalogued rule: the cluster joins the finding's clusters. for (const item of techniqueAssignments) { const entry = findings.get(item.occurrenceId) ?? { occurrenceId: item.occurrenceId, resultProjectionId: item.resultProjectionId, tool: item.tool, sourceRuleId: item.sourceRuleId, observationIds: [], conditionIds: [], clusterIds: [], declaredReferenceIds: [], cisControlAreaIds: [] }; if (!entry.clusterIds.includes(item.clusterId)) entry.clusterIds.push(item.clusterId); findings.set(item.occurrenceId, entry); } // Tiers 2 to 5 count for their clusters and carry their confidence. for (const item of recognitions) { const entry = item.subject === 'scanner-finding' ? findings.get(item.occurrenceId!) ?? findings.set(item.occurrenceId!, { occurrenceId: item.occurrenceId!, resultProjectionId: item.resultProjectionIds[0], tool: item.tool, sourceRuleId: item.sourceRuleId ?? '', observationIds: [], conditionIds: [], clusterIds: [], declaredReferenceIds: [], cisControlAreaIds: [] }).get(item.occurrenceId!)! : settings.get(`${item.scope}\u0000${item.identityKey}`); if (!entry) continue; if (!entry.clusterIds.includes(item.clusterId)) entry.clusterIds.push(item.clusterId); entry.clusterConfidence = { ...(entry.clusterConfidence ?? {}), [item.clusterId]: item.confidence }; } // One finding, one cluster (user decision 2026-10-04): the strongest evidence decides, the rest stays traceable. const support = new Map>(); const supports = (key: string, clusterIds: string[]) => { const counts = support.get(key) ?? new Map(); for (const clusterId of clusterIds) counts.set(clusterId, (counts.get(clusterId) ?? 0) + 1); support.set(key, counts); }; // A catalogued condition several clusters list supports only its declared primary cluster. const primaryOf = (conditionId: string) => { const id = primaryClusterOfCondition(input.catalog, conditionId); return id ? [id] : []; }; for (const item of observations) supports(`finding:${item.occurrenceId}`, primaryOf(item.conditionId)); for (const item of techniqueAssignments) supports(`finding:${item.occurrenceId}`, [item.clusterId]); for (const fact of facts) supports(`setting:${fact.scope}\u0000${fact.identityKey}`, semanticById.get(fact.semanticId)!.conditions.flatMap(condition => primaryOf(condition.id))); for (const item of evaluations.filter(evaluation => evaluation.matchedBy !== 'assigned-by-recognition')) supports(`setting:${item.scope}\u0000${item.identityKey}`, primaryOf(item.conditionId)); for (const item of recognitions) supports(item.subject === 'scanner-finding' ? `finding:${item.occurrenceId}` : `setting:${item.scope}\u0000${item.identityKey}`, [item.clusterId]); const confidenceRank = { catalogued: 0, recognized: 1, estimated: 2 } as const; const severityRank = new Map(contextClusterSeverities.map(item => [item.clusterId, ({ critical: 0, high: 1, medium: 2, undetermined: 3 } as const)[item.severity]])); const choosePrimary = (key: string, entry: ContextFindingAttachment | ContextSettingAttachment) => { if (entry.clusterIds.length < 2) return; const counts = support.get(key); const ranked = [...entry.clusterIds].sort((left, right) => confidenceRank[entry.clusterConfidence?.[left] ?? 'catalogued'] - confidenceRank[entry.clusterConfidence?.[right] ?? 'catalogued'] || (counts?.get(right) ?? 0) - (counts?.get(left) ?? 0) || (severityRank.get(left) ?? 3) - (severityRank.get(right) ?? 3) || left.localeCompare(right)); entry.clusterIds = [ranked[0]]; entry.secondaryClusterIds = ranked.slice(1); if (entry.clusterConfidence) { const confidence = entry.clusterConfidence[ranked[0]]; if (confidence) entry.clusterConfidence = { [ranked[0]]: confidence }; else delete entry.clusterConfidence; } }; for (const entry of findings.values()) choosePrimary(`finding:${entry.occurrenceId}`, entry); for (const entry of settings.values()) choosePrimary(`setting:${entry.scope}\u0000${entry.identityKey}`, entry); // Declared ATT&CK and D3FEND references: latest run per scan run, only findings of this scope, only declared and active IDs. const scopedFindings = new Map((input.scannerFindings ?? []).map(finding => [finding.sourceResultId, finding])); const groups = new Map(); for (const run of latestRuns.values()) { for (const result of run.results) { const finding = scopedFindings.get(result.occurrenceId); if (!finding) continue; const declared = [...new Map(result.references .filter(reference => (reference.framework === 'mitre-attack' || reference.framework === 'mitre-d3fend') && reference.evidence === 'declared' && reference.status === 'active') .map(reference => [`${reference.framework}\u0000${reference.id}`, reference])).values()]; for (const reference of declared) { const groupKey = `${reference.framework}\u0000${reference.id}`; const group = groups.get(groupKey) ?? { framework: reference.framework, id: reference.id, title: reference.title, kind: reference.kind, findings: [] }; if (!group.findings.some(item => item.occurrenceId === result.occurrenceId)) { group.findings.push({ occurrenceId: result.occurrenceId, resultProjectionId: finding.resultProjectionId, tool: finding.sourceTool ?? 'unknown', sourceRuleId: finding.sourceRuleId, outcome: finding.outcomeNormalized }); } groups.set(groupKey, group); const entry = findings.get(result.occurrenceId) ?? { occurrenceId: result.occurrenceId, resultProjectionId: finding.resultProjectionId, tool: finding.sourceTool ?? 'unknown', sourceRuleId: finding.sourceRuleId ?? '', observationIds: [], conditionIds: [], clusterIds: [], declaredReferenceIds: [], cisControlAreaIds: [] }; if (!entry.declaredReferenceIds.includes(reference.id)) entry.declaredReferenceIds.push(reference.id); findings.set(result.occurrenceId, entry); } } } const declaredReferences = [...groups.values()] .map(group => ({ ...group, findings: group.findings.sort((left, right) => left.occurrenceId.localeCompare(right.occurrenceId)) })) .sort((left, right) => right.findings.length - left.findings.length || left.id.localeCompare(right.id)); // CIS control areas: the exact rule-number hierarchy inside each scan run; titles and counts from that scan report. const cisTitles = sectionTitles; const cisReported = cisReportedCounts(input.cisReportSections); const areas = new Map(); for (const finding of input.scannerFindings ?? []) { const ruleId = finding.sourceRuleId?.trim() ?? ''; if (finding.sourceTool !== 'cis-cat' || !finding.scanRunId || !CIS_RULE_ID.test(ruleId)) continue; const scanRunId = finding.scanRunId; const segments = ruleId.split('.'); const areaIds = segments.slice(0, -1).map((_segment, index) => { const sectionId = segments.slice(0, index + 1).join('.'); const id = `cis:${scanRunId}:${sectionId}`; const title = cisTitles.get(scanRunId)?.get(sectionId); const reported = cisReported.get(scanRunId)?.get(sectionId); const area = areas.get(id) ?? { id, scanRunId, sectionId, ...(index > 0 ? { parentSectionId: segments.slice(0, index).join('.') } : {}), ...(title ? { title } : {}), findings: 0, fail: 0, pass: 0, other: 0, ...(reported ? { reported } : {}) }; area.findings += 1; if (finding.outcomeNormalized === 'fail') area.fail += 1; else if (finding.outcomeNormalized === 'pass') area.pass += 1; else area.other += 1; areas.set(id, area); return id; }); const entry = findings.get(finding.sourceResultId) ?? { occurrenceId: finding.sourceResultId, resultProjectionId: finding.resultProjectionId, tool: finding.sourceTool, sourceRuleId: ruleId, observationIds: [], conditionIds: [], clusterIds: [], declaredReferenceIds: [], cisControlAreaIds: [] }; entry.cisControlAreaIds = [...new Set([...entry.cisControlAreaIds, ...areaIds])]; findings.set(finding.sourceResultId, entry); } const cisControlAreas = [...areas.values()].sort((left, right) => left.scanRunId.localeCompare(right.scanRunId) || left.sectionId.localeCompare(right.sectionId, undefined, { numeric: true })); const levers: ContextLever[] = input.catalog.semantics.flatMap(semantic => { const satisfiedConditions = semantic.conditions.filter(condition => conditionResults.get(condition.id)?.state === 'satisfied'); if (!satisfiedConditions.length) return []; const satisfying = new Set(satisfiedConditions.flatMap(condition => conditionResults.get(condition.id)!.satisfiedBy)); return [{ semanticId: semantic.id, settingName: semantic.settingName, satisfiedConditions: satisfiedConditions.length, clusters: new Set(satisfiedConditions.flatMap(condition => clustersOfCondition(condition.id))).size, backupInstanceIds: [...new Set(facts.filter(fact => satisfying.has(fact.id)).map(fact => fact.backupInstanceId))].sort() }]; }); return { version: 1, engine: ASSESSMENT_CONTEXT_ENGINE, catalog: input.catalog.catalog, projectId: input.projectId, assessmentIds: input.assessmentIds, ...(input.scanRunIds?.length ? { scanRunIds: input.scanRunIds } : {}), generatedAt: input.generatedAt, repositoryRevision: input.repositoryRevision, facts, observations, evaluations, settings: [...settings.values()], findings: [...findings.values()], clusters, declaredReferences, cisControlAreas, levers, techniqueAssignments, recognitions, recognitionVocabulary: CONTEXT_RECOGNITION_VOCABULARY, semantics: input.catalog.semantics.map(({ id, label, settingName, valueMeaning, sources }) => ({ id, label, settingName, valueMeaning, sources })), scannerRules: input.catalog.scannerRules.map(({ tool, ruleId, conditionId, ruleText, sources }) => ({ tool, ruleId, conditionId, ruleText, sources })), coverage: { observations: input.observations.length, semanticallyMapped: facts.length, notSemanticallyMapped: notMapped, unresolvedIdentity, scannerFindings: input.scannerFindings?.length ?? 0, scannerMapped: observations.length, scannerNotInCatalog, scannerGuardRejected, gpoEvaluatedSettings: verdicts.size, gpoMappedByVerdict, gpoUncatalogued, scannerMappedBySection, scannerUncatalogued, scannerWithDeclaredReferences: [...findings.values()].filter(item => item.declaredReferenceIds.length > 0).length, scannerWithCisControlArea: [...findings.values()].filter(item => item.cisControlAreaIds.length > 0).length, scannerMappedByDeclaredTechnique: new Set(techniqueAssignments.map(item => item.occurrenceId)).size, scannerRecognized: countSubjects(recognitions, 'scanner-finding', 'recognized'), scannerEstimated: countSubjects(recognitions, 'scanner-finding', 'estimated'), gpoRecognized: countSubjects(recognitions, 'gpo-setting', 'recognized'), gpoEstimated: countSubjects(recognitions, 'gpo-setting', 'estimated'), uncataloguedWithoutRecognition: scannerUncatalogued + gpoUncatalogued } }; }