import type { Assessment, AssessmentProject, CanonicalExposureState, CanonicalOutcome, CanonicalFindingStatus, CanonicalResultType, CanonicalSeverity, CanonicalTechnicalExecutionState, FindingDefinition, FindingOccurrence, FindingReview, ImportFileRecord, JsonValue, ReviewPriority, ScanRun, SourcePosition } from './canonical-model'; import type { PolicyFindingOccurrence } from './policy-finding'; import type { EnvironmentCollectionState, EnvironmentObservation, EnvironmentValueType } from './environment-context'; import type { ImportArtifact, ImportProcessingRun } from './ingestion-persistence'; import type { StoredGpoBackupInstance, StoredGpoPolicyAnalysis, StoredGpoPolicyIdentityResolution } from './gpo-analysis-persistence'; import type { GpoPolicyObservation } from './gpo-policy'; import { deriveExposureState, deriveIsIoe, deriveResultType } from './result-contract-mapping'; import { isDeviation } from './deviation-predicate'; import { severityRank } from './severity-order'; import { processingReadiness, type ProcessingReadinessStatus } from './processing-compatibility'; export type UnifiedResultSourceKind = 'scanner-finding' | 'policy-evaluation' | 'policy-observation' | 'environment-observation'; export type UnifiedOutcome = CanonicalOutcome; export type TechnicalExecutionState = CanonicalTechnicalExecutionState; export type UnifiedReviewStatus = 'unreviewed' | 'open' | 'in-progress' | 'resolved'; export type UnifiedReviewDisposition = 'accepted' | 'deferred' | 'not-relevant'; export type UnifiedLifecycleState = 'new' | 'persistent' | 'resolved' | 'reoccurred' | 'unknown'; export type UnifiedEffectiveState = | 'confirmed_configured_deviation' | 'configured_match' | 'not_applicable' | 'unsupported' | 'unresolved_identity' | 'incomplete_observation' | 'missing_baseline_recommendation' | 'not_evaluated' | 'unknown_effective_state'; export interface UnifiedResultCategory { namespace: string; categoryCode: string; categoryLabel: string; categoryType: 'source' | 'canonical'; isPrimary: boolean; sourceTool?: string; sourceProfileId?: string; sourceProfileVersion?: string; } export interface UnifiedResultProjection { resultProjectionId: string; sourceKind: UnifiedResultSourceKind; sourceResultId: string; projectId: string; assessmentId?: string; collectionRunId: string; scanRunId?: string; gpoSnapshotId?: string; evaluationRunId?: string; sourceTool?: string; sourceToolVersion?: string; sourceProfileId?: string; sourceProfileVersion?: string; mappingRuleVersion?: string; normalizationRuleVersion?: string; presentationVersion?: string; processingCompatibilityKey?: string; processingStatus?: ProcessingReadinessStatus; reprocessingAvailable?: boolean; sourceRuleId?: string; canonicalDefinitionId?: string; importArtifactId?: string; sourceViewId?: string; sourceLocator?: SourcePosition; environmentProperty?: string; environmentSubjectType?: string; environmentSubjectIdentity?: string; environmentValueType?: EnvironmentValueType; environmentState?: EnvironmentCollectionState; environmentOriginalValue?: JsonValue; environmentNormalizedValue?: JsonValue; environmentSectionKey?: string; environmentAttributes?: Record; policyScope?: GpoPolicyObservation['scope']; policyDeliveryMechanism?: GpoPolicyObservation['deliveryMechanism']; policyState?: GpoPolicyObservation['state']; policyIdentityStatus?: GpoPolicyIdentityResolutionStatus; policyIdentityKey?: string; policyValues?: JsonValue[]; policyDiagnosticCodes?: string[]; policyBackupInstanceId?: string; title: string; description?: string; recommendation?: string; affectedAsset?: string; score?: number; severityOriginal: JsonValue; severityNormalized: CanonicalSeverity; outcomeOriginal: JsonValue; outcomeNormalized: UnifiedOutcome; technicalExecutionState: TechnicalExecutionState; /** * RESULT_CONTRACT-Facetten (docs/RESULT_CONTRACT.md), additiv realisiert * über src/domain/result-contract-mapping.ts. exposureState existiert nur * bei resultType = 'exposure_finding'. */ resultType: CanonicalResultType; isIoe: boolean; /** * Abgeleitet ueber src/domain/deviation-predicate.ts, nicht persistiert. * Bewusst breiter als `isIoe`, damit Funde ohne festgestellte Exposition * (Outcome `warning`) nicht aus den Abweichungsansichten fallen. */ isDeviation: boolean; exposureState?: CanonicalExposureState; effectiveState?: UnifiedEffectiveState; lifecycleState: UnifiedLifecycleState; mappingConfidence: number; applicabilityState?: string; completenessState?: string; scopeState?: 'known' | 'unknown'; effectivenessVerification?: 'verified' | 'not-verified'; scopeConfidence?: number; reviewStatus: UnifiedReviewStatus; reviewDisposition?: UnifiedReviewDisposition; reviewPriority?: ReviewPriority; reviewOwner?: string; reviewDueDate?: string; sourceObservedAt?: string; importedAt: string; evaluatedAt?: string; createdAt: string; categories: UnifiedResultCategory[]; } export interface UnifiedResultQuery { projectId?: string; assessmentId?: string; assessmentIds?: string[]; scanRunId?: string; scanRunIds?: string[]; evaluationRunId?: string; sourceKind?: UnifiedResultSourceKind[]; sourceTool?: string[]; severity?: CanonicalSeverity[]; outcome?: UnifiedOutcome[]; technicalExecutionState?: TechnicalExecutionState[]; resultType?: CanonicalResultType[]; isIoe?: boolean; deviation?: boolean; mappingRuleVersion?: string[]; sourceProfileVersion?: string[]; processingStatus?: ProcessingReadinessStatus[]; reviewDisposition?: UnifiedReviewDisposition[]; lifecycleState?: UnifiedLifecycleState[]; categoryNamespace?: string[]; categoryValue?: string[]; affectedAsset?: string; environmentState?: EnvironmentCollectionState[]; environmentProperty?: string[]; environmentSubjectType?: string[]; search?: string; observedFrom?: string; observedTo?: string; offset?: number; limit?: number; } export interface UnifiedResultQueryResult { total: number; items: UnifiedResultProjection[]; } export interface UnifiedResultFacetValue { value: string; label: string; count: number; namespace?: string; } export interface UnifiedResultFacet { id: 'sourceKind' | 'sourceTool' | 'severity' | 'outcome' | 'technicalExecutionState' | 'reviewStatus' | 'reviewDisposition' | 'lifecycleState' | 'effectiveState' | 'category' | 'resultType' | 'isIoe' | 'deviation' | 'mappingRuleVersion' | 'sourceProfileVersion' | 'processingStatus' | 'environmentState' | 'environmentProperty' | 'environmentSubjectType'; label: string; values: UnifiedResultFacetValue[]; } export interface UnifiedResultFacetResult { total: number; facets: UnifiedResultFacet[]; } export interface ScannerUnifiedResultSource { project: AssessmentProject; assessment: Assessment; scanRun: ScanRun; importFile: ImportFileRecord; definition: FindingDefinition; occurrence: FindingOccurrence; reviews: FindingReview[]; } export interface PolicyUnifiedResultSource { definition: FindingDefinition; occurrence: PolicyFindingOccurrence; } export interface EnvironmentUnifiedResultSource { project: AssessmentProject; assessment: Assessment; scanRun: ScanRun; importFile: ImportFileRecord; observation: EnvironmentObservation; } type GpoPolicyIdentityResolutionStatus = StoredGpoPolicyIdentityResolution['resolution']['status']; export interface PolicyObservationUnifiedResultSource { artifact: ImportArtifact; backup: StoredGpoBackupInstance; observation: GpoPolicyObservation; resolution?: StoredGpoPolicyIdentityResolution; } export function outcomeFromLegacyStatus(status: CanonicalFindingStatus): UnifiedOutcome { if (status === 'failed') return 'fail'; if (status === 'passed') return 'pass'; if (status === 'error') return 'execution_error'; if (status === 'not-run') return 'not_run'; if (status === 'not-applicable') return 'not_applicable'; if (status === 'manual') return 'informational'; if (status === 'investigate') return 'warning'; return 'unknown'; } export function technicalStateFromLegacyStatus(status: CanonicalFindingStatus): TechnicalExecutionState { if (status === 'error') return 'failed'; if (status === 'not-run') return 'not-run'; if (status === 'unknown') return 'unknown'; return 'completed'; } function matchingReview(source: ScannerUnifiedResultSource): FindingReview | undefined { return source.reviews .filter(review => review.target.kind === 'occurrence' ? review.target.occurrenceId === source.occurrence.id : review.target.definitionId === source.definition.id) .sort((left, right) => { const leftOccurrence = left.target.kind === 'occurrence' ? 1 : 0; const rightOccurrence = right.target.kind === 'occurrence' ? 1 : 0; return rightOccurrence - leftOccurrence || right.revision - left.revision || right.id.localeCompare(left.id); })[0]; } function reviewDimensions(review?: FindingReview): { status: UnifiedReviewStatus; disposition?: UnifiedReviewDisposition; } { if (!review || review.workflowStatus === 'unreviewed') return { status: 'unreviewed' }; if (review.workflowStatus === 'open') return { status: 'open' }; if (review.workflowStatus === 'in-progress') return { status: 'in-progress' }; if (review.workflowStatus === 'accepted') return { status: 'resolved', disposition: 'accepted' }; if (review.workflowStatus === 'deferred') return { status: 'resolved', disposition: 'deferred' }; if (review.workflowStatus === 'not-relevant') return { status: 'resolved', disposition: 'not-relevant' }; return { status: 'resolved' }; } function categoryStrings(value: JsonValue): string[] { if (typeof value === 'string') return value.trim() ? [value.trim()] : []; if (Array.isArray(value)) return value.flatMap(categoryStrings); return []; } export function scannerResultCategories( definition: FindingDefinition, occurrence: FindingOccurrence, importFile: ImportFileRecord ): UnifiedResultCategory[] { const values = [ ...(definition.category?.trim() ? [definition.category.trim()] : []), ...occurrence.fieldMappings .filter(mapping => mapping.targetRole === 'category') .flatMap(mapping => categoryStrings(mapping.normalizedValue)) ]; const unique = [...new Set(values)]; return unique.map((value, index) => ({ namespace: occurrence.sourceIdentity.tool.id, categoryCode: value, categoryLabel: value, categoryType: 'source', isPrimary: index === 0, sourceTool: occurrence.sourceIdentity.tool.id, sourceProfileId: importFile.processedWith.profileId, sourceProfileVersion: importFile.processedWith.profileVersion })); } function mappingConfidence(occurrence: FindingOccurrence): number { const scores = [ occurrence.severity.provenance.confidence.score, occurrence.status.provenance.confidence.score, occurrence.outcome?.provenance.confidence.score, occurrence.technicalExecutionState?.provenance.confidence.score, ...occurrence.fieldMappings.map(mapping => mapping.provenance.confidence.score) ].filter((score): score is number => score !== undefined && Number.isFinite(score)); return scores.length ? Math.min(...scores) : 0; } export function createScannerUnifiedResult(source: ScannerUnifiedResultSource): UnifiedResultProjection { const review = matchingReview(source); const dimensions = reviewDimensions(review); const outcomeNormalized = source.occurrence.outcome?.normalizedValue ?? outcomeFromLegacyStatus(source.occurrence.status.normalizedValue ?? 'unknown'); const resultType = source.occurrence.resultType ?? deriveResultType(source.occurrence.sourceIdentity.tool.id); const exposureState = source.occurrence.exposureState ?? deriveExposureState(resultType, outcomeNormalized); const isIoe = source.occurrence.isIoe ?? deriveIsIoe(resultType, exposureState).isIoe; return { resultProjectionId: `scanner-finding:${source.occurrence.id}`, sourceKind: 'scanner-finding', sourceResultId: source.occurrence.id, projectId: source.project.id, assessmentId: source.assessment.id, collectionRunId: source.scanRun.id, scanRunId: source.scanRun.id, sourceTool: source.occurrence.sourceIdentity.tool.id, sourceToolVersion: source.occurrence.sourceIdentity.tool.version, sourceProfileId: source.importFile.processedWith.profileId, sourceProfileVersion: source.importFile.processedWith.profileVersion, mappingRuleVersion: source.occurrence.mappingRuleVersion ?? source.importFile.processedWith.mappingVersion, normalizationRuleVersion: source.importFile.processedWith.normalizationVersion, presentationVersion: source.importFile.processedWith.presentationVersion, processingCompatibilityKey: source.importFile.processedWith.compatibilityKey, processingStatus: processingReadiness(source.importFile.processedWith.profileId, source.importFile.processedWith), reprocessingAvailable: source.importFile.sourceArtifact.kind !== 'not-retained', sourceRuleId: source.occurrence.sourceIdentity.originalRuleId ?? source.occurrence.sourceIdentity.canonicalRuleId, canonicalDefinitionId: source.definition.id, importArtifactId: source.importFile.id, sourceViewId: source.occurrence.sourceTableId, sourceLocator: source.occurrence.sourcePosition, title: source.definition.title, description: source.definition.description, recommendation: source.definition.recommendation, affectedAsset: source.occurrence.affectedAsset?.normalizedValue ?? undefined, score: source.occurrence.score?.normalizedValue ?? undefined, severityOriginal: source.occurrence.severity.originalValue, severityNormalized: source.occurrence.severity.normalizedValue ?? 'unknown', outcomeOriginal: source.occurrence.outcome?.originalValue ?? source.occurrence.status.originalValue, outcomeNormalized: outcomeNormalized, technicalExecutionState: source.occurrence.technicalExecutionState?.normalizedValue ?? technicalStateFromLegacyStatus(source.occurrence.status.normalizedValue ?? 'unknown'), resultType, isIoe, isDeviation: isDeviation(isIoe, outcomeNormalized), exposureState, lifecycleState: 'unknown', mappingConfidence: mappingConfidence(source.occurrence), reviewStatus: dimensions.status, reviewDisposition: dimensions.disposition, reviewPriority: review?.priority, reviewOwner: review?.owner, reviewDueDate: review?.dueAt, sourceObservedAt: source.occurrence.observedAt, importedAt: source.importFile.importedAt, createdAt: source.importFile.importedAt, categories: scannerResultCategories(source.definition, source.occurrence, source.importFile) }; } export function createPolicyUnifiedResult(source: PolicyUnifiedResultSource): UnifiedResultProjection { const { definition, occurrence } = source; return { resultProjectionId: `policy-evaluation:${occurrence.id}`, sourceKind: 'policy-evaluation', sourceResultId: occurrence.id, projectId: occurrence.projectId, collectionRunId: occurrence.evaluationRunId, gpoSnapshotId: occurrence.snapshotId, evaluationRunId: occurrence.evaluationRunId, sourceTool: 'group-policy-baseline', sourceProfileId: occurrence.environmentProfileId, sourceProfileVersion: String(occurrence.environmentProfileRevision), mappingRuleVersion: 'policy-finding-factory-1.0.0', normalizationRuleVersion: 'policy-finding-factory-1.0.0', presentationVersion: 'policy-finding-factory-1.0.0', processingCompatibilityKey: 'policy-finding-canonical-v1', processingStatus: 'current', reprocessingAvailable: true, sourceRuleId: occurrence.riskRuleId, canonicalDefinitionId: definition.id, sourceViewId: occurrence.aggregationStateId, title: definition.title, description: definition.description, recommendation: definition.recommendation, severityOriginal: occurrence.severity, severityNormalized: occurrence.severity, outcomeOriginal: occurrence.classification, outcomeNormalized: occurrence.outcome, technicalExecutionState: 'completed', resultType: 'policy_deviation', isIoe: false, isDeviation: true, effectiveState: 'unknown_effective_state', lifecycleState: 'unknown', mappingConfidence: 1, applicabilityState: 'explicit-recommendation', completenessState: 'complete', scopeState: occurrence.scopeState, effectivenessVerification: occurrence.effectiveState, scopeConfidence: 0, reviewStatus: 'unreviewed', importedAt: occurrence.createdAt, evaluatedAt: occurrence.createdAt, createdAt: occurrence.createdAt, categories: [{ namespace: 'assessor-policy', categoryCode: definition.category ?? 'group-policy-baseline', categoryLabel: definition.category ?? 'group-policy-baseline', categoryType: 'canonical', isPrimary: true, sourceTool: 'group-policy-baseline', sourceProfileId: occurrence.environmentProfileId, sourceProfileVersion: String(occurrence.environmentProfileRevision) }] }; } function policyObservationTitle(observation: GpoPolicyObservation): string { const section = observation.identity.section.trim(); const key = observation.identity.key.trim(); return [section, key].filter(Boolean).join(' · '); } function policyObservationLocator(observation: GpoPolicyObservation): SourcePosition { const suffix = observation.sourceLines?.length ? `#lines=${observation.sourceLines.join(',')}` : observation.sourceRecords?.length ? `#records=${observation.sourceRecords.map(record => record.index).join(',')}` : ''; return { objectPath: observation.sourcePath + suffix }; } /** * Projects a parsed GPO setting as reviewable evidence. This is deliberately * not a policy deviation: only the Finding Factory may create one after a * complete baseline evaluation. The projection makes imported GPO content * visible without weakening that safety boundary. */ export function createPolicyObservationUnifiedResult(source: PolicyObservationUnifiedResultSource): UnifiedResultProjection { const { artifact, backup, observation, resolution } = source; const resolved = resolution?.resolution.status === 'resolved' ? resolution.resolution.identity : undefined; const unresolvedReason = resolution?.resolution.status === 'unresolved' ? resolution.resolution.reasonCode : 'policy-identity-resolution-missing'; const policyIdentityKey = resolved?.id ?? `${observation.identity.namespace}:${observation.identity.section}:${observation.identity.key}`; const diagnosticCodes = [...new Set([ ...observation.diagnosticCodes, ...(resolved ? [] : [unresolvedReason]) ])].sort(); const needsReview = !resolved || observation.state === 'conflicted' || diagnosticCodes.length > 0; return { resultProjectionId: `policy-observation:${observation.id}`, sourceKind: 'policy-observation', sourceResultId: observation.id, projectId: backup.projectId, assessmentId: artifact.assessmentId, collectionRunId: backup.processingRunId, sourceTool: 'group-policy', sourceProfileId: observation.parser.id, sourceProfileVersion: observation.parser.version, mappingRuleVersion: resolved?.resolver.version ?? resolution?.analysisRunId ?? 'unresolved', normalizationRuleVersion: observation.reducer.version, presentationVersion: 'gpo-policy-observation-1.0.0', processingCompatibilityKey: 'gpo-policy-observation-v1', processingStatus: 'current', reprocessingAvailable: true, sourceRuleId: resolved?.id, importArtifactId: artifact.id, sourceViewId: backup.id, sourceLocator: policyObservationLocator(observation), title: policyObservationTitle(observation), description: resolved ? 'Imported Group Policy setting with a resolved technical identity.' : `Imported Group Policy setting requiring identity review (${unresolvedReason}).`, severityOriginal: null, severityNormalized: 'unknown', outcomeOriginal: observation.state, outcomeNormalized: needsReview ? 'warning' : 'informational', technicalExecutionState: 'completed', resultType: 'informational', isIoe: false, isDeviation: false, effectiveState: resolved ? 'not_evaluated' : 'unresolved_identity', lifecycleState: 'unknown', mappingConfidence: resolved ? 1 : 0, completenessState: backup.instance.inventory.status, scopeState: 'unknown', effectivenessVerification: 'not-verified', scopeConfidence: 0, reviewStatus: needsReview ? 'open' : 'unreviewed', sourceObservedAt: backup.instance.backupCreatedAt ?? backup.createdAt, importedAt: artifact.createdAt, createdAt: backup.createdAt, policyScope: observation.scope, policyDeliveryMechanism: observation.deliveryMechanism, policyState: observation.state, policyIdentityStatus: resolved ? 'resolved' : 'unresolved', policyIdentityKey, policyValues: observation.values as unknown as JsonValue[], policyDiagnosticCodes: diagnosticCodes, policyBackupInstanceId: backup.instance.id, categories: [{ namespace: 'group-policy', categoryCode: observation.deliveryMechanism, categoryLabel: observation.deliveryMechanism, categoryType: 'source', isPrimary: true, sourceTool: 'group-policy', sourceProfileId: observation.parser.id, sourceProfileVersion: observation.parser.version }] }; } export function createCurrentPolicyObservationUnifiedResults( artifacts: ImportArtifact[], processingRuns: ImportProcessingRun[], backups: StoredGpoBackupInstance[], analyses: StoredGpoPolicyAnalysis[] ): UnifiedResultProjection[] { const artifactById = new Map(artifacts.map(artifact => [artifact.id, artifact])); const supersededProcessingIds = new Set(processingRuns.map(run => run.supersedesRunId).filter(Boolean)); const currentProcessingIds = new Set(processingRuns .filter(run => !supersededProcessingIds.has(run.id) && run.status !== 'failed') .map(run => run.id)); const supersededAnalysisIds = new Set(analyses.map(record => record.run.supersedesAnalysisRunId).filter(Boolean)); const resolutionByObservation = new Map(); for (const analysis of analyses) { if (supersededAnalysisIds.has(analysis.run.id) || !currentProcessingIds.has(analysis.run.sourceProcessingRunId)) continue; for (const resolution of analysis.resolutions) resolutionByObservation.set(resolution.observationId, resolution); } return backups .filter(backup => currentProcessingIds.has(backup.processingRunId)) .flatMap(backup => { const artifact = artifactById.get(backup.artifactId); if (!artifact) return []; return (backup.instance.policyObservations ?? []).map(observation => createPolicyObservationUnifiedResult({ artifact, backup, observation, resolution: resolutionByObservation.get(observation.id) })); }) .sort((left, right) => left.importedAt.localeCompare(right.importedAt) || left.resultProjectionId.localeCompare(right.resultProjectionId)); } function environmentOutcome(state: EnvironmentCollectionState): UnifiedOutcome { if (state === 'parse-failed') return 'execution_error'; if (['not-assessed', 'access-denied', 'license-restricted', 'unsupported', 'unknown'].includes(state)) return 'not_evaluated'; return 'informational'; } function environmentExecutionState(state: EnvironmentCollectionState): TechnicalExecutionState { if (state === 'parse-failed') return 'failed'; if (['not-assessed', 'access-denied', 'license-restricted', 'unsupported'].includes(state)) return 'not-run'; if (state === 'unknown') return 'unknown'; return 'completed'; } function environmentPropertyGroup(property: string): string { return property.split('.')[0]?.trim() || 'environment'; } export function createEnvironmentUnifiedResult(source: EnvironmentUnifiedResultSource): UnifiedResultProjection { const { observation, importFile, scanRun, assessment, project } = source; const subjectIdentity = observation.subject.identity ?? observation.subject.label; const normalized = observation.normalizedValue ?? observation.originalValue; return { resultProjectionId: `environment-observation:${observation.id}`, sourceKind: 'environment-observation', sourceResultId: observation.id, projectId: project.id, assessmentId: assessment.id, collectionRunId: scanRun.id, scanRunId: scanRun.id, sourceTool: observation.tool.id, sourceToolVersion: observation.tool.version, sourceProfileId: importFile.processedWith.profileId, sourceProfileVersion: importFile.processedWith.profileVersion, mappingRuleVersion: observation.mappingVersion, normalizationRuleVersion: importFile.processedWith.normalizationVersion, presentationVersion: importFile.processedWith.presentationVersion, processingCompatibilityKey: importFile.processedWith.compatibilityKey, processingStatus: processingReadiness(importFile.processedWith.profileId, importFile.processedWith), reprocessingAvailable: importFile.sourceArtifact.kind !== 'not-retained', importArtifactId: importFile.id, sourceViewId: observation.sectionKey, sourceLocator: observation.source, environmentProperty: observation.property, environmentSubjectType: observation.subject.type, environmentSubjectIdentity: subjectIdentity, environmentValueType: observation.valueType, environmentState: observation.state, environmentOriginalValue: observation.originalValue, environmentNormalizedValue: normalized, environmentSectionKey: observation.sectionKey, environmentAttributes: observation.attributes, title: observation.label, affectedAsset: subjectIdentity, severityOriginal: null, severityNormalized: 'info', outcomeOriginal: observation.state, outcomeNormalized: environmentOutcome(observation.state), technicalExecutionState: environmentExecutionState(observation.state), resultType: 'informational', isIoe: false, isDeviation: false, lifecycleState: 'unknown', mappingConfidence: 1, completenessState: observation.state, reviewStatus: 'unreviewed', sourceObservedAt: observation.observedAt, importedAt: importFile.importedAt, createdAt: importFile.importedAt, categories: [{ namespace: 'environment-subject', categoryCode: observation.subject.type, categoryLabel: observation.subject.type, categoryType: 'canonical', isPrimary: true, sourceTool: observation.tool.id, sourceProfileId: importFile.processedWith.profileId, sourceProfileVersion: importFile.processedWith.profileVersion }, { namespace: 'environment-property', categoryCode: environmentPropertyGroup(observation.property), categoryLabel: environmentPropertyGroup(observation.property), categoryType: 'canonical', isPrimary: false, sourceTool: observation.tool.id, sourceProfileId: importFile.processedWith.profileId, sourceProfileVersion: importFile.processedWith.profileVersion }] }; } function includesAny(values: T[] | undefined, actual: T | undefined): boolean { return !values?.length || actual !== undefined && values.includes(actual); } function normalizedText(value: string): string { return value.normalize('NFKC').toLocaleLowerCase('de-DE'); } export function filterUnifiedResults(items: UnifiedResultProjection[], query: UnifiedResultQuery = {}): UnifiedResultProjection[] { const sourceTools = query.sourceTool?.map(normalizedText); const search = query.search?.trim() ? normalizedText(query.search.trim()) : ''; const asset = query.affectedAsset?.trim() ? normalizedText(query.affectedAsset.trim()) : ''; const from = query.observedFrom ? Date.parse(query.observedFrom) : Number.NEGATIVE_INFINITY; const to = query.observedTo ? Date.parse(query.observedTo) : Number.POSITIVE_INFINITY; return items.filter(item => { if (query.projectId && item.projectId !== query.projectId) return false; if (query.assessmentId && item.assessmentId !== query.assessmentId) return false; if (query.assessmentIds?.length && (!item.assessmentId || !query.assessmentIds.includes(item.assessmentId))) return false; if (query.scanRunId && item.scanRunId !== query.scanRunId) return false; if (query.scanRunIds?.length && item.scanRunId && !query.scanRunIds.includes(item.scanRunId)) return false; if (query.scanRunIds?.length && !item.scanRunId && !query.assessmentIds?.includes(item.assessmentId ?? '')) return false; if (query.evaluationRunId && item.evaluationRunId !== query.evaluationRunId) return false; if (!includesAny(query.sourceKind, item.sourceKind)) return false; if (sourceTools?.length && (!item.sourceTool || !sourceTools.includes(normalizedText(item.sourceTool)))) return false; if (!includesAny(query.severity, item.severityNormalized)) return false; if (!includesAny(query.outcome, item.outcomeNormalized)) return false; if (!includesAny(query.technicalExecutionState, item.technicalExecutionState)) return false; if (!includesAny(query.resultType, item.resultType)) return false; if (query.isIoe !== undefined && item.isIoe !== query.isIoe) return false; if (query.deviation !== undefined && item.sourceKind !== 'policy-observation' && item.isDeviation !== query.deviation) return false; if (!includesAny(query.mappingRuleVersion, item.mappingRuleVersion)) return false; if (!includesAny(query.sourceProfileVersion, item.sourceProfileVersion)) return false; if (!includesAny(query.processingStatus, item.processingStatus)) return false; if (!includesAny(query.reviewDisposition, item.reviewDisposition)) return false; if (!includesAny(query.lifecycleState, item.lifecycleState)) return false; if (!includesAny(query.environmentState, item.environmentState)) return false; if (!includesAny(query.environmentProperty, item.environmentProperty)) return false; if (!includesAny(query.environmentSubjectType, item.environmentSubjectType)) return false; if (query.categoryNamespace?.length && !item.categories.some(category => query.categoryNamespace!.includes(category.namespace))) return false; if (query.categoryValue?.length && !item.categories.some(category => query.categoryValue!.includes(category.categoryCode) || query.categoryValue!.includes(category.categoryLabel))) return false; if (asset && !normalizedText(item.affectedAsset ?? '').includes(asset)) return false; const observed = Date.parse(item.sourceObservedAt ?? item.evaluatedAt ?? item.importedAt); if (observed < from || observed > to) return false; if (search) { const text = normalizedText(JSON.stringify({ title: item.title, description: item.description, recommendation: item.recommendation, asset: item.affectedAsset, sourceRuleId: item.sourceRuleId, environmentProperty: item.environmentProperty, environmentSubjectType: item.environmentSubjectType, environmentSubjectIdentity: item.environmentSubjectIdentity, environmentState: item.environmentState, environmentOriginalValue: item.environmentOriginalValue, environmentNormalizedValue: item.environmentNormalizedValue, policyScope: item.policyScope, policyDeliveryMechanism: item.policyDeliveryMechanism, policyState: item.policyState, policyIdentityStatus: item.policyIdentityStatus, policyIdentityKey: item.policyIdentityKey, policyValues: item.policyValues, policyDiagnosticCodes: item.policyDiagnosticCodes, categories: item.categories.map(category => category.categoryLabel) })); if (!text.includes(search)) return false; } return true; }); } export function queryUnifiedResultItems(items: UnifiedResultProjection[], query: UnifiedResultQuery = {}): UnifiedResultQueryResult { // Rangfolge aus src/domain/severity-order.ts: werkzeugunabhaengig, weil // severityNormalized fuer alle Quellen auf dieselbe Skala abgebildet wird. // Zuvor stand hier eine zweite, wortgleiche Kopie derselben Tabelle. const filtered = filterUnifiedResults(items, query) .sort((left, right) => severityRank(left.severityNormalized) - severityRank(right.severityNormalized) || left.importedAt.localeCompare(right.importedAt) || left.resultProjectionId.localeCompare(right.resultProjectionId)); const offset = Math.max(0, query.offset ?? 0); const limit = Math.min(10000, Math.max(0, query.limit ?? 1000)); return { total: filtered.length, items: filtered.slice(offset, offset + limit) }; } /** * Facetten-Werte werden aus zwei getrennten Mengen gebaut: * - `universe`: legt innerhalb von Projekt/Assessment/Scan-Lauf die * grundsaetzlich bekannten Werte fest. * - `countScope`: wendet alle uebrigen aktiven Filter an, nimmt aber den * eigenen Facettenfilter aus. So bleiben erreichbare Alternativen waehlbar. * Werte mit Trefferzahl null werden nicht an die Darstellung weitergegeben. */ function scopeOnlyQuery(query: UnifiedResultQuery): UnifiedResultQuery { return { projectId: query.projectId, assessmentId: query.assessmentId, assessmentIds: query.assessmentIds, scanRunId: query.scanRunId, scanRunIds: query.scanRunIds, evaluationRunId: query.evaluationRunId }; } export function deriveUnifiedResultFacets(items: UnifiedResultProjection[], query: UnifiedResultQuery = {}): UnifiedResultFacetResult { const baseQuery: UnifiedResultQuery = { ...query, offset: undefined, limit: undefined }; const universe = filterUnifiedResults(items, scopeOnlyQuery(query)); const filtered = filterUnifiedResults(items, baseQuery); const withoutOwnFilter = (...keys: Array): UnifiedResultProjection[] => { if (!keys.length) return filtered; const next = { ...baseQuery }; keys.forEach(key => { next[key] = undefined; }); return filterUnifiedResults(items, next); }; function facet( id: UnifiedResultFacet['id'], label: string, getValue: (item: UnifiedResultProjection) => string | undefined, ...ownQueryKeys: Array ): UnifiedResultFacet { const countScope = withoutOwnFilter(...ownQueryKeys); const counts = new Map(); countScope.forEach(item => { const value = getValue(item); if (value === undefined) return; counts.set(value, (counts.get(value) ?? 0) + 1); }); const seen = new Set(); const values: UnifiedResultFacetValue[] = []; universe.forEach(item => { const value = getValue(item); if (value === undefined || seen.has(value)) return; seen.add(value); const count = counts.get(value) ?? 0; if (count > 0) values.push({ value, label: value, count }); }); // Der Schweregrad folgt seiner fachlichen Rangfolge, nicht dem Alphabet - // sonst stuende "critical" zwischen "high" und "info". values.sort(id === 'severity' ? (left, right) => severityRank(left.value as CanonicalSeverity) - severityRank(right.value as CanonicalSeverity) : (left, right) => left.label.localeCompare(right.label)); return { id, label, values }; } function categoryFacet(): UnifiedResultFacet { const key = (namespace: string, code: string) => namespace + '::' + code; const countScope = withoutOwnFilter('categoryValue'); const counts = new Map(); countScope.forEach(item => item.categories.forEach(category => { const mapKey = key(category.namespace, category.categoryCode); counts.set(mapKey, (counts.get(mapKey) ?? 0) + 1); })); const seen = new Map(); universe.forEach(item => item.categories.forEach(category => { const mapKey = key(category.namespace, category.categoryCode); if (!seen.has(mapKey)) seen.set(mapKey, { label: category.categoryLabel, namespace: category.namespace, code: category.categoryCode }); })); const values: UnifiedResultFacetValue[] = [...seen.entries()].map(([mapKey, meta]) => ({ value: meta.code, label: meta.label, namespace: meta.namespace, count: counts.get(mapKey) ?? 0 })).filter(value => value.count > 0) .sort((left, right) => (left.namespace ?? '').localeCompare(right.namespace ?? '') || left.label.localeCompare(right.label)); return { id: 'category', label: 'Categories', values }; } const findingAxis = (item: UnifiedResultProjection): boolean => item.sourceKind !== 'environment-observation' && item.sourceKind !== 'policy-observation'; return { total: filtered.length, facets: [ facet('sourceKind', 'Origin Type', item => item.sourceKind, 'sourceKind'), facet('sourceTool', 'Source Tool', item => item.sourceTool, 'sourceTool'), facet('environmentState', 'Environment State', item => item.environmentState, 'environmentState'), facet('environmentSubjectType', 'Environment Subject', item => item.environmentSubjectType, 'environmentSubjectType'), facet('environmentProperty', 'Environment Property', item => item.environmentProperty, 'environmentProperty'), facet('severity', 'Severity', item => findingAxis(item) ? item.severityNormalized : undefined, 'severity'), facet('outcome', 'Outcome', item => findingAxis(item) ? item.outcomeNormalized : undefined, 'outcome'), facet('technicalExecutionState', 'Execution', item => findingAxis(item) ? item.technicalExecutionState : undefined, 'technicalExecutionState'), facet('resultType', 'Result Type', item => findingAxis(item) ? item.resultType : undefined, 'resultType'), facet('isIoe', 'IOE', item => findingAxis(item) ? String(item.isIoe) : undefined, 'isIoe'), facet('deviation', 'Deviation', item => findingAxis(item) ? String(item.isDeviation) : undefined, 'deviation'), facet('mappingRuleVersion', 'Mapping Rule Set', item => item.mappingRuleVersion, 'mappingRuleVersion'), facet('sourceProfileVersion', 'Profile Version', item => item.sourceProfileVersion, 'sourceProfileVersion'), facet('processingStatus', 'Processing Status', item => item.processingStatus, 'processingStatus'), facet('reviewStatus', 'Review Status', item => findingAxis(item) ? item.reviewStatus : undefined), facet('reviewDisposition', 'Review Disposition', item => item.reviewDisposition, 'reviewDisposition'), facet('lifecycleState', 'Lifecycle', item => item.lifecycleState, 'lifecycleState'), facet('effectiveState', 'Effectiveness', item => item.effectiveState), categoryFacet() ] }; } export function listUnifiedResultCategories(items: UnifiedResultProjection[]): UnifiedResultCategory[] { const categories = new Map(); items.flatMap(item => item.categories).forEach(category => { const key = `${category.namespace}\u0000${category.categoryCode}\u0000${category.sourceProfileId ?? ''}\u0000${category.sourceProfileVersion ?? ''}`; if (!categories.has(key)) categories.set(key, category); }); return [...categories.values()].sort((left, right) => left.namespace.localeCompare(right.namespace) || left.categoryLabel.localeCompare(right.categoryLabel)); }