Added a resolver/validation flow that fetches endpoint security template definitions and enforces them before CREATE/PATCH so we don’t call Graph with invalid settings. Hardened restore endpoint resolution (built-in fallback to deviceManagement/configurationPolicies, clearer error metadata, preview-only fallback when metadata is missing) and exposed Graph path/method in restore UI details. Stripped read-only fields when PATCHing endpointSecurityIntent so the request no longer fails with “properties not patchable”. Added regression tests covering endpoint security restore, intent sanitization, unknown type safety, Graph error metadata, and endpoint resolution behavior. Testing GraphClientEndpointResolutionTest.php ./vendor/bin/pint --dirty Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #25
375 lines
24 KiB
PHP
375 lines
24 KiB
PHP
@php
|
|
$results = $getState() ?? [];
|
|
$foundationItems = collect($results)->filter(function ($item) {
|
|
return is_array($item) && array_key_exists('decision', $item) && array_key_exists('sourceId', $item);
|
|
});
|
|
$policyItems = collect($results)->reject(function ($item) {
|
|
return is_array($item) && array_key_exists('decision', $item) && array_key_exists('sourceId', $item);
|
|
});
|
|
@endphp
|
|
|
|
@if (empty($results))
|
|
<p class="text-sm text-gray-600">No results recorded.</p>
|
|
@else
|
|
@php
|
|
$needsAttention = $policyItems->contains(function ($item) {
|
|
$status = $item['status'] ?? null;
|
|
|
|
return in_array($status, ['partial', 'manual_required'], true);
|
|
});
|
|
@endphp
|
|
|
|
<div class="space-y-4">
|
|
@if ($foundationItems->isNotEmpty())
|
|
<div class="space-y-2">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Foundations</div>
|
|
@foreach ($foundationItems as $item)
|
|
@php
|
|
$decision = $item['decision'] ?? 'mapped_existing';
|
|
$decisionColor = match ($decision) {
|
|
'created' => 'text-green-700 bg-green-100 border-green-200',
|
|
'created_copy' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
'mapped_existing' => 'text-blue-700 bg-blue-100 border-blue-200',
|
|
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
'skipped' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
};
|
|
@endphp
|
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
|
<div class="flex items-center justify-between text-sm text-gray-800">
|
|
<span class="font-semibold">{{ $item['sourceName'] ?? $item['sourceId'] ?? 'Foundation' }}</span>
|
|
<span class="rounded border px-2 py-0.5 text-xs font-semibold uppercase tracking-wide {{ $decisionColor }}">
|
|
{{ $decision }}
|
|
</span>
|
|
</div>
|
|
<div class="mt-1 text-xs text-gray-600">
|
|
{{ $item['type'] ?? 'foundation' }}
|
|
</div>
|
|
@if (! empty($item['targetName']))
|
|
<div class="mt-1 text-xs text-gray-600">
|
|
Target: {{ $item['targetName'] }}
|
|
</div>
|
|
@endif
|
|
@if (! empty($item['reason']))
|
|
<div class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1 text-xs text-amber-900">
|
|
{{ $item['reason'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
@if ($needsAttention)
|
|
<div class="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">
|
|
Some settings could not be applied automatically. Review the per-setting details below.
|
|
</div>
|
|
@endif
|
|
|
|
@if ($policyItems->isNotEmpty())
|
|
<div class="space-y-3">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Policies</div>
|
|
@foreach ($policyItems as $item)
|
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
|
<div class="flex items-center justify-between text-sm">
|
|
<div class="font-semibold text-gray-900">
|
|
{{ $item['policy_identifier'] ?? $item['policy_id'] ?? 'Policy' }}
|
|
<span class="ml-2 text-xs text-gray-500">{{ $item['policy_type'] ?? '' }}</span>
|
|
</div>
|
|
@php
|
|
$status = $item['status'] ?? 'unknown';
|
|
$restoreMode = $item['restore_mode'] ?? null;
|
|
$statusColor = match ($status) {
|
|
'applied' => 'text-green-700 bg-green-100 border-green-200',
|
|
'dry_run' => 'text-blue-700 bg-blue-100 border-blue-200',
|
|
'skipped' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
'partial' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
'manual_required' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
};
|
|
@endphp
|
|
<div class="flex items-center gap-2">
|
|
@if ($restoreMode === 'preview-only')
|
|
<span class="rounded border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-amber-900">
|
|
preview-only
|
|
</span>
|
|
@endif
|
|
<span class="rounded border px-2 py-0.5 text-xs font-semibold uppercase tracking-wide {{ $statusColor }}">
|
|
{{ $status }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
@php
|
|
$itemReason = $item['reason'] ?? null;
|
|
$itemGraphMessage = $item['graph_error_message'] ?? null;
|
|
|
|
if ($itemReason === 'preview_only') {
|
|
$itemReason = 'Preview-only policy type; execution skipped.';
|
|
}
|
|
@endphp
|
|
|
|
@if (! empty($itemReason) && ($itemGraphMessage === null || $itemGraphMessage !== $itemReason))
|
|
<div class="mt-2 text-sm text-gray-800">
|
|
{{ $itemReason }}
|
|
</div>
|
|
@endif
|
|
|
|
@if (! empty($item['assignment_summary']) && is_array($item['assignment_summary']))
|
|
@php
|
|
$summary = $item['assignment_summary'];
|
|
$assignmentOutcomes = $item['assignment_outcomes'] ?? [];
|
|
$assignmentIssues = collect($assignmentOutcomes)
|
|
->filter(fn ($outcome) => in_array($outcome['status'] ?? null, ['failed', 'skipped'], true))
|
|
->values();
|
|
@endphp
|
|
|
|
<div class="mt-2 text-xs text-gray-700">
|
|
Assignments: {{ (int) ($summary['success'] ?? 0) }} success •
|
|
{{ (int) ($summary['failed'] ?? 0) }} failed •
|
|
{{ (int) ($summary['skipped'] ?? 0) }} skipped
|
|
</div>
|
|
|
|
@if ($assignmentIssues->isNotEmpty())
|
|
<details class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1 text-xs text-amber-900">
|
|
<summary class="cursor-pointer font-semibold">Assignment details</summary>
|
|
<div class="mt-2 space-y-2">
|
|
@foreach ($assignmentIssues as $outcome)
|
|
@php
|
|
$outcomeStatus = $outcome['status'] ?? 'unknown';
|
|
$outcomeColor = match ($outcomeStatus) {
|
|
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
'skipped' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
};
|
|
$assignmentGroupId = $outcome['group_id']
|
|
?? ($outcome['assignment']['target']['groupId'] ?? null);
|
|
@endphp
|
|
|
|
<div class="rounded border border-amber-200 bg-white p-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="font-semibold text-gray-900">
|
|
Assignment {{ $assignmentGroupId ?? 'unknown group' }}
|
|
</div>
|
|
<span class="rounded border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {{ $outcomeColor }}">
|
|
{{ $outcomeStatus }}
|
|
</span>
|
|
</div>
|
|
|
|
@if (! empty($outcome['mapped_group_id']))
|
|
<div class="mt-1 text-[11px] text-gray-800">
|
|
Mapped to: {{ $outcome['mapped_group_id'] }}
|
|
</div>
|
|
@endif
|
|
|
|
@php
|
|
$outcomeReason = $outcome['reason'] ?? null;
|
|
$outcomeGraphMessage = $outcome['graph_error_message'] ?? null;
|
|
@endphp
|
|
|
|
@if (! empty($outcomeReason) && ($outcomeGraphMessage === null || $outcomeGraphMessage !== $outcomeReason))
|
|
<div class="mt-1 text-[11px] text-gray-800">
|
|
{{ $outcomeReason }}
|
|
</div>
|
|
@endif
|
|
|
|
@if (! empty($outcome['graph_error_message']) || ! empty($outcome['graph_error_code']))
|
|
<div class="mt-1 text-[11px] text-amber-900">
|
|
<div>{{ $outcome['graph_error_message'] ?? 'Unknown error' }}</div>
|
|
@if (! empty($outcome['graph_error_code']))
|
|
<div class="mt-0.5 text-amber-800">Code: {{ $outcome['graph_error_code'] }}</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</details>
|
|
@endif
|
|
@endif
|
|
|
|
@if (! empty($item['compliance_action_summary']) && is_array($item['compliance_action_summary']))
|
|
@php
|
|
$summary = $item['compliance_action_summary'];
|
|
$complianceOutcomes = is_array($item['compliance_action_outcomes'] ?? null)
|
|
? $item['compliance_action_outcomes']
|
|
: [];
|
|
$complianceEntries = collect($complianceOutcomes)->values();
|
|
@endphp
|
|
|
|
<div class="mt-2 text-xs text-gray-700">
|
|
Compliance notifications: {{ (int) ($summary['mapped'] ?? 0) }} mapped •
|
|
{{ (int) ($summary['skipped'] ?? 0) }} skipped
|
|
</div>
|
|
|
|
@if ($complianceEntries->isNotEmpty())
|
|
<details class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1 text-xs text-amber-900">
|
|
<summary class="cursor-pointer font-semibold">Compliance notification details</summary>
|
|
<div class="mt-2 space-y-2">
|
|
@foreach ($complianceEntries as $outcome)
|
|
@php
|
|
$outcomeStatus = $outcome['status'] ?? 'unknown';
|
|
$outcomeColor = match ($outcomeStatus) {
|
|
'mapped' => 'text-green-700 bg-green-100 border-green-200',
|
|
'skipped' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
};
|
|
@endphp
|
|
<div class="rounded border border-amber-200 bg-white p-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="font-semibold text-gray-900">
|
|
Template {{ $outcome['template_id'] ?? 'unknown' }}
|
|
</div>
|
|
<span class="rounded border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {{ $outcomeColor }}">
|
|
{{ $outcomeStatus }}
|
|
</span>
|
|
</div>
|
|
@if (! empty($outcome['rule_name']))
|
|
<div class="mt-1 text-[11px] text-gray-700">
|
|
Rule: {{ $outcome['rule_name'] }}
|
|
</div>
|
|
@endif
|
|
@if (! empty($outcome['mapped_template_id']))
|
|
<div class="mt-1 text-[11px] text-gray-700">
|
|
Mapped to: {{ $outcome['mapped_template_id'] }}
|
|
</div>
|
|
@endif
|
|
@if (! empty($outcome['reason']))
|
|
<div class="mt-1 text-[11px] text-gray-800">
|
|
{{ $outcome['reason'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</details>
|
|
@endif
|
|
@endif
|
|
|
|
@if (! empty($item['created_policy_id']))
|
|
@php
|
|
$createdMode = $item['created_policy_mode'] ?? null;
|
|
$createdMessage = match ($createdMode) {
|
|
'metadata_only' => 'New policy created (metadata only). Apply settings manually.',
|
|
'created' => 'New policy created.',
|
|
default => 'New policy created (manual cleanup required).',
|
|
};
|
|
@endphp
|
|
<div class="mt-2 text-xs text-amber-800">
|
|
{{ $createdMessage }} ID: {{ $item['created_policy_id'] }}
|
|
</div>
|
|
@endif
|
|
|
|
@if (! empty($item['graph_error_message']) || ! empty($item['graph_error_code']))
|
|
<div class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1 text-xs text-amber-900">
|
|
<div class="font-semibold">Graph error</div>
|
|
<div>{{ $item['graph_error_message'] ?? 'Unknown error' }}</div>
|
|
@if (! empty($item['graph_error_code']))
|
|
<div class="mt-1 text-[11px] text-amber-800">Code: {{ $item['graph_error_code'] }}</div>
|
|
@endif
|
|
@if (! empty($item['graph_request_id']) || ! empty($item['graph_client_request_id']) || ! empty($item['graph_method']) || ! empty($item['graph_path']))
|
|
<details class="mt-1">
|
|
<summary class="cursor-pointer text-[11px] font-semibold text-amber-800">Details</summary>
|
|
<div class="mt-1 space-y-0.5 text-[11px] text-amber-800">
|
|
@if (! empty($item['graph_method']))
|
|
<div>method: {{ $item['graph_method'] }}</div>
|
|
@endif
|
|
@if (! empty($item['graph_path']))
|
|
<div>path: {{ $item['graph_path'] }}</div>
|
|
@endif
|
|
@if (! empty($item['graph_request_id']))
|
|
<div>request-id: {{ $item['graph_request_id'] }}</div>
|
|
@endif
|
|
@if (! empty($item['graph_client_request_id']))
|
|
<div>client-request-id: {{ $item['graph_client_request_id'] }}</div>
|
|
@endif
|
|
</div>
|
|
</details>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
@if (! empty($item['settings_apply']) && is_array($item['settings_apply']))
|
|
@php
|
|
$apply = $item['settings_apply'];
|
|
$total = (int) ($apply['total'] ?? 0);
|
|
$applied = (int) ($apply['applied'] ?? 0);
|
|
$failed = (int) ($apply['failed'] ?? 0);
|
|
$manual = (int) ($apply['manual_required'] ?? 0);
|
|
$issues = $apply['issues'] ?? [];
|
|
@endphp
|
|
|
|
<div class="mt-2 text-xs text-gray-700">
|
|
Settings applied: {{ $applied }}/{{ $total }}
|
|
@if ($failed > 0 || $manual > 0)
|
|
• {{ $failed }} failed • {{ $manual }} manual
|
|
@endif
|
|
</div>
|
|
|
|
@if (! empty($issues))
|
|
<details class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1 text-xs text-amber-900">
|
|
<summary class="cursor-pointer font-semibold">Settings requiring attention</summary>
|
|
<div class="mt-2 space-y-2">
|
|
@foreach ($issues as $issue)
|
|
@php
|
|
$issueStatus = $issue['status'] ?? 'unknown';
|
|
$issueColor = match ($issueStatus) {
|
|
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
'manual_required' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
};
|
|
@endphp
|
|
<div class="rounded border border-amber-200 bg-white p-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="font-semibold text-gray-900">
|
|
Setting {{ $issue['setting_id'] ?? 'unknown' }}
|
|
</div>
|
|
<span class="rounded border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {{ $issueColor }}">
|
|
{{ $issueStatus }}
|
|
</span>
|
|
</div>
|
|
|
|
@if (! empty($issue['reason']))
|
|
<div class="mt-1 text-[11px] text-gray-800">
|
|
{{ $issue['reason'] }}
|
|
</div>
|
|
@endif
|
|
|
|
@if (! empty($issue['graph_error_message']) || ! empty($issue['graph_error_code']))
|
|
<div class="mt-1 text-[11px] text-amber-900">
|
|
<div>{{ $issue['graph_error_message'] ?? 'Unknown error' }}</div>
|
|
@if (! empty($issue['graph_error_code']))
|
|
<div class="mt-0.5 text-amber-800">Code: {{ $issue['graph_error_code'] }}</div>
|
|
@endif
|
|
@if (! empty($issue['graph_request_id']) || ! empty($issue['graph_client_request_id']))
|
|
<div class="mt-0.5 space-y-0.5 text-amber-800">
|
|
@if (! empty($issue['graph_request_id']))
|
|
<div>request-id: {{ $issue['graph_request_id'] }}</div>
|
|
@endif
|
|
@if (! empty($issue['graph_client_request_id']))
|
|
<div>client-request-id: {{ $issue['graph_client_request_id'] }}</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</details>
|
|
@endif
|
|
@endif
|
|
|
|
@if (! empty($item['platform']))
|
|
<div class="mt-2 text-[11px] text-gray-500">
|
|
Platform: {{ $item['platform'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|