TenantAtlas/resources/views/filament/pages/baseline-compare-landing.blade.php
ahmido ef41c9193a feat: add Intune RBAC baseline compare support (#156)
## Summary
- add Intune RBAC Role Definition baseline scope support, capture references, compare classification, findings evidence, and landing/detail UI labels
- keep Intune Role Assignments explicitly excluded from baseline compare scope, summaries, findings, and restore messaging
- add focused Pest coverage for baseline scope selection, capture, compare behavior, recurrence, isolation, findings rendering, inventory anchoring, and RBAC summaries

## Verification
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Unit/Inventory/InventoryPolicyTypeMetaBaselineSupportTest.php tests/Unit/Baselines/BaselinePolicyVersionResolverTest.php tests/Unit/Baselines/BaselineScopeTest.php tests/Unit/IntuneRoleDefinitionNormalizerTest.php tests/Feature/Baselines/BaselineCaptureRbacRoleDefinitionsTest.php tests/Feature/Baselines/BaselineCompareRbacRoleDefinitionsTest.php tests/Feature/Baselines/BaselineCompareDriftEvidenceContractRbacTest.php tests/Feature/Baselines/BaselineCompareCoverageGuardTest.php tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php tests/Feature/Baselines/BaselineCompareFindingRecurrenceKeyTest.php tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php tests/Feature/Filament/BaselineProfileFoundationScopeTest.php tests/Feature/Filament/BaselineSnapshotRbacRoleDefinitionsTest.php tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php tests/Feature/Filament/FindingViewRbacEvidenceTest.php tests/Feature/Findings/FindingRecurrenceTest.php tests/Feature/Findings/DriftStaleAutoResolveTest.php tests/Feature/Inventory/InventorySyncButtonTest.php tests/Feature/Inventory/InventorySyncServiceTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php`
- result: `71 passed (467 assertions)`

## Filament / Platform Notes
- Livewire compliance: unchanged and compatible with Livewire v4.0+
- Provider registration: no panel/provider changes; `bootstrap/providers.php` remains the registration location
- Global search: no new globally searchable resource added; existing global search behavior is unchanged
- Destructive actions: no new destructive actions introduced; existing confirmed actions remain unchanged
- Assets: no new Filament assets introduced; deploy asset handling remains unchanged, including `php artisan filament:assets`
- Testing plan covered: baseline profile scope, snapshot detail, compare job, findings recurrence, findings detail, compare landing labels, inventory sync anchoring, and tenant isolation

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #156
2026-03-09 18:49:20 +00:00

382 lines
19 KiB
PHP

<x-filament::page>
{{-- Auto-refresh while comparison is running --}}
@if ($state === 'comparing')
<div wire:poll.5s="refreshStats"></div>
@endif
@php
$duplicateNamePoliciesCountValue = (int) ($duplicateNamePoliciesCount ?? 0);
@endphp
@if ($duplicateNamePoliciesCountValue > 0)
<div role="alert" class="rounded-lg border border-warning-300 bg-warning-50 p-4 dark:border-warning-700 dark:bg-warning-950/40">
<div class="flex items-start gap-3">
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-warning-600 dark:text-warning-400" />
<div class="flex flex-col gap-1">
<div class="text-base font-semibold text-warning-900 dark:text-warning-200">
{{ __('baseline-compare.duplicate_warning_title') }}
</div>
<div class="text-sm text-warning-800 dark:text-warning-300">
{{ __($duplicateNamePoliciesCountValue === 1 ? 'baseline-compare.duplicate_warning_body_singular' : 'baseline-compare.duplicate_warning_body_plural', [
'count' => $duplicateNamePoliciesCountValue,
'app' => config('app.name', 'TenantPilot'),
]) }}
</div>
</div>
</div>
</div>
@endif
{{-- Row 1: Stats Overview --}}
@if (in_array($state, ['ready', 'idle', 'comparing', 'failed']))
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
{{-- Stat: Assigned Baseline --}}
<x-filament::section>
<div class="flex flex-col gap-1">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('baseline-compare.stat_assigned_baseline') }}</div>
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ $profileName ?? '—' }}</div>
<div class="flex flex-wrap items-center gap-2">
@if ($snapshotId)
<x-filament::badge color="success" size="sm" class="w-fit">
{{ __('baseline-compare.badge_snapshot', ['id' => $snapshotId]) }}
</x-filament::badge>
@endif
@if (filled($coverageStatus))
<x-filament::badge
:color="$coverageStatus === 'ok' ? 'success' : 'warning'"
size="sm"
class="w-fit"
>
{{ $coverageStatus === 'ok' ? __('baseline-compare.badge_coverage_ok') : __('baseline-compare.badge_coverage_warnings') }}
</x-filament::badge>
@endif
@if (filled($fidelity))
<x-filament::badge color="gray" size="sm" class="w-fit">
{{ __('baseline-compare.badge_fidelity', ['level' => Str::title($fidelity)]) }}
</x-filament::badge>
@endif
@if ($hasEvidenceGaps)
<x-filament::badge color="warning" size="sm" class="w-fit" :title="$evidenceGapsTooltip">
{{ __('baseline-compare.badge_evidence_gaps', ['count' => $evidenceGapsCountValue]) }}
</x-filament::badge>
@endif
</div>
@if ($hasEvidenceGaps && filled($evidenceGapsSummary))
<div class="mt-1 text-xs text-warning-700 dark:text-warning-300">
{{ __('baseline-compare.evidence_gaps_tooltip', ['summary' => $evidenceGapsSummary]) }}
</div>
@endif
</div>
</x-filament::section>
{{-- Stat: Total Findings --}}
<x-filament::section>
<div class="flex flex-col gap-1">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('baseline-compare.stat_total_findings') }}</div>
@if ($state === 'failed')
<div class="text-lg font-semibold text-danger-600 dark:text-danger-400">{{ __('baseline-compare.stat_error') }}</div>
@else
<div class="text-3xl font-bold {{ $findingsColorClass }}">
{{ $findingsCount ?? 0 }}
</div>
@endif
@if ($state === 'comparing')
<div class="flex items-center gap-1 text-sm text-info-600 dark:text-info-400">
<x-filament::loading-indicator class="h-3 w-3" />
{{ __('baseline-compare.comparing_indicator') }}
</div>
@elseif (($findingsCount ?? 0) === 0 && $state === 'ready')
<span class="text-sm {{ $whyNoFindingsColor }}">{{ $whyNoFindingsMessage ?? $whyNoFindingsFallback }}</span>
@endif
</div>
</x-filament::section>
{{-- Stat: Last Compared --}}
<x-filament::section>
<div class="flex flex-col gap-1">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('baseline-compare.stat_last_compared') }}</div>
<div class="text-lg font-semibold text-gray-950 dark:text-white" @if ($lastComparedIso) title="{{ $lastComparedIso }}" @endif>
{{ $lastComparedAt ?? __('baseline-compare.stat_last_compared_never') }}
</div>
@if ($this->getRunUrl())
<x-filament::link :href="$this->getRunUrl()" size="sm">
{{ __('baseline-compare.button_view_run') }}
</x-filament::link>
@endif
</div>
</x-filament::section>
</div>
@endif
@if ($hasRbacRoleDefinitionSummary)
<x-filament::section :heading="__('baseline-compare.rbac_summary_title')">
<x-slot name="description">
{{ __('baseline-compare.rbac_summary_description') }}
</x-slot>
<div class="flex flex-wrap items-center gap-3">
<x-filament::badge color="gray">
{{ __('baseline-compare.rbac_summary_compared') }}: {{ (int) ($rbacRoleDefinitionSummary['total_compared'] ?? 0) }}
</x-filament::badge>
<x-filament::badge color="success">
{{ __('baseline-compare.rbac_summary_unchanged') }}: {{ (int) ($rbacRoleDefinitionSummary['unchanged'] ?? 0) }}
</x-filament::badge>
<x-filament::badge color="warning">
{{ __('baseline-compare.rbac_summary_modified') }}: {{ (int) ($rbacRoleDefinitionSummary['modified'] ?? 0) }}
</x-filament::badge>
<x-filament::badge color="danger">
{{ __('baseline-compare.rbac_summary_missing') }}: {{ (int) ($rbacRoleDefinitionSummary['missing'] ?? 0) }}
</x-filament::badge>
<x-filament::badge color="info">
{{ __('baseline-compare.rbac_summary_unexpected') }}: {{ (int) ($rbacRoleDefinitionSummary['unexpected'] ?? 0) }}
</x-filament::badge>
</div>
</x-filament::section>
@endif
{{-- Coverage warnings banner --}}
@if ($state === 'ready' && $hasCoverageWarnings)
<div role="alert" class="rounded-lg border border-warning-300 bg-warning-50 p-4 dark:border-warning-700 dark:bg-warning-950/40">
<div class="flex items-start gap-3">
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-warning-600 dark:text-warning-400" />
<div class="flex flex-col gap-1">
<div class="text-base font-semibold text-warning-900 dark:text-warning-200">
{{ __('baseline-compare.coverage_warning_title') }}
</div>
<div class="text-sm text-warning-800 dark:text-warning-300">
@if (($coverageStatus ?? null) === 'unproven')
{{ __('baseline-compare.coverage_unproven_body') }}
@else
{{ __('baseline-compare.coverage_incomplete_body', [
'count' => (int) ($uncoveredTypesCount ?? 0),
'types' => Str::plural('type', (int) ($uncoveredTypesCount ?? 0)),
]) }}
@endif
@if (! empty($uncoveredTypes))
<div class="mt-2 text-xs text-warning-800 dark:text-warning-300">
{{ __('baseline-compare.coverage_uncovered_label', [
'list' => implode(', ', array_slice($uncoveredTypes, 0, 6)) . (count($uncoveredTypes) > 6 ? '…' : ''),
]) }}
</div>
@endif
</div>
@if ($this->getRunUrl())
<div class="mt-2">
<x-filament::button
:href="$this->getRunUrl()"
tag="a"
color="warning"
outlined
icon="heroicon-o-queue-list"
size="sm"
>
{{ __('baseline-compare.button_view_run') }}
</x-filament::button>
</div>
@endif
</div>
</div>
</div>
@endif
{{-- Failed run banner --}}
@if ($state === 'failed')
<div role="alert" class="rounded-lg border border-danger-300 bg-danger-50 p-4 dark:border-danger-700 dark:bg-danger-950/50">
<div class="flex items-start gap-3">
<x-heroicon-s-x-circle class="h-6 w-6 shrink-0 text-danger-600 dark:text-danger-400" />
<div class="flex flex-col gap-1">
<div class="text-base font-semibold text-danger-800 dark:text-danger-200">
{{ __('baseline-compare.failed_title') }}
</div>
<div class="text-sm text-danger-700 dark:text-danger-300">
{{ $failureReason ?? __('baseline-compare.failed_body_default') }}
</div>
<div class="mt-2 flex items-center gap-3">
@if ($this->getRunUrl())
<x-filament::button
:href="$this->getRunUrl()"
tag="a"
color="danger"
outlined
icon="heroicon-o-queue-list"
size="sm"
>
{{ __('baseline-compare.button_view_failed_run') }}
</x-filament::button>
@endif
</div>
</div>
</div>
</div>
@endif
{{-- Critical drift banner --}}
@if ($state === 'ready' && ($severityCounts['high'] ?? 0) > 0)
<div role="alert" class="rounded-lg border border-danger-300 bg-danger-50 p-4 dark:border-danger-700 dark:bg-danger-950/50">
<div class="flex items-start gap-3">
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-danger-600 dark:text-danger-400" />
<div class="flex flex-col gap-1">
<div class="text-base font-semibold text-danger-800 dark:text-danger-200">
{{ __('baseline-compare.critical_drift_title') }}
</div>
<div class="text-sm text-danger-700 dark:text-danger-300">
{{ __('baseline-compare.critical_drift_body', [
'profile' => $profileName,
'count' => $severityCounts['high'],
'findings' => Str::plural('finding', $severityCounts['high']),
]) }}
</div>
</div>
</div>
</div>
@endif
{{-- State: No tenant / no assignment / no snapshot --}}
@if (in_array($state, ['no_tenant', 'no_assignment', 'no_snapshot']))
<x-filament::section>
<div class="flex flex-col items-center justify-center gap-3 py-8 text-center">
@if ($state === 'no_tenant')
<x-heroicon-o-building-office class="h-12 w-12 text-gray-400 dark:text-gray-500" />
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ __('baseline-compare.empty_no_tenant') }}</div>
@elseif ($state === 'no_assignment')
<x-heroicon-o-link-slash class="h-12 w-12 text-gray-400 dark:text-gray-500" />
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ __('baseline-compare.empty_no_assignment') }}</div>
@elseif ($state === 'no_snapshot')
<x-heroicon-o-camera class="h-12 w-12 text-warning-400 dark:text-warning-500" />
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ __('baseline-compare.empty_no_snapshot') }}</div>
@endif
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
</div>
</x-filament::section>
@endif
{{-- Severity breakdown + actions --}}
@if ($state === 'ready' && ($findingsCount ?? 0) > 0)
<x-filament::section>
<x-slot name="heading">
{{ $findingsCount }} {{ Str::plural('Finding', $findingsCount) }}
</x-slot>
<x-slot name="description">
{{ __('baseline-compare.findings_description') }}
</x-slot>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-3">
@if (($severityCounts['high'] ?? 0) > 0)
<x-filament::badge color="danger">
{{ $severityCounts['high'] }} High
</x-filament::badge>
@endif
@if (($severityCounts['medium'] ?? 0) > 0)
<x-filament::badge color="warning">
{{ $severityCounts['medium'] }} Medium
</x-filament::badge>
@endif
@if (($severityCounts['low'] ?? 0) > 0)
<x-filament::badge color="gray">
{{ $severityCounts['low'] }} Low
</x-filament::badge>
@endif
</div>
<div class="flex items-center gap-3">
@if ($this->getFindingsUrl())
<x-filament::button
:href="$this->getFindingsUrl()"
tag="a"
color="gray"
icon="heroicon-o-eye"
size="sm"
>
{{ __('baseline-compare.button_view_findings') }}
</x-filament::button>
@endif
@if ($this->getRunUrl())
<x-filament::button
:href="$this->getRunUrl()"
tag="a"
color="gray"
outlined
icon="heroicon-o-queue-list"
size="sm"
>
{{ __('baseline-compare.button_review_last_run') }}
</x-filament::button>
@endif
</div>
</div>
</x-filament::section>
@endif
{{-- Ready: no drift --}}
@if ($state === 'ready' && ($findingsCount ?? 0) === 0 && ! $hasCoverageWarnings)
<x-filament::section>
<div class="flex flex-col items-center justify-center gap-3 py-6 text-center">
<x-heroicon-o-check-circle class="h-12 w-12 text-success-500" />
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ __('baseline-compare.no_drift_title') }}</div>
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">
{{ __('baseline-compare.no_drift_body') }}
</div>
@if ($this->getRunUrl())
<x-filament::button
:href="$this->getRunUrl()"
tag="a"
color="gray"
outlined
icon="heroicon-o-queue-list"
size="sm"
>
{{ __('baseline-compare.button_review_last_run') }}
</x-filament::button>
@endif
</div>
</x-filament::section>
@endif
{{-- Ready: warnings, no findings --}}
@if ($state === 'ready' && ($findingsCount ?? 0) === 0 && $hasCoverageWarnings)
<x-filament::section>
<div class="flex flex-col items-center justify-center gap-3 py-6 text-center">
<x-heroicon-o-exclamation-triangle class="h-12 w-12 text-warning-500" />
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ __('baseline-compare.coverage_warnings_title') }}</div>
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">
{{ __('baseline-compare.coverage_warnings_body') }}
</div>
@if ($this->getRunUrl())
<x-filament::button
:href="$this->getRunUrl()"
tag="a"
color="gray"
outlined
icon="heroicon-o-queue-list"
size="sm"
>
{{ __('baseline-compare.button_review_last_run') }}
</x-filament::button>
@endif
</div>
</x-filament::section>
@endif
{{-- Idle state --}}
@if ($state === 'idle')
<x-filament::section>
<div class="flex flex-col items-center justify-center gap-3 py-6 text-center">
<x-heroicon-o-play class="h-12 w-12 text-gray-400 dark:text-gray-500" />
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ __('baseline-compare.idle_title') }}</div>
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">
{{ $message }}
</div>
</div>
</x-filament::section>
@endif
</x-filament::page>