*/ public function buildSettingsDiff(?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion): array { $policyType = $currentVersion?->policy_type ?? $baselineVersion?->policy_type ?? ''; $platform = $currentVersion?->platform ?? $baselineVersion?->platform; $from = $baselineVersion ? $this->settingsNormalizer->normalizeForDiff(is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [], (string) $policyType, $platform) : []; $to = $currentVersion ? $this->settingsNormalizer->normalizeForDiff(is_array($currentVersion->snapshot) ? $currentVersion->snapshot : [], (string) $policyType, $platform) : []; $result = $this->versionDiff->compare($from, $to); $result['policy_type'] = $policyType; return $result; } /** * @return array */ public function buildAssignmentsDiff(Tenant $tenant, ?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion, int $limit = 200): array { $baseline = $baselineVersion ? $this->assignmentsNormalizer->normalizeForDiff($baselineVersion->assignments) : []; $current = $currentVersion ? $this->assignmentsNormalizer->normalizeForDiff($currentVersion->assignments) : []; $baselineMap = []; foreach ($baseline as $row) { $baselineMap[$row['key']] = $row; } $currentMap = []; foreach ($current as $row) { $currentMap[$row['key']] = $row; } $allKeys = array_values(array_unique(array_merge(array_keys($baselineMap), array_keys($currentMap)))); sort($allKeys); $added = []; $removed = []; $changed = []; foreach ($allKeys as $key) { $from = $baselineMap[$key] ?? null; $to = $currentMap[$key] ?? null; if ($from === null && is_array($to)) { $added[] = $to; continue; } if ($to === null && is_array($from)) { $removed[] = $from; continue; } if (! is_array($from) || ! is_array($to)) { continue; } $diffFields = [ 'filter_type', 'filter_id', 'intent', 'mode', ]; $fieldChanges = []; foreach ($diffFields as $field) { $fromValue = $from[$field] ?? null; $toValue = $to[$field] ?? null; if ($fromValue !== $toValue) { $fieldChanges[$field] = [ 'from' => $fromValue, 'to' => $toValue, ]; } } if ($fieldChanges !== []) { $changed[] = [ 'key' => $key, 'include_exclude' => $to['include_exclude'], 'target_type' => $to['target_type'], 'target_id' => $to['target_id'], 'from' => $from, 'to' => $to, 'changes' => $fieldChanges, ]; } } $truncated = false; $total = count($added) + count($removed) + count($changed); if ($total > $limit) { $truncated = true; $budget = $limit; $changed = array_slice($changed, 0, min(count($changed), $budget)); $budget -= count($changed); $added = array_slice($added, 0, min(count($added), $budget)); $budget -= count($added); $removed = array_slice($removed, 0, min(count($removed), $budget)); } $labels = $this->groupLabelsForDiff($tenant, $added, $removed, $changed); $decorateAssignment = function (array $row) use ($labels): array { $row['target_label'] = $this->targetLabel($row, $labels); return $row; }; $decorateChanged = function (array $row) use ($decorateAssignment): array { $row['from'] = is_array($row['from'] ?? null) ? $decorateAssignment($row['from']) : $row['from']; $row['to'] = is_array($row['to'] ?? null) ? $decorateAssignment($row['to']) : $row['to']; $row['target_label'] = is_array($row['to'] ?? null) ? ($row['to']['target_label'] ?? null) : null; return $row; }; return [ 'summary' => [ 'added' => count($added), 'removed' => count($removed), 'changed' => count($changed), 'message' => sprintf('%d added, %d removed, %d changed', count($added), count($removed), count($changed)), 'truncated' => $truncated, 'limit' => $limit, ], 'added' => array_map($decorateAssignment, $added), 'removed' => array_map($decorateAssignment, $removed), 'changed' => array_map($decorateChanged, $changed), ]; } /** * @return array */ public function buildScopeTagsDiff(?PolicyVersion $baselineVersion, ?PolicyVersion $currentVersion): array { $baselineIds = $baselineVersion ? ($this->scopeTagsNormalizer->normalizeIdsForHash($baselineVersion->scope_tags) ?? []) : []; $currentIds = $currentVersion ? ($this->scopeTagsNormalizer->normalizeIdsForHash($currentVersion->scope_tags) ?? []) : []; $baselineLabels = $baselineVersion ? $this->scopeTagsNormalizer->labelsById($baselineVersion->scope_tags) : []; $currentLabels = $currentVersion ? $this->scopeTagsNormalizer->labelsById($currentVersion->scope_tags) : []; $baselineSet = array_fill_keys($baselineIds, true); $currentSet = array_fill_keys($currentIds, true); $addedIds = array_values(array_diff($currentIds, $baselineIds)); $removedIds = array_values(array_diff($baselineIds, $currentIds)); sort($addedIds); sort($removedIds); $decorate = static function (array $ids, array $labels): array { $rows = []; foreach ($ids as $id) { if (! is_string($id) || $id === '') { continue; } $rows[] = [ 'id' => $id, 'name' => $labels[$id] ?? ($id === '0' ? 'Default' : $id), ]; } return $rows; }; return [ 'summary' => [ 'added' => count($addedIds), 'removed' => count($removedIds), 'changed' => 0, 'message' => sprintf('%d added, %d removed', count($addedIds), count($removedIds)), 'baseline_count' => count($baselineSet), 'current_count' => count($currentSet), ], 'added' => $decorate($addedIds, $currentLabels), 'removed' => $decorate($removedIds, $baselineLabels), 'baseline' => $decorate($baselineIds, $baselineLabels), 'current' => $decorate($currentIds, $currentLabels), 'changed' => [], ]; } /** * @param array> $added * @param array> $removed * @param array> $changed * @return array */ private function groupLabelsForDiff(Tenant $tenant, array $added, array $removed, array $changed): array { $groupIds = []; foreach ([$added, $removed] as $items) { foreach ($items as $row) { $targetType = $row['target_type'] ?? null; $targetId = $row['target_id'] ?? null; if (! is_string($targetType) || ! is_string($targetId)) { continue; } if (! str_contains($targetType, 'groupassignmenttarget')) { continue; } $groupIds[] = $targetId; } } foreach ($changed as $row) { $targetType = $row['target_type'] ?? null; $targetId = $row['target_id'] ?? null; if (! is_string($targetType) || ! is_string($targetId)) { continue; } if (! str_contains($targetType, 'groupassignmenttarget')) { continue; } $groupIds[] = $targetId; } $groupIds = array_values(array_unique($groupIds)); if ($groupIds === []) { return []; } return $this->groupLabelResolver->resolveMany($tenant, $groupIds); } /** * @param array $assignment * @param array $groupLabels */ private function targetLabel(array $assignment, array $groupLabels): string { $targetType = $assignment['target_type'] ?? null; $targetId = $assignment['target_id'] ?? null; if (! is_string($targetType) || ! is_string($targetId)) { return 'Unknown target'; } if (str_contains($targetType, 'alldevicesassignmenttarget')) { return 'All devices'; } if (str_contains($targetType, 'allusersassignmenttarget')) { return 'All users'; } if (str_contains($targetType, 'groupassignmenttarget')) { return $groupLabels[$targetId] ?? EntraGroupLabelResolver::formatLabel(null, $targetId); } return sprintf('%s (%s)', $targetType, $targetId); } }