Assessor_2.0/src/domain/unified-result.ts

1177 lines
55 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, GpoPreferenceContext } from './gpo-policy';
import {
POLICY_COMPARATOR_CATALOG,
POLICY_EVALUATION_ENGINE,
type PolicyEvaluationResult,
type PolicyEvaluationRun,
type StoredPolicyEvaluation
} from './policy-evaluation';
import type { StoredPolicySnapshotAggregation } from './gpo-snapshot-aggregation';
import { deriveExposureState, deriveIsIoe, deriveResultType } from './result-contract-mapping';
import { isDeviation } from './deviation-predicate';
import { POLICY_CLASSIFICATION_LABELS } from './policy-status-labels';
import { policyComparisonDisposition } from './policy-comparison-disposition';
import { severityRank } from './severity-order';
import { processingReadiness, type ProcessingReadinessStatus } from './processing-compatibility';
import {
discloseSupplementalObservation,
type ResultDbSupplementalDisclosureClass
} from './result-db-supplemental-disclosure';
import type { ResultDbDisclosureTrace } from './result-db-trace';
export type UnifiedResultSourceKind = 'scanner-finding' | 'policy-evaluation' | 'policy-observation' | 'environment-observation';
export type UnifiedResultAudience = 'result-db' | 'internal-analysis';
export type UnifiedOutcome = CanonicalOutcome;
export type PolicyResultClassification =
| 'misconfigured' | 'not-configured' | 'match' | 'conflict' | 'duplicate' | 'review-required';
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 UnifiedResultFrameworkReference {
namespace: string;
value: string;
sourceFieldKey?: string;
sourceLabel?: string;
}
export interface UnifiedResultProjection {
resultProjectionId: string;
sourceKind: UnifiedResultSourceKind;
sourceResultId: string;
projectId: string;
assessmentId?: string;
collectionRunId: string;
scanRunId?: string;
gpoSnapshotId?: string;
evaluationRunId?: string;
sourceTool?: string;
/** Controlled scanner scope used to distinguish products sharing one canonical profile. */
sourceToolScope?: 'active-directory' | 'entra-id' | 'combined';
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<string, JsonValue>;
supplementalDisclosurePolicyVersion?: string;
supplementalDisclosureClass?: ResultDbSupplementalDisclosureClass;
policyScope?: GpoPolicyObservation['scope'];
policyDeliveryMechanism?: GpoPolicyObservation['deliveryMechanism'];
policyState?: GpoPolicyObservation['state'];
policyIdentityStatus?: GpoPolicyIdentityResolutionStatus;
policyIdentityKey?: string;
policyValues?: JsonValue[];
policyDiagnosticCodes?: string[];
policyBackupInstanceId?: string;
/** Link status of the contributing GPO backup from its own GPReport.xml; absent when not retained. */
policyBackupLinkStatus?: 'linked' | 'not-linked';
/**
* Retained Group Policy Preference context. A preference is an instruction with its own
* action, item-level targeting and apply-once semantics, so it is never reduced to a
* comparable policy setting; these fields only make that retained context readable.
*/
policyPreferenceCse?: GpoPreferenceContext['cse'];
policyPreferenceAction?: GpoPreferenceContext['operation'];
policyPreferenceTargeting?: GpoPreferenceContext['targeting']['status'];
/** Retained native identities, distinct from ASSESSOR's backup-instance ID. */
policyGpoId?: string;
policyNativeBackupId?: string;
/** Retained instruction order within its source, never cross-GPO precedence. */
policySourceSequence?: number;
policyBaselineId?: string;
policyBaselineLabel?: string;
policyBaselineValue?: JsonValue;
policyBaselineVariantAssessment?: PolicyEvaluationResult['baselineVariantAssessment'];
policyActualValue?: JsonValue;
policyComparisonRelation?: string;
policyCustomerState?: PolicyEvaluationResult['customerState'];
policyClassification?: PolicyResultClassification;
policySourceCount?: number;
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;
/** Client-owned review state; internal analysis only, withheld by the Result DB disclosure policy. */
reviewStatus?: UnifiedReviewStatus;
reviewDisposition?: UnifiedReviewDisposition;
reviewPriority?: ReviewPriority;
reviewOwner?: string;
reviewDueDate?: string;
sourceObservedAt?: string;
importedAt: string;
evaluatedAt?: string;
createdAt: string;
categories: UnifiedResultCategory[];
frameworkReferences: UnifiedResultFrameworkReference[];
/** Result DB only: content-free redaction trace stored with the materialized row. */
disclosureTrace?: ResultDbDisclosureTrace;
}
export interface UnifiedResultQuery {
audience?: UnifiedResultAudience;
projectId?: string;
assessmentId?: string;
assessmentIds?: string[];
scanRunId?: string;
scanRunIds?: string[];
evaluationRunId?: string;
sourceKind?: UnifiedResultSourceKind[];
sourceTool?: string[];
severity?: CanonicalSeverity[];
outcome?: UnifiedOutcome[];
technicalExecutionState?: TechnicalExecutionState[];
resultType?: CanonicalResultType[];
policyClassification?: PolicyResultClassification[];
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' | 'policyClassification';
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[];
}
function scannerSourceToolScope(source: ScannerUnifiedResultSource): UnifiedResultProjection['sourceToolScope'] {
if (source.occurrence.sourceIdentity.tool.id !== 'purple-knight') return undefined;
const name = source.scanRun.tool.name.trim().toLocaleLowerCase('en-US');
const activeDirectory = name.includes('active directory');
const entra = name.includes('entra');
if (activeDirectory && entra) return 'combined';
if (entra) return 'entra-id';
if (activeDirectory) return 'active-directory';
return undefined;
}
export function resultSourceToolLabel(value?: string): string {
const labels: Record<string, string> = {
'purple-knight-ad': 'Purple Knight for Active Directory',
'purple-knight-entra': 'Purple Knight for Entra ID',
'purple-knight-combined': 'Purple Knight for Active Directory and Entra ID'
};
return value ? labels[value] ?? value : '–';
}
export interface PolicyUnifiedResultSource {
comparison?: PolicyEvaluationResult;
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
}));
}
export function scannerResultFrameworkReferences(occurrence: FindingOccurrence): UnifiedResultFrameworkReference[] {
return occurrence.fieldMappings.filter(mapping => mapping.targetRole === 'framework-reference')
.flatMap(mapping => {
const sourceFieldKey = mapping.sourceFieldKeys[0];
const sourceLabel = occurrence.originalFields.find(field => field.key === sourceFieldKey)?.label;
const values = typeof mapping.normalizedValue === 'string' ? [mapping.normalizedValue]
: Array.isArray(mapping.normalizedValue) ? mapping.normalizedValue.filter((value): value is string => typeof value === 'string') : [];
return values.filter(value => value.trim() !== '').map(value => ({
namespace: occurrence.sourceIdentity.tool.id,
value, sourceFieldKey, sourceLabel
}));
});
}
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,
sourceToolScope: scannerSourceToolScope(source),
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),
frameworkReferences: scannerResultFrameworkReferences(source.occurrence)
};
}
export function createPolicyUnifiedResult(source: PolicyUnifiedResultSource): UnifiedResultProjection {
const { definition, occurrence, comparison } = 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: 'GPOs',
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: comparison ? policyEvaluationOutcome(comparison) : 'not_evaluated',
technicalExecutionState: 'completed',
resultType: 'policy_deviation',
isIoe: false,
isDeviation: comparison ? policyComparisonDisposition(comparison) === 'misconfigured' : false,
policyCustomerState: comparison?.customerState,
policyClassification: comparison ? policyEvaluationClassification(comparison) : undefined,
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,
frameworkReferences: [],
categories: [{
namespace: 'assessor-policy',
categoryCode: definition.category ?? 'group-policy-baseline',
categoryLabel: definition.category ?? 'group-policy-baseline',
categoryType: 'canonical',
isPrimary: true,
sourceTool: 'GPOs',
sourceProfileId: occurrence.environmentProfileId,
sourceProfileVersion: String(occurrence.environmentProfileRevision)
}]
};
}
function policyEvaluationTitle(result: PolicyEvaluationResult): string {
if (result.identity.admx.status === 'matched') return result.identity.admx.reference.policyName;
const target = result.identity.target;
if (target.kind === 'registry-key' || target.kind === 'registry-value') {
return [target.key, target.valueName].filter(Boolean).join(' · ');
}
return `${'section' in target ? target.section : target.kind} · ${target.key}`;
}
function policyEvaluationOutcome(result: PolicyEvaluationResult): UnifiedOutcome {
if (policyComparisonDisposition(result) === 'not-configured') return 'informational';
if (result.classification === 'configured_match') return 'pass';
if (policyComparisonDisposition(result) === 'misconfigured') return 'fail';
if (result.classification === 'not_applicable') return 'not_applicable';
return 'warning';
}
function policyEvaluationClassification(result: PolicyEvaluationResult): PolicyResultClassification {
if (policyComparisonDisposition(result) === 'misconfigured') return 'misconfigured';
if (result.customerState === 'conflicted'
|| result.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict')) return 'conflict';
if (policyComparisonDisposition(result) === 'not-configured') return 'not-configured';
if (result.classification === 'configured_match') return 'match';
return 'review-required';
}
function policyDeliveryMechanism(result: PolicyEvaluationResult): GpoPolicyObservation['deliveryMechanism'] {
if (result.identity.target.kind === 'security-template') return 'security-template';
if (result.identity.target.kind === 'advanced-audit-policy') return 'advanced-audit-policy';
return 'registry-policy';
}
export interface PolicyEvaluationUnifiedResultContext {
assessmentId?: string;
importArtifactId?: string;
snapshotId?: string;
}
/**
* Projects a persisted baseline comparison as a normal Result DB row. Finding
* eligibility only controls risk/severity enrichment by the Finding Factory;
* it does not hide a deterministic PASS/FAIL compliance result.
*/
export function createPolicyEvaluationUnifiedResult(
run: PolicyEvaluationRun,
result: PolicyEvaluationResult,
context: PolicyEvaluationUnifiedResultContext = {}
): UnifiedResultProjection {
const outcomeNormalized = policyEvaluationOutcome(result);
return {
resultProjectionId: `policy-evaluation-comparison:${result.id}`,
sourceKind: 'policy-evaluation',
sourceResultId: result.id,
projectId: run.projectId,
assessmentId: context.assessmentId,
collectionRunId: run.aggregationRunId,
gpoSnapshotId: context.snapshotId,
evaluationRunId: run.id,
sourceTool: 'GPOs',
sourceProfileId: run.evaluator.id,
sourceProfileVersion: run.evaluator.version,
mappingRuleVersion: POLICY_COMPARATOR_CATALOG.version,
normalizationRuleVersion: POLICY_COMPARATOR_CATALOG.version,
presentationVersion: 'gpo-policy-evaluation-1.1.0',
processingCompatibilityKey: 'gpo-policy-evaluation-v1',
processingStatus: 'current',
reprocessingAvailable: true,
sourceRuleId: result.identity.id,
importArtifactId: context.importArtifactId,
sourceViewId: result.aggregationStateId,
title: policyEvaluationTitle(result),
severityOriginal: null,
severityNormalized: 'unknown',
outcomeOriginal: result.classification,
outcomeNormalized,
technicalExecutionState: 'completed',
resultType: 'policy_deviation',
isIoe: false,
isDeviation: policyComparisonDisposition(result) === 'misconfigured',
effectiveState: result.classification === 'review_required'
? 'incomplete_observation'
: result.classification,
lifecycleState: 'unknown',
mappingConfidence: 1,
applicabilityState: result.baselineState,
completenessState: result.diagnosticCodes.includes('policy-evaluation-baseline-incomplete') ? 'partial' : 'complete',
scopeState: 'known',
effectivenessVerification: 'not-verified',
scopeConfidence: 1,
reviewStatus: result.classification === 'review_required' ? 'open' : 'unreviewed',
importedAt: run.completedAt,
evaluatedAt: run.completedAt,
createdAt: run.completedAt,
policyScope: result.identity.scope,
policyDeliveryMechanism: policyDeliveryMechanism(result),
policyIdentityStatus: 'resolved',
policyIdentityKey: result.identity.id,
policyDiagnosticCodes: result.diagnosticCodes,
policyBaselineId: result.baselineId,
policyBaselineLabel: result.baselineLabel,
policyBaselineValue: result.comparator.baseline as JsonValue | undefined,
policyBaselineVariantAssessment: result.baselineVariantAssessment,
policyActualValue: result.comparator.actual as JsonValue | undefined,
policyComparisonRelation: result.comparator.relation,
policyCustomerState: result.customerState,
policyClassification: policyEvaluationClassification(result),
frameworkReferences: [],
categories: [{
namespace: 'group-policy',
categoryCode: 'GPOs',
categoryLabel: 'GPOs',
categoryType: 'canonical',
isPrimary: true,
sourceTool: 'GPOs',
sourceProfileId: run.evaluator.id,
sourceProfileVersion: run.evaluator.version
}]
};
}
export function selectCurrentPolicyEvaluations(
records: StoredPolicyEvaluation[],
options: { allowPreviousVersionForDisplay?: boolean } = {}
): StoredPolicyEvaluation[] {
const selected = new Map<string, StoredPolicyEvaluation>();
// Broader benchmark coverage wins, measured in the distinct baselines actually compared
// rather than in analysis references: several retained analysis generations of one
// baseline package are one benchmark, not extra coverage, and must not outrank a
// narrower current run. Where the retained results name no baseline, nothing better is
// known and the declared analysis references remain the only available measure.
const coverage = new Map<string, number>();
const baselineCoverage = (record: StoredPolicyEvaluation): number => {
const cached = coverage.get(record.run.id);
if (cached !== undefined) return cached;
const compared = new Set(record.results.flatMap(result => result.baselineId ? [result.baselineId] : []));
const value = compared.size || record.run.baselineAnalysisIds.length;
coverage.set(record.run.id, value);
return value;
};
for (const record of records) {
const isCurrent = record.run.evaluator.version === POLICY_EVALUATION_ENGINE.version;
// Read-only Work compatibility, never a completion/finding-generation signal.
const readablePrevious = options.allowPreviousVersionForDisplay && ['1.3.3', '1.4.0'].includes(record.run.evaluator.version);
if (record.run.evaluator.id !== POLICY_EVALUATION_ENGINE.id
|| (!isCurrent && !readablePrevious)) continue;
const current = selected.get(record.run.aggregationRunId);
const sameVersion = current?.run.evaluator.version === record.run.evaluator.version;
const newerPrevious = current?.run.evaluator.version === '1.3.3' && record.run.evaluator.version === '1.4.0';
const replacesCurrent = !current || (isCurrent && !sameVersion) || newerPrevious
|| (sameVersion && (baselineCoverage(record) > baselineCoverage(current!)
|| (baselineCoverage(record) === baselineCoverage(current!)
&& `${record.run.completedAt}\u001f${record.run.id}` > `${current.run.completedAt}\u001f${current.run.id}`)));
if (replacesCurrent) selected.set(record.run.aggregationRunId, record);
}
return [...selected.values()].sort((left, right) => left.run.completedAt.localeCompare(right.run.completedAt));
}
export function createCurrentPolicyEvaluationUnifiedResults(
evaluations: StoredPolicyEvaluation[],
aggregations: StoredPolicySnapshotAggregation[],
analyses: StoredGpoPolicyAnalysis[],
artifacts: ImportArtifact[],
excludedEvaluationResultIds: ReadonlySet<string> = new Set()
): UnifiedResultProjection[] {
const aggregationById = new Map(aggregations.map(record => [record.run.id, record]));
const analysisById = new Map(analyses.map(record => [record.run.id, record]));
const artifactById = new Map(artifacts.map(record => [record.id, record]));
return selectCurrentPolicyEvaluations(evaluations).flatMap(record => {
const aggregation = aggregationById.get(record.run.aggregationRunId);
if (!aggregation) return [];
const sourceArtifacts = aggregation.run.sourceAnalysisRunIds
.map(id => analysisById.get(id)?.run.artifactId)
.filter((id): id is string => Boolean(id))
.map(id => artifactById.get(id))
.filter((item): item is ImportArtifact => Boolean(item));
const assessmentIds = [...new Set(sourceArtifacts.map(item => item.assessmentId).filter(Boolean))];
const artifactIds = [...new Set(sourceArtifacts.map(item => item.id))];
const context: PolicyEvaluationUnifiedResultContext = {
assessmentId: assessmentIds.length === 1 ? assessmentIds[0] : undefined,
importArtifactId: artifactIds.length === 1 ? artifactIds[0] : undefined,
snapshotId: aggregation.run.snapshotId
};
return record.results
.filter(result => !excludedEvaluationResultIds.has(result.id))
.map(result => createPolicyEvaluationUnifiedResult(record.run, result, context));
});
}
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,
...(backup.instance.links ? { policyBackupLinkStatus: backup.instance.links.status } : {}),
policyPreferenceCse: observation.preference?.cse,
policyPreferenceAction: observation.preference?.operation,
policyPreferenceTargeting: observation.preference?.targeting.status,
policyGpoId: backup.instance.identity.status === 'resolved' ? backup.instance.identity.gpoId : undefined,
policyNativeBackupId: backup.instance.identity.status === 'resolved' ? backup.instance.identity.backupId : undefined,
policySourceSequence: observation.registryPolicy?.sequence ?? observation.preference?.sequence,
frameworkReferences: [],
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<string, StoredGpoPolicyIdentityResolution>();
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 | undefined {
const { observation, importFile, scanRun, assessment, project } = source;
const disclosure = discloseSupplementalObservation(observation);
if (!disclosure) return undefined;
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,
environmentProperty: disclosure.property,
environmentSubjectType: disclosure.subjectType,
environmentValueType: disclosure.valueType,
environmentState: disclosure.state,
environmentNormalizedValue: disclosure.value,
supplementalDisclosurePolicyVersion: disclosure.policyVersion,
supplementalDisclosureClass: disclosure.disclosureClass,
title: disclosure.title,
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',
importedAt: importFile.importedAt,
createdAt: importFile.importedAt,
frameworkReferences: [],
categories: [{
namespace: 'environment-subject',
categoryCode: disclosure.subjectType,
categoryLabel: disclosure.subjectType,
categoryType: 'canonical',
isPrimary: true,
sourceTool: observation.tool.id,
sourceProfileId: importFile.processedWith.profileId,
sourceProfileVersion: importFile.processedWith.profileVersion
}, {
namespace: 'environment-property',
categoryCode: environmentPropertyGroup(disclosure.property),
categoryLabel: environmentPropertyGroup(disclosure.property),
categoryType: 'canonical',
isPrimary: false,
sourceTool: observation.tool.id,
sourceProfileId: importFile.processedWith.profileId,
sourceProfileVersion: importFile.processedWith.profileVersion
}]
};
}
function includesAny<T>(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 (!includesAny(query.policyClassification, item.policyClassification)) 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,
policyPreferenceCse: item.policyPreferenceCse,
policyPreferenceAction: item.policyPreferenceAction,
policyPreferenceTargeting: item.policyPreferenceTargeting,
policyBaselineLabel: item.policyBaselineLabel,
policyBaselineValue: item.policyBaselineValue,
policyBaselineVariantAssessment: item.policyBaselineVariantAssessment,
policyActualValue: item.policyActualValue,
policyComparisonRelation: item.policyComparisonRelation,
policyCustomerState: item.policyCustomerState,
policyClassification: item.policyClassification,
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<keyof UnifiedResultQuery>): 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<keyof UnifiedResultQuery>
): UnifiedResultFacet {
const countScope = withoutOwnFilter(...ownQueryKeys);
const counts = new Map<string, number>();
countScope.forEach(item => {
const value = getValue(item);
if (value === undefined) return;
counts.set(value, (counts.get(value) ?? 0) + 1);
});
const seen = new Set<string>();
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<string, number>();
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<string, { label: string; namespace: string; code: string }>();
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'),
(() => {
const result = facet('sourceTool', 'Source Tool', item => item.sourceTool, 'sourceTool');
result.values.forEach(value => { value.label = resultSourceToolLabel(value.value); });
result.values.sort((left, right) => left.label.localeCompare(right.label));
return result;
})(),
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'),
(() => {
const result = facet(
'policyClassification', 'Result Status', item => item.policyClassification,
'policyClassification'
);
const labels: Record<PolicyResultClassification, string> = POLICY_CLASSIFICATION_LABELS;
// A selected status must stay visible and removable even if another filter
// reduces it to zero (for example Passed together with Deviation = Yes).
for (const value of query.policyClassification ?? []) {
if (!result.values.some(item => item.value === value)) result.values.push({ value, label: labels[value], count: 0 });
}
result.values.forEach(value => { value.label = labels[value.value as PolicyResultClassification] ?? value.label; });
result.values.sort((left, right) => left.label.localeCompare(right.label));
return result;
})(),
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('lifecycleState', 'Lifecycle', item => item.lifecycleState, 'lifecycleState'),
facet('effectiveState', 'Effectiveness', item => item.effectiveState),
categoryFacet()
]
};
}
export function listUnifiedResultCategories(items: UnifiedResultProjection[]): UnifiedResultCategory[] {
const categories = new Map<string, UnifiedResultCategory>();
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));
}