Beschreibung Implementiert das Drift MVP Feature (Spec: 044-drift-mvp) mit Fokus auf automatische Drift-Erkennung zwischen Inventory Sync Runs und Bulk-Triage für Findings. Was wurde implementiert? Drift-Erkennung: Vergleicht Policy-Snapshots, Assignments und Scope Tags zwischen Baseline- und Current-Runs. Deterministische Fingerprints verhindern Duplikate. Findings UI: Neue Filament Resource für Findings mit Listen- und Detail-Ansicht. DB-only Diffs (keine Graph-Calls zur Laufzeit). Bulk Acknowledge: "Acknowledge selected" (Bulk-Action auf der Liste) "Acknowledge all matching" (Header-Action, respektiert aktuelle Filter; Type-to-Confirm bei >100 Findings) Scope Tag Fix: Behebt False Positives bei Legacy-Daten ohne scope_tags.ids (inferiert Default-Werte). Authorization: Tenant-isoliert, Rollen-basiert (Owner/Manager/Operator können acknowledge). Tests: Vollständige Pest-Coverage (28 Tests, 347 Assertions) für Drift-Logik, UI und Bulk-Actions. Warum diese Änderungen? Problem: Keine automatisierte Drift-Erkennung; manuelle Triage bei vielen Findings ist mühsam. Lösung: Async Drift-Generierung mit persistenter Findings-Tabelle. Safe Bulk-Tools für Massen-Triage ohne Deletes. Konformität: Folgt AGENTS.md Workflow, Spec-Kit (Tasks + Checklists abgehakt), Laravel/Filament Best Practices. Technische Details Neue Dateien: ~40 (Models, Services, Tests, Views, Migrations) Änderungen: Filament Resources, Jobs, Policies DB: Neue findings Tabelle (JSONB für Evidence, Indexes für Performance) Tests: ./vendor/bin/sail artisan test tests/Feature/Drift --parallel → 28 passed Migration: ./vendor/bin/sail artisan migrate (neue Tabelle + Indexes) Screenshots / Links Spec: spec.md Tasks: tasks.md (alle abgehakt) UI: Findings-Liste mit Bulk-Actions; Detail-View mit Diffs Checklist Tests passieren (parallel + serial) Code formatiert (./vendor/bin/pint --dirty) Migration reversibel Tenant-Isolation enforced No Graph-Calls in Views Authorization checks Spec + Tasks aligned Deployment Notes Neue Migration: create_findings_table Neue Permissions: drift.view, drift.acknowledge Queue-Job: GenerateDriftFindingsJob (async, deduped)
115 lines
5.2 KiB
PHP
115 lines
5.2 KiB
PHP
@php
|
|
$diff = $getState() ?? [];
|
|
$summary = $diff['summary'] ?? [];
|
|
|
|
$added = is_array($diff['added'] ?? null) ? $diff['added'] : [];
|
|
$removed = is_array($diff['removed'] ?? null) ? $diff['removed'] : [];
|
|
$changed = is_array($diff['changed'] ?? null) ? $diff['changed'] : [];
|
|
|
|
$renderRow = static function (array $row): array {
|
|
return [
|
|
'include_exclude' => (string) ($row['include_exclude'] ?? 'include'),
|
|
'target_label' => (string) ($row['target_label'] ?? 'Unknown target'),
|
|
'filter_type' => (string) ($row['filter_type'] ?? 'none'),
|
|
'filter_id' => $row['filter_id'] ?? null,
|
|
'intent' => $row['intent'] ?? null,
|
|
'mode' => $row['mode'] ?? null,
|
|
];
|
|
};
|
|
@endphp
|
|
|
|
<div class="space-y-4">
|
|
<x-filament::section
|
|
heading="Assignments diff"
|
|
:description="$summary['message'] ?? sprintf('%d added, %d removed, %d changed', $summary['added'] ?? 0, $summary['removed'] ?? 0, $summary['changed'] ?? 0)"
|
|
>
|
|
<div class="flex flex-wrap gap-2">
|
|
<x-filament::badge color="success">
|
|
{{ (int) ($summary['added'] ?? 0) }} added
|
|
</x-filament::badge>
|
|
<x-filament::badge color="danger">
|
|
{{ (int) ($summary['removed'] ?? 0) }} removed
|
|
</x-filament::badge>
|
|
<x-filament::badge color="warning">
|
|
{{ (int) ($summary['changed'] ?? 0) }} changed
|
|
</x-filament::badge>
|
|
|
|
@if (($summary['truncated'] ?? false) === true)
|
|
<x-filament::badge color="gray">
|
|
Truncated to {{ (int) ($summary['limit'] ?? 0) }} items
|
|
</x-filament::badge>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@if ($changed !== [])
|
|
<x-filament::section heading="Changed" collapsible>
|
|
<div class="space-y-3">
|
|
@foreach ($changed as $row)
|
|
@php
|
|
$to = is_array($row['to'] ?? null) ? $renderRow($row['to']) : $renderRow([]);
|
|
$from = is_array($row['from'] ?? null) ? $renderRow($row['from']) : $renderRow([]);
|
|
@endphp
|
|
|
|
<div class="rounded-lg border border-gray-200/70 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
|
<div class="font-medium">
|
|
{{ $to['target_label'] }}
|
|
</div>
|
|
|
|
<div class="mt-2 grid gap-2 text-sm md:grid-cols-2">
|
|
<div>
|
|
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">From</div>
|
|
<div class="mt-1 space-y-1">
|
|
<div>Type: {{ $from['include_exclude'] }}</div>
|
|
<div>Filter: {{ $from['filter_type'] }}@if($from['filter_id']) ({{ $from['filter_id'] }})@endif</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">To</div>
|
|
<div class="mt-1 space-y-1">
|
|
<div>Type: {{ $to['include_exclude'] }}</div>
|
|
<div>Filter: {{ $to['filter_type'] }}@if($to['filter_id']) ({{ $to['filter_id'] }})@endif</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
@if ($added !== [])
|
|
<x-filament::section heading="Added" collapsible :collapsed="true">
|
|
<div class="space-y-2">
|
|
@foreach ($added as $row)
|
|
@php $row = $renderRow(is_array($row) ? $row : []); @endphp
|
|
|
|
<div class="rounded-lg border border-gray-200/70 bg-white p-3 text-sm dark:border-gray-700 dark:bg-gray-900">
|
|
<div class="font-medium">{{ $row['target_label'] }}</div>
|
|
<div class="mt-1 text-gray-600 dark:text-gray-300">
|
|
{{ $row['include_exclude'] }} · filter: {{ $row['filter_type'] }}
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
@if ($removed !== [])
|
|
<x-filament::section heading="Removed" collapsible :collapsed="true">
|
|
<div class="space-y-2">
|
|
@foreach ($removed as $row)
|
|
@php $row = $renderRow(is_array($row) ? $row : []); @endphp
|
|
|
|
<div class="rounded-lg border border-gray-200/70 bg-white p-3 text-sm dark:border-gray-700 dark:bg-gray-900">
|
|
<div class="font-medium">{{ $row['target_label'] }}</div>
|
|
<div class="mt-1 text-gray-600 dark:text-gray-300">
|
|
{{ $row['include_exclude'] }} · filter: {{ $row['filter_type'] }}
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
</div>
|