1177 lines
55 KiB
TypeScript
1177 lines
55 KiB
TypeScript
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));
|
||
}
|