clusev/app/Livewire/Posture/Index.php

64 lines
1.9 KiB
PHP

<?php
namespace App\Livewire\Posture;
use App\Models\Server;
use App\Services\PostureService;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Throwable;
/**
* Fleet security-posture scoreboard. Scores each active-credential server from its hardening state
* (PostureService). Reading a server's posture exposes its hardening GAPS, so the page is
* `operate`-gated (route + mount). The scan is lazy (wire:init) and per-server guarded — one
* unreachable host shows an error row instead of blocking the whole scan.
*/
#[Layout('layouts.app')]
class Index extends Component
{
/** @var array<string, array<string, mixed>> server uuid → score result (or ['error' => msg]) */
public array $scores = [];
public bool $ready = false;
public function mount(): void
{
abort_unless(Auth::user()?->can('operate'), 403);
}
public function title(): string
{
return __('posture.title');
}
public function scan(PostureService $posture): void
{
abort_unless(Auth::user()?->can('operate'), 403);
$this->scores = [];
foreach (Server::withActiveCredential()->orderBy('name')->get() as $server) {
try {
$this->scores[$server->uuid] = $posture->score($server);
} catch (Throwable $e) {
$this->scores[$server->uuid] = ['error' => $e->getMessage()];
}
}
$this->ready = true;
}
public function render(): View
{
$scored = array_filter($this->scores, fn ($s) => isset($s['score']));
$avg = $scored !== [] ? (int) round(array_sum(array_column($scored, 'score')) / count($scored)) : null;
return view('livewire.posture.index', [
'servers' => Server::withActiveCredential()->orderBy('name')->get(['uuid', 'name']),
'average' => $avg,
])->title($this->title());
}
}