feat(patches): mark security updates, reboot state, and risky packages

- Security updates: apt marks a package from a "-security" suite (Debian
  12 now reports oldstable-security). dnf reads `dnf updateinfo list
  --security` (dnf5 keeps updateinfo as an alias of advisory). zypper
  uses `zypper -x patch --dry-run --category security`, because
  `zypper -x list-patches` ignores --category and has no package names.
  pacman and apk cannot tell, so they show no security count.
  https://dnf5.readthedocs.io/en/latest/commands/advisory.8.html
  https://github.com/openSUSE/zypper/blob/master/doc/zypper.8.txt
- Reboot required: /var/run/reboot-required, `needrestart -b` (KSTA 2
  or 3), `dnf needs-restarting -r` (matched by text, because a missing
  plugin also exits 1), and `zypper needs-rebooting` (exit 102).
  https://github.com/liske/needrestart/blob/master/README.batch.md
  https://dnf-plugins-core.readthedocs.io/en/latest/needs_restarting.html
- An explicit risk list per package manager (Docker engine, kernels,
  libc/glibc/musl, systemd, dbus, openssh) replaces the "name contains
  docker/kernel" guess on the page and in every notification channel.
- The page shows security and reboot badges, a security count, and the
  risk of each package. "Reboot server" (with confirmation, update
  policy on the loaded server) schedules the reboot with systemd-run and
  checks the server again five minutes later. Archived (transferred)
  servers are never touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-19 10:39:51 +02:00
co-authored by Claude Opus 5.5
parent 6806ed31fa
commit 3da85e2c07
9 changed files with 489 additions and 71 deletions
+18 -30
View File
@@ -8,6 +8,7 @@ use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\PushoverMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
class ServerPatchCheck extends CustomEmailNotification
{
@@ -48,6 +49,8 @@ class ServerPatchCheck extends CustomEmailNotification
'name' => $this->server->name,
'total_updates' => $totalUpdates,
'updates' => $this->patchData['updates'] ?? [],
'security_updates' => $this->patchData['security_updates'] ?? null,
'reboot_required' => $this->patchData['reboot_required'] ?? null,
'osId' => $this->patchData['osId'] ?? 'unknown',
'package_manager' => $this->patchData['package_manager'] ?? 'unknown',
'server_url' => $this->serverUrl,
@@ -101,12 +104,7 @@ class ServerPatchCheck extends CustomEmailNotification
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
$criticalPackages = $this->criticalPackages();
if ($criticalPackages->count() > 0) {
$description .= "\n **Critical packages detected** ({$criticalPackages->count()} packages may require restarts)";
@@ -169,12 +167,7 @@ class ServerPatchCheck extends CustomEmailNotification
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
$criticalPackages = $this->criticalPackages();
if ($criticalPackages->count() > 0) {
$message .= "\n⚠️ Critical packages detected: {$criticalPackages->count()} packages may require restarts\n";
@@ -247,12 +240,7 @@ class ServerPatchCheck extends CustomEmailNotification
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
$criticalPackages = $this->criticalPackages();
if ($criticalPackages->count() > 0) {
$message .= "\nCritical packages detected: {$criticalPackages->count()} may require restarts";
@@ -316,12 +304,7 @@ class ServerPatchCheck extends CustomEmailNotification
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
$criticalPackages = $this->criticalPackages();
if ($criticalPackages->count() > 0) {
$description .= "\n:warning: *Critical packages detected:* {$criticalPackages->count()} packages may require restarts\n";
@@ -364,12 +347,7 @@ class ServerPatchCheck extends CustomEmailNotification
$updates = $this->patchData['updates'] ?? [];
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
$criticalPackages = $this->criticalPackages();
return [
'success' => false,
@@ -382,7 +360,17 @@ class ServerPatchCheck extends CustomEmailNotification
'package_manager' => $this->patchData['package_manager'] ?? 'unknown',
'updates' => $updates,
'critical_packages_count' => $criticalPackages->count(),
'security_updates' => $this->patchData['security_updates'] ?? null,
'reboot_required' => $this->patchData['reboot_required'] ?? null,
'url' => $this->serverUrl,
];
}
/**
* Updates that can restart Docker or core services, or need a reboot (see CheckUpdates::RISKY_PACKAGES).
*/
private function criticalPackages(): Collection
{
return collect($this->patchData['updates'] ?? [])->filter(fn (array $update) => filled($update['risk'] ?? null))->values();
}
}