clusev/app/Services/PostureService.php

69 lines
2.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\Server;
/**
* Turns a server's hardening state into a single security-posture SCORE. Reuses
* HardeningService::state() (the CIS-relevant gates: SSH root/password, fail2ban, firewall) rather
* than re-probing. `neutral` items (auto-updates = an operator preference, not a gate) and
* OS-unsupported items don't count toward the score. The remediation for a failing check is the
* existing hardening toggle on the server-details page.
*/
class PostureService
{
public function __construct(private HardeningService $hardening) {}
/**
* @return array{score:int, rating:string, passed:int, applicable:int,
* checks:array<int, array{key:string,label:string,detail:string,secure:bool,supported:bool,neutral:bool,counts:bool}>}
*/
public function score(Server $server): array
{
$items = $this->hardening->state($server); // throws if the host can't be read
$checks = [];
$passed = 0;
$applicable = 0;
foreach ($items as $it) {
// A check counts toward the score only when the OS supports it AND it's a real security
// gate (not the neutral auto-updates preference).
$counts = $it['supported'] && ! $it['neutral'];
if ($counts) {
$applicable++;
if ($it['secure']) {
$passed++;
}
}
$checks[] = [
'key' => $it['key'],
'label' => $it['label'],
'detail' => $it['detail'],
'secure' => (bool) $it['secure'],
'supported' => (bool) $it['supported'],
'neutral' => (bool) $it['neutral'],
'counts' => $counts,
];
}
$score = $applicable > 0 ? (int) round($passed / $applicable * 100) : 100;
return [
'score' => $score,
'rating' => $this->rating($score),
'passed' => $passed,
'applicable' => $applicable,
'checks' => $checks,
];
}
/** strong ≥90 · fair ≥60 · weak below — mapped to the status triad in the UI. */
private function rating(int $score): string
{
return $score >= 90 ? 'strong' : ($score >= 60 ? 'fair' : 'weak');
}
}