* }, * summary: array{match: int, different: int, missing: int, ambiguous: int, blocked: int, total: int}, * subjects: list> * } */ public function build(CrossTenantCompareSelection $selection): array { $sourceIndex = $this->indexTenantSubjects($selection->sourceTenant, $selection->policyTypes); $targetIndex = $this->indexTenantSubjects($selection->targetTenant, $selection->policyTypes); $sourceEvidence = $this->resolvedEvidenceMap($selection->sourceTenant, $sourceIndex['evidence_subjects']); $targetEvidence = $this->resolvedEvidenceMap($selection->targetTenant, $targetIndex['evidence_subjects']); $subjects = []; $summary = [ 'match' => 0, 'different' => 0, 'missing' => 0, 'ambiguous' => 0, 'blocked' => 0, 'total' => 0, ]; foreach ($sourceIndex['preview_subjects'] as $sourceSubject) { $previewSubject = $this->buildPreviewSubject( sourceSubject: $sourceSubject, sourceTenant: $selection->sourceTenant, targetTenant: $selection->targetTenant, targetIndex: $targetIndex['subjects'], sourceEvidence: $sourceEvidence, targetEvidence: $targetEvidence, ); $subjects[] = $previewSubject; $summary[$previewSubject['state']]++; $summary['total']++; } usort($subjects, function (array $left, array $right): int { $policyTypeComparison = strcmp((string) ($left['policyType'] ?? ''), (string) ($right['policyType'] ?? '')); if ($policyTypeComparison !== 0) { return $policyTypeComparison; } $displayNameComparison = strcmp( Str::lower((string) ($left['displayName'] ?? '')), Str::lower((string) ($right['displayName'] ?? '')), ); if ($displayNameComparison !== 0) { return $displayNameComparison; } return strcmp((string) ($left['subjectKey'] ?? ''), (string) ($right['subjectKey'] ?? '')); }); return [ 'selection' => [ 'workspaceId' => $selection->workspaceId(), 'sourceTenantId' => $selection->sourceTenantId(), 'sourceTenantName' => (string) $selection->sourceTenant->name, 'targetTenantId' => $selection->targetTenantId(), 'targetTenantName' => (string) $selection->targetTenant->name, 'policyTypes' => $selection->policyTypes, ], 'summary' => $summary, 'subjects' => $subjects, ]; } /** * @param Tenant $tenant * @param list $policyTypes * @return array{ * preview_subjects: list>, * evidence_subjects: list, * subjects: array> * } */ private function indexTenantSubjects(Tenant $tenant, array $policyTypes): array { $inventoryItems = InventoryItem::query() ->where('tenant_id', (int) $tenant->getKey()) ->when( $policyTypes !== [], fn ($query) => $query->whereIn('policy_type', $policyTypes), ) ->orderBy('policy_type') ->orderBy('display_name') ->orderBy('id') ->get(); $subjects = []; $previewSubjects = []; $evidenceSubjects = []; foreach ($inventoryItems as $inventoryItem) { if (! $inventoryItem instanceof InventoryItem) { continue; } $policyType = trim((string) $inventoryItem->policy_type); $subjectKey = BaselineSubjectKey::forPolicy( $policyType, is_string($inventoryItem->display_name ?? null) ? (string) $inventoryItem->display_name : null, is_string($inventoryItem->external_id ?? null) ? (string) $inventoryItem->external_id : null, ); $subjectRecord = $this->inventorySubjectRecord($tenant, $inventoryItem, $policyType, $subjectKey); if ($subjectKey === null) { $previewSubjects[] = [ ...$subjectRecord, 'resolution' => 'identifier_missing', 'duplicateCount' => 1, ]; continue; } $indexKey = $this->subjectIndexKey($policyType, $subjectKey); if (! array_key_exists($indexKey, $subjects)) { $subjects[$indexKey] = [ 'policyType' => $policyType, 'subjectKey' => $subjectKey, 'displayName' => $subjectRecord['displayName'], 'items' => [], ]; } $subjects[$indexKey]['items'][] = $subjectRecord; } foreach ($subjects as $indexKey => $subjectGroup) { $items = is_array($subjectGroup['items'] ?? null) ? $subjectGroup['items'] : []; $firstItem = $items[0] ?? null; if (! is_array($firstItem)) { continue; } $previewSubjects[] = [ ...$firstItem, 'resolution' => count($items) > 1 ? 'ambiguous_match' : 'resolved', 'duplicateCount' => count($items), ]; if (count($items) === 1) { $evidenceSubjects[] = [ 'policy_type' => (string) $firstItem['policyType'], 'subject_external_id' => (string) $firstItem['subjectExternalId'], ]; } $subjects[$indexKey]['representative'] = $firstItem; $subjects[$indexKey]['duplicateCount'] = count($items); } return [ 'preview_subjects' => $previewSubjects, 'evidence_subjects' => $evidenceSubjects, 'subjects' => $subjects, ]; } /** * @param array> $targetIndex * @param array $sourceEvidence * @param array $targetEvidence * @return array */ private function buildPreviewSubject( array $sourceSubject, Tenant $sourceTenant, Tenant $targetTenant, array $targetIndex, array $sourceEvidence, array $targetEvidence, ): array { $policyType = (string) ($sourceSubject['policyType'] ?? ''); $displayName = (string) ($sourceSubject['displayName'] ?? ''); $subjectKey = is_string($sourceSubject['subjectKey'] ?? null) ? (string) $sourceSubject['subjectKey'] : null; $reasonCodes = []; $state = 'blocked'; $trustLevel = 'unusable'; $sourceEvidenceRecord = $this->resolvedEvidenceForSubject($sourceEvidence, $sourceSubject); $targetEvidenceRecord = null; $targetSubject = null; if (($sourceSubject['resolution'] ?? null) === 'identifier_missing') { $reasonCodes[] = 'source_identifier_missing'; } elseif (($sourceSubject['resolution'] ?? null) === 'ambiguous_match') { $state = 'ambiguous'; $trustLevel = 'diagnostic_only'; $reasonCodes[] = 'source_subject_ambiguous'; } elseif ($subjectKey !== null) { $targetSubject = $targetIndex[$this->subjectIndexKey($policyType, $subjectKey)] ?? null; if (! is_array($targetSubject)) { $state = 'missing'; $trustLevel = $sourceEvidenceRecord instanceof ResolvedEvidence && $sourceEvidenceRecord->fidelity === EvidenceProvenance::FidelityContent ? 'trustworthy' : 'limited_confidence'; $reasonCodes[] = 'target_subject_missing'; } elseif ((int) ($targetSubject['duplicateCount'] ?? 0) > 1) { $state = 'ambiguous'; $trustLevel = 'diagnostic_only'; $reasonCodes[] = 'target_subject_ambiguous'; } else { $representative = $targetSubject['representative'] ?? null; if (is_array($representative)) { $targetEvidenceRecord = $this->resolvedEvidenceForSubject($targetEvidence, $representative); } if (! $sourceEvidenceRecord instanceof ResolvedEvidence) { $reasonCodes[] = 'source_evidence_refresh_required'; } if (! $targetEvidenceRecord instanceof ResolvedEvidence) { $reasonCodes[] = 'target_evidence_refresh_required'; } if ($sourceEvidenceRecord instanceof ResolvedEvidence && $targetEvidenceRecord instanceof ResolvedEvidence) { $state = $sourceEvidenceRecord->hash === $targetEvidenceRecord->hash ? 'match' : 'different'; $trustLevel = $sourceEvidenceRecord->fidelity === EvidenceProvenance::FidelityContent && $targetEvidenceRecord->fidelity === EvidenceProvenance::FidelityContent ? 'trustworthy' : 'limited_confidence'; if ($sourceEvidenceRecord->fidelity !== EvidenceProvenance::FidelityContent) { $reasonCodes[] = 'source_evidence_refresh_required'; } if ($targetEvidenceRecord->fidelity !== EvidenceProvenance::FidelityContent) { $reasonCodes[] = 'target_evidence_refresh_required'; } } else { $state = 'blocked'; $trustLevel = 'unusable'; } } } if ($state === 'missing' && ! $sourceEvidenceRecord instanceof ResolvedEvidence) { $reasonCodes[] = 'source_evidence_refresh_required'; } if ($state === 'blocked' && $reasonCodes === []) { $reasonCodes[] = 'source_evidence_refresh_required'; } $reasonCodes = array_values(array_unique($reasonCodes)); return [ 'policyType' => $policyType, 'displayName' => $displayName, 'subjectKey' => $subjectKey, 'state' => $state, 'trustLevel' => $trustLevel, 'reasonCodes' => $reasonCodes, 'source' => $this->subjectSidePayload($sourceTenant, $sourceSubject, $sourceEvidenceRecord), 'target' => $this->subjectSidePayload( $targetTenant, is_array($targetSubject['representative'] ?? null) ? $targetSubject['representative'] : null, $targetEvidenceRecord, ), ]; } /** * @param list $subjects * @return array */ private function resolvedEvidenceMap(Tenant $tenant, array $subjects): array { return $this->currentStateHashResolver->resolveForSubjects($tenant, $subjects); } /** * @param array|null $subject * @return array */ private function subjectSidePayload(Tenant $tenant, ?array $subject, ?ResolvedEvidence $evidence): array { return [ 'tenantId' => (int) $tenant->getKey(), 'tenantName' => (string) $tenant->name, 'inventoryItemId' => is_numeric($subject['inventoryItemId'] ?? null) ? (int) $subject['inventoryItemId'] : null, 'subjectExternalId' => is_string($subject['subjectExternalId'] ?? null) ? (string) $subject['subjectExternalId'] : null, 'displayName' => is_string($subject['displayName'] ?? null) ? (string) $subject['displayName'] : null, 'subjectKey' => is_string($subject['subjectKey'] ?? null) ? (string) $subject['subjectKey'] : null, 'lastSeenAt' => is_string($subject['lastSeenAt'] ?? null) ? (string) $subject['lastSeenAt'] : null, 'evidence' => $this->evidencePayload($evidence), ]; } /** * @return array{ * policyType: string, * displayName: string, * subjectKey: ?string, * inventoryItemId: int, * subjectExternalId: string, * lastSeenAt: ?string * } */ private function inventorySubjectRecord(Tenant $tenant, InventoryItem $inventoryItem, string $policyType, ?string $subjectKey): array { $displayName = is_string($inventoryItem->display_name ?? null) ? trim((string) $inventoryItem->display_name) : ''; $displayName = $displayName !== '' ? $displayName : ($subjectKey !== null ? Str::headline($subjectKey) : Str::headline($policyType)); return [ 'tenantId' => (int) $tenant->getKey(), 'policyType' => $policyType, 'displayName' => $displayName, 'subjectKey' => $subjectKey, 'inventoryItemId' => (int) $inventoryItem->getKey(), 'subjectExternalId' => (string) $inventoryItem->external_id, 'lastSeenAt' => $inventoryItem->last_seen_at?->toIso8601String(), ]; } /** * @param array $evidenceMap */ private function resolvedEvidenceForSubject(array $evidenceMap, array $subject): ?ResolvedEvidence { $policyType = trim((string) ($subject['policyType'] ?? '')); $subjectExternalId = trim((string) ($subject['subjectExternalId'] ?? '')); if ($policyType === '' || $subjectExternalId === '') { return null; } $key = $policyType.'|'.$subjectExternalId; $evidence = $evidenceMap[$key] ?? null; return $evidence instanceof ResolvedEvidence ? $evidence : null; } /** * @return array{ * hash: string, * fidelity: string, * source: string, * observedAt: ?string, * policyVersionId: ?int, * operationRunId: ?int, * capturePurpose: ?string * }|null */ private function evidencePayload(?ResolvedEvidence $evidence): ?array { if (! $evidence instanceof ResolvedEvidence) { return null; } return [ 'hash' => $evidence->hash, 'fidelity' => $evidence->fidelity, 'source' => $evidence->source, 'observedAt' => $evidence->observedAt?->toIso8601String(), 'policyVersionId' => is_numeric($evidence->meta['policy_version_id'] ?? null) ? (int) $evidence->meta['policy_version_id'] : null, 'operationRunId' => is_numeric($evidence->meta['operation_run_id'] ?? null) ? (int) $evidence->meta['operation_run_id'] : null, 'capturePurpose' => is_string($evidence->meta['capture_purpose'] ?? null) ? (string) $evidence->meta['capture_purpose'] : null, ]; } private function subjectIndexKey(string $policyType, string $subjectKey): string { return $policyType.'|'.$subjectKey; } }