249 lines
8.7 KiB
PHP
249 lines
8.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Intune;
|
|
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\Tenant;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class RestoreDiffGenerator
|
|
{
|
|
public function __construct(
|
|
private readonly PolicyNormalizer $policyNormalizer,
|
|
private readonly VersionDiff $versionDiff,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<int>|null $selectedItemIds
|
|
* @return array{summary: array<string, mixed>, diffs: array<int, array<string, mixed>>}
|
|
*/
|
|
public function generate(Tenant $tenant, BackupSet $backupSet, ?array $selectedItemIds = null): array
|
|
{
|
|
if ($backupSet->tenant_id !== $tenant->id) {
|
|
throw new \InvalidArgumentException('Backup set does not belong to the provided tenant.');
|
|
}
|
|
|
|
if ($selectedItemIds === []) {
|
|
$selectedItemIds = null;
|
|
}
|
|
|
|
$items = $this->loadItems($backupSet, $selectedItemIds);
|
|
$policyItems = $items
|
|
->reject(fn (BackupItem $item): bool => $item->isFoundation())
|
|
->values();
|
|
|
|
$policyIds = $policyItems
|
|
->pluck('policy_id')
|
|
->filter()
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$latestVersions = $this->latestVersionsByPolicyId($tenant, $policyIds);
|
|
|
|
$maxDetailedDiffs = 25;
|
|
$maxEntriesPerSection = 200;
|
|
|
|
$policiesChanged = 0;
|
|
$assignmentsChanged = 0;
|
|
$scopeTagsChanged = 0;
|
|
|
|
$diffs = [];
|
|
$diffsOmitted = 0;
|
|
|
|
foreach ($policyItems as $index => $item) {
|
|
$policyId = $item->policy_id ? (int) $item->policy_id : null;
|
|
$currentVersion = $policyId ? ($latestVersions[$policyId] ?? null) : null;
|
|
|
|
$currentSnapshot = is_array($currentVersion?->snapshot) ? $currentVersion->snapshot : [];
|
|
$backupSnapshot = is_array($item->payload) ? $item->payload : [];
|
|
|
|
$policyType = (string) ($item->policy_type ?? '');
|
|
$platform = $item->platform;
|
|
|
|
$from = $this->policyNormalizer->flattenForDiff($currentSnapshot, $policyType, $platform);
|
|
$to = $this->policyNormalizer->flattenForDiff($backupSnapshot, $policyType, $platform);
|
|
|
|
$diff = $this->versionDiff->compare($from, $to);
|
|
$summary = $diff['summary'] ?? ['added' => 0, 'removed' => 0, 'changed' => 0];
|
|
|
|
$hasPolicyChanges = ((int) ($summary['added'] ?? 0) + (int) ($summary['removed'] ?? 0) + (int) ($summary['changed'] ?? 0)) > 0;
|
|
|
|
if ($hasPolicyChanges) {
|
|
$policiesChanged++;
|
|
}
|
|
|
|
$assignmentDiff = $this->assignmentsChanged($item->assignments, $currentVersion?->assignments);
|
|
if ($assignmentDiff) {
|
|
$assignmentsChanged++;
|
|
}
|
|
|
|
$scopeTagDiff = $this->scopeTagsChanged($item, $currentVersion);
|
|
if ($scopeTagDiff) {
|
|
$scopeTagsChanged++;
|
|
}
|
|
|
|
$diffEntry = [
|
|
'backup_item_id' => $item->id,
|
|
'display_name' => $item->resolvedDisplayName(),
|
|
'policy_identifier' => $item->policy_identifier,
|
|
'policy_type' => $policyType,
|
|
'platform' => $platform,
|
|
'action' => $currentVersion ? 'update' : 'create',
|
|
'diff' => [
|
|
'summary' => $summary,
|
|
'added' => [],
|
|
'removed' => [],
|
|
'changed' => [],
|
|
],
|
|
'assignments_changed' => $assignmentDiff,
|
|
'scope_tags_changed' => $scopeTagDiff,
|
|
'diff_omitted' => false,
|
|
'diff_truncated' => false,
|
|
];
|
|
|
|
if ($index >= $maxDetailedDiffs) {
|
|
$diffEntry['diff_omitted'] = true;
|
|
$diffEntry['diff_truncated'] = true;
|
|
$diffEntry['diff'] = [
|
|
'summary' => $summary,
|
|
];
|
|
$diffsOmitted++;
|
|
$diffs[] = $diffEntry;
|
|
|
|
continue;
|
|
}
|
|
|
|
$added = is_array($diff['added'] ?? null) ? $diff['added'] : [];
|
|
$removed = is_array($diff['removed'] ?? null) ? $diff['removed'] : [];
|
|
$changed = is_array($diff['changed'] ?? null) ? $diff['changed'] : [];
|
|
|
|
$diffEntry['diff_truncated'] = count($added) > $maxEntriesPerSection
|
|
|| count($removed) > $maxEntriesPerSection
|
|
|| count($changed) > $maxEntriesPerSection;
|
|
|
|
$diffEntry['diff'] = [
|
|
'summary' => $summary,
|
|
'added' => array_slice($added, 0, $maxEntriesPerSection, true),
|
|
'removed' => array_slice($removed, 0, $maxEntriesPerSection, true),
|
|
'changed' => array_slice($changed, 0, $maxEntriesPerSection, true),
|
|
];
|
|
|
|
$diffs[] = $diffEntry;
|
|
}
|
|
|
|
return [
|
|
'summary' => [
|
|
'generated_at' => CarbonImmutable::now()->toIso8601String(),
|
|
'policies_total' => $policyItems->count(),
|
|
'policies_changed' => $policiesChanged,
|
|
'assignments_changed' => $assignmentsChanged,
|
|
'scope_tags_changed' => $scopeTagsChanged,
|
|
'diffs_detailed' => min($policyItems->count(), $maxDetailedDiffs),
|
|
'diffs_omitted' => $diffsOmitted,
|
|
'limits' => [
|
|
'max_detailed_diffs' => $maxDetailedDiffs,
|
|
'max_entries_per_section' => $maxEntriesPerSection,
|
|
],
|
|
],
|
|
'diffs' => $diffs,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<int>|null $selectedItemIds
|
|
* @return Collection<int, BackupItem>
|
|
*/
|
|
private function loadItems(BackupSet $backupSet, ?array $selectedItemIds): Collection
|
|
{
|
|
$query = $backupSet->items()->getQuery();
|
|
|
|
if ($selectedItemIds !== null) {
|
|
$query->whereIn('id', $selectedItemIds);
|
|
}
|
|
|
|
return $query->orderBy('id')->get();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $policyIds
|
|
* @return array<int, PolicyVersion>
|
|
*/
|
|
private function latestVersionsByPolicyId(Tenant $tenant, array $policyIds): array
|
|
{
|
|
if ($policyIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$latestVersionsQuery = PolicyVersion::query()
|
|
->where('tenant_id', $tenant->id)
|
|
->whereIn('policy_id', $policyIds)
|
|
->selectRaw('policy_id, max(version_number) as version_number')
|
|
->groupBy('policy_id');
|
|
|
|
return PolicyVersion::query()
|
|
->where('tenant_id', $tenant->id)
|
|
->joinSub($latestVersionsQuery, 'latest_versions', function ($join): void {
|
|
$join->on('policy_versions.policy_id', '=', 'latest_versions.policy_id')
|
|
->on('policy_versions.version_number', '=', 'latest_versions.version_number');
|
|
})
|
|
->get()
|
|
->keyBy('policy_id')
|
|
->all();
|
|
}
|
|
|
|
private function assignmentsChanged(?array $backupAssignments, ?array $currentAssignments): bool
|
|
{
|
|
$backup = $this->normalizeAssignments($backupAssignments);
|
|
$current = $this->normalizeAssignments($currentAssignments);
|
|
|
|
return $backup !== $current;
|
|
}
|
|
|
|
private function scopeTagsChanged(BackupItem $backupItem, ?PolicyVersion $currentVersion): bool
|
|
{
|
|
$backupIds = $backupItem->scope_tag_ids;
|
|
$backupIds = is_array($backupIds) ? $backupIds : [];
|
|
$backupIds = array_values(array_filter($backupIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0'));
|
|
sort($backupIds);
|
|
|
|
$scopeTags = $currentVersion?->scope_tags;
|
|
$currentIds = is_array($scopeTags) ? ($scopeTags['ids'] ?? []) : [];
|
|
$currentIds = is_array($currentIds) ? $currentIds : [];
|
|
$currentIds = array_values(array_filter($currentIds, fn (mixed $id): bool => is_string($id) && $id !== '' && $id !== '0'));
|
|
sort($currentIds);
|
|
|
|
return $backupIds !== $currentIds;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function normalizeAssignments(?array $assignments): array
|
|
{
|
|
$assignments = is_array($assignments) ? $assignments : [];
|
|
|
|
$normalized = [];
|
|
|
|
foreach ($assignments as $assignment) {
|
|
if (! is_array($assignment)) {
|
|
continue;
|
|
}
|
|
|
|
$normalized[] = $assignment;
|
|
}
|
|
|
|
usort($normalized, function (array $a, array $b): int {
|
|
$left = json_encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
|
|
$right = json_encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
|
|
|
|
return $left <=> $right;
|
|
});
|
|
|
|
return $normalized;
|
|
}
|
|
}
|