Feature: ClamAV-Verwaltung in Sicherheit-Sidebar

- Neue Seite /security/clamav: Status, Aktivieren/Deaktivieren,
  Signatur-Update, RAM-Hinweis, Info-Box
- Optionale Dienste (ClamAV) im Dashboard nur sichtbar wenn aktiv
- mailwolt-clamav sbin-Wrapper + sudoers-Regel in ensure_system

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main v1.1.311
boban 2026-04-26 19:21:37 +02:00
parent a7f6d8e242
commit b107e9a580
8 changed files with 358 additions and 0 deletions

View File

@ -90,6 +90,8 @@ class Dashboard extends Component
foreach ($card['sources'] as $src) { foreach ($card['sources'] as $src) {
if ($this->probeSource($src)) { $isOk = true; break; } if ($this->probeSource($src)) { $isOk = true; break; }
} }
// Optionale Dienste (z.B. ClamAV) nur anzeigen wenn aktiv
if (!$isOk && ($card['optional'] ?? false)) continue;
$rows[] = ['label' => $card['label'], 'hint' => $card['hint'], 'ok' => $isOk]; $rows[] = ['label' => $card['label'], 'hint' => $card['hint'], 'ok' => $isOk];
} }
} }

View File

@ -0,0 +1,142 @@
<?php
namespace App\Livewire\Ui\Security;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.dvx')]
#[Title('Virenschutz · Mailwolt')]
class ClamavManager extends Component
{
public bool $installed = false;
public bool $running = false;
public bool $enabled = false;
public string $dbDate = '—';
public string $dbVersion = '—';
public ?int $ramMb = null;
public string $lastError = '';
public function mount(): void
{
$this->refresh();
}
public function refresh(): void
{
$this->lastError = '';
$this->installed = $this->isInstalled();
$this->running = $this->installed && $this->serviceActive();
$this->enabled = $this->installed && $this->serviceEnabled();
$this->ramMb = $this->running ? $this->readRamMb() : null;
[$this->dbDate, $this->dbVersion] = $this->readDbInfo();
}
public function enable(): void
{
if (!$this->installed) {
$this->lastError = 'ClamAV ist nicht installiert.';
return;
}
$out = [];
@exec('sudo -n /usr/local/sbin/mailwolt-clamav enable 2>&1', $out, $rc);
if ($rc !== 0) {
$this->lastError = implode(' ', $out) ?: 'Fehler beim Aktivieren.';
} else {
$this->dispatch('toast', type: 'success', badge: 'ClamAV',
title: 'Aktiviert', text: 'ClamAV läuft jetzt und startet automatisch beim Booten.');
}
$this->refresh();
}
public function disable(): void
{
$out = [];
@exec('sudo -n /usr/local/sbin/mailwolt-clamav disable 2>&1', $out, $rc);
if ($rc !== 0) {
$this->lastError = implode(' ', $out) ?: 'Fehler beim Deaktivieren.';
} else {
$this->dispatch('toast', type: 'success', badge: 'ClamAV',
title: 'Deaktiviert', text: 'ClamAV wurde gestoppt und aus dem Autostart entfernt.');
}
$this->refresh();
}
public function updateDb(): void
{
$out = [];
@exec('sudo -n /usr/local/sbin/mailwolt-clamav freshclam 2>&1', $out, $rc);
if ($rc !== 0) {
$this->lastError = implode(' ', $out) ?: 'Fehler beim Aktualisieren.';
} else {
$this->dispatch('toast', type: 'success', badge: 'ClamAV',
title: 'Datenbank aktualisiert', text: 'Virensignaturen wurden aktualisiert.');
}
$this->refresh();
}
private function isInstalled(): bool
{
return is_executable('/usr/bin/clamscan') || is_executable('/usr/sbin/clamd')
|| @file_exists('/usr/sbin/clamd') || @file_exists('/usr/bin/clamdscan');
}
private function serviceActive(): bool
{
$exit = null;
@exec('systemctl is-active --quiet clamav-daemon 2>/dev/null', $_, $exit);
return $exit === 0;
}
private function serviceEnabled(): bool
{
$exit = null;
@exec('systemctl is-enabled --quiet clamav-daemon 2>/dev/null', $_, $exit);
return $exit === 0;
}
private function readRamMb(): ?int
{
$out = [];
@exec("ps -C clamd -o rss= 2>/dev/null", $out);
$kb = array_sum(array_map('intval', array_filter($out)));
return $kb > 0 ? (int) round($kb / 1024) : null;
}
private function readDbInfo(): array
{
$paths = [
'/var/lib/clamav/main.cvd',
'/var/lib/clamav/main.cld',
'/var/lib/clamav/daily.cvd',
'/var/lib/clamav/daily.cld',
];
$latest = 0;
foreach ($paths as $p) {
if (@file_exists($p)) {
$mtime = @filemtime($p);
if ($mtime > $latest) $latest = $mtime;
}
}
if ($latest === 0) return ['—', '—'];
$date = date('d.m.Y H:i', $latest);
$ver = '—';
$out = [];
@exec('sigtool --info /var/lib/clamav/daily.cld 2>/dev/null | grep "^Version:" | head -1', $out);
if (empty($out)) {
@exec('sigtool --info /var/lib/clamav/daily.cvd 2>/dev/null | grep "^Version:" | head -1', $out);
}
if (!empty($out[0])) {
$ver = trim(str_replace('Version:', '', $out[0]));
}
return [$date, $ver];
}
public function render()
{
return view('livewire.ui.security.clamav-manager');
}
}

View File

@ -40,6 +40,7 @@ return [
'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [ 'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [
['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'], ['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
['label' => 'Ratelimits', 'route' => 'ui.security.abuse'], ['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
['label' => 'Virenschutz', 'route' => 'ui.security.clamav'],
['label' => 'Audit-Logs', 'route' => 'ui.security.audit'], ['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
], ],
], ],

View File

@ -23,6 +23,7 @@ return [
], ],
'clamav' => [ 'clamav' => [
'label' => 'ClamAV', 'hint' => 'Virenscanner', 'label' => 'ClamAV', 'hint' => 'Virenscanner',
'optional' => true,
'sources' => [ 'sources' => [
'systemd:clamav-daemon', 'systemd:clamav-daemon@scan', 'systemd:clamd', 'systemd:clamav-daemon', 'systemd:clamav-daemon@scan', 'systemd:clamd',
'socket:/run/clamav/clamd.ctl', 'pid:/run/clamav/clamd.pid', 'tcp:127.0.0.1:3310', 'socket:/run/clamav/clamd.ctl', 'pid:/run/clamav/clamd.pid', 'tcp:127.0.0.1:3310',

View File

@ -0,0 +1,178 @@
<x-slot:breadcrumbParent>Sicherheit</x-slot:breadcrumbParent>
<x-slot:breadcrumb>Virenschutz</x-slot:breadcrumb>
<div>
<div class="mbx-page-header">
<div class="mbx-page-title">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 1.5C8 1.5 3 3.5 3 7.5v3.5l5 2.5 5-2.5V7.5C13 3.5 8 1.5 8 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M5.5 8l1.5 1.5L10 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Virenschutz (ClamAV)
</div>
<div class="mbx-page-actions">
<button wire:click="refresh" class="mbx-act-btn">
<svg width="12" height="12" viewBox="0 0 14 14" fill="none" wire:loading.class="animate-spin" wire:target="refresh,enable,disable,updateDb"><path d="M12 7A5 5 0 1 1 7 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><path d="M7 2l2-2v4H5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Aktualisieren
</button>
</div>
</div>
{{-- Fehler-Banner --}}
@if($lastError)
<div style="margin-bottom:16px;padding:10px 14px;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.25);border-radius:8px;display:flex;align-items:center;gap:8px">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" style="color:#ef4444;flex-shrink:0"><circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.3"/><path d="M7 4v3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="7" cy="10" r=".6" fill="currentColor"/></svg>
<span style="font-size:12.5px;color:#fca5a5">{{ $lastError }}</span>
</div>
@endif
<div class="sec-layout">
<div style="min-width:0;display:flex;flex-direction:column;gap:16px">
{{-- RAM-Hinweis --}}
<div style="padding:12px 16px;background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.25);border-radius:10px;display:flex;gap:12px;align-items:flex-start">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" style="color:#f59e0b;flex-shrink:0;margin-top:1px"><path d="M8 1.5L13.5 12H2.5L8 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M8 6v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="8" cy="10.5" r=".6" fill="currentColor"/></svg>
<div>
<div style="font-size:12.5px;font-weight:600;color:#fcd34d;margin-bottom:3px">Hoher RAM-Verbrauch</div>
<div style="font-size:12px;color:rgba(253,230,138,.7);line-height:1.5">
ClamAV lädt die komplette Viren-Datenbank (~500 MB) in den Arbeitsspeicher.
Auf Servern mit weniger als 2 GB RAM kann das die Performance beeinträchtigen.
Empfohlen für Produktiv-Mailserver mit ausreichend RAM.
</div>
</div>
</div>
{{-- Status-Karte --}}
<div class="mbx-section">
<div class="mbx-domain-head">
<div class="mbx-domain-info">
<span class="mbx-badge-mute">Status</span>
</div>
</div>
<div style="padding:16px 18px">
@if(!$installed)
{{-- Nicht installiert --}}
<div style="display:flex;flex-direction:column;gap:14px">
<div style="display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
<div style="width:8px;height:8px;border-radius:50%;background:#6b7280;flex-shrink:0"></div>
<div>
<div style="font-size:13px;font-weight:500;color:var(--mw-t2)">ClamAV nicht installiert</div>
<div style="font-size:11.5px;color:var(--mw-t4);margin-top:2px">Das Paket <code style="font-size:11px;background:var(--mw-bg3);padding:1px 5px;border-radius:4px">clamav clamav-daemon</code> ist nicht verfügbar.</div>
</div>
</div>
<div style="padding:12px 14px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
<div style="font-size:12px;font-weight:500;color:var(--mw-t3);margin-bottom:6px">Installation</div>
<code style="font-size:11.5px;color:var(--mw-t2);background:var(--mw-bg3);padding:6px 10px;border-radius:6px;display:block">apt install clamav clamav-daemon -y</code>
<div style="font-size:11.5px;color:var(--mw-t4);margin-top:6px">Nach der Installation diese Seite neu laden.</div>
</div>
</div>
@else
{{-- Installiert --}}
<div style="display:flex;flex-direction:column;gap:16px">
{{-- Status-Zeile --}}
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
<div style="display:flex;align-items:center;gap:10px">
@if($running)
<div style="width:8px;height:8px;border-radius:50%;background:#34d399;box-shadow:0 0 6px rgba(52,211,153,.4);flex-shrink:0"></div>
<div>
<div style="font-size:13px;font-weight:500;color:var(--mw-t1)">ClamAV läuft</div>
<div style="font-size:11.5px;color:var(--mw-t4);margin-top:1px">
Aktiv{{ $enabled ? ', Autostart ein' : '' }}
@if($ramMb) · {{ $ramMb }} MB RAM @endif
</div>
</div>
@else
<div style="width:8px;height:8px;border-radius:50%;background:#6b7280;flex-shrink:0"></div>
<div>
<div style="font-size:13px;font-weight:500;color:var(--mw-t2)">ClamAV inaktiv</div>
<div style="font-size:11.5px;color:var(--mw-t4);margin-top:1px">Installiert, aber nicht aktiv{{ $enabled ? '' : ' · Autostart aus' }}</div>
</div>
@endif
</div>
<div style="display:flex;gap:8px">
@if($running)
<button wire:click="disable" wire:loading.attr="disabled" wire:target="disable"
class="mbx-act-danger"
onclick="return confirm('ClamAV wirklich stoppen und deaktivieren?')">
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="2" stroke="currentColor" stroke-width="1.3"/></svg>
<span wire:loading.remove wire:target="disable">Deaktivieren</span>
<span wire:loading wire:target="disable">Bitte warten…</span>
</button>
@else
<button wire:click="enable" wire:loading.attr="disabled" wire:target="enable"
class="mbx-btn-primary">
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><path d="M7 2v10M2 7h10" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
<span wire:loading.remove wire:target="enable">Aktivieren &amp; starten</span>
<span wire:loading wire:target="enable">Wird gestartet…</span>
</button>
@endif
</div>
</div>
{{-- Signaturen --}}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="padding:10px 14px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:3px">Signatur-Datenbank</div>
<div style="font-size:13px;font-weight:500;color:var(--mw-t1)">{{ $dbDate }}</div>
@if($dbVersion !== '—')<div style="font-size:11.5px;color:var(--mw-t4);margin-top:1px">Version {{ $dbVersion }}</div>@endif
</div>
<div style="padding:10px 14px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px;display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:3px">Signaturen aktualisieren</div>
<div style="font-size:12px;color:var(--mw-t3)">Läuft automatisch täglich</div>
</div>
<button wire:click="updateDb" wire:loading.attr="disabled" wire:target="updateDb" class="mbx-act-btn" style="flex-shrink:0">
<svg width="11" height="11" viewBox="0 0 14 14" fill="none" wire:loading.class="animate-spin" wire:target="updateDb"><path d="M12 7A5 5 0 1 1 7 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><path d="M7 2l2-2v4H5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span wire:loading.remove wire:target="updateDb">Jetzt</span>
<span wire:loading wire:target="updateDb"></span>
</button>
</div>
</div>
</div>
@endif
</div>
</div>
{{-- Info-Box --}}
<div class="mbx-section">
<div class="mbx-domain-head">
<div class="mbx-domain-info">
<span class="mbx-badge-mute">Info</span>
</div>
</div>
<div style="padding:16px 18px;display:flex;flex-direction:column;gap:10px">
<div style="font-size:12.5px;color:var(--mw-t3);line-height:1.6">
ClamAV ist ein Open-Source-Virenscanner der in Kombination mit Rspamd eingehende E-Mails auf Schadsoftware prüft.
Infizierte Anhänge werden automatisch abgewiesen oder in Quarantäne verschoben.
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:4px">
<div style="padding:8px 12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:7px">
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:2px">Empfohlen bei</div>
<div style="font-size:12px;color:var(--mw-t2)"> 2 GB RAM</div>
</div>
<div style="padding:8px 12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:7px">
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:2px">RAM-Verbrauch</div>
<div style="font-size:12px;color:var(--mw-t2)">~500700 MB</div>
</div>
<div style="padding:8px 12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:7px">
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:2px">Integration</div>
<div style="font-size:12px;color:var(--mw-t2)">Rspamd Milter</div>
</div>
<div style="padding:8px 12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:7px">
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:2px">Signaturen</div>
<div style="font-size:12px;color:var(--mw-t2)">Täglich automatisch</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -60,6 +60,7 @@ Route::middleware(['auth.user', 'require2fa'])->name('ui.')->group(function () {
Route::get('/rspamd', \App\Livewire\Ui\Security\RspamdForm::class)->name('rspamd'); Route::get('/rspamd', \App\Livewire\Ui\Security\RspamdForm::class)->name('rspamd');
Route::get('/tls-ciphers', \App\Livewire\Ui\Security\TlsCiphersForm::class)->name('tls'); Route::get('/tls-ciphers', \App\Livewire\Ui\Security\TlsCiphersForm::class)->name('tls');
Route::get('/audit-logs', \App\Livewire\Ui\Security\AuditLogsTable::class)->name('audit'); Route::get('/audit-logs', \App\Livewire\Ui\Security\AuditLogsTable::class)->name('audit');
Route::get('/clamav', \App\Livewire\Ui\Security\ClamavManager::class)->name('clamav');
}); });
#DOMAIN ROUTES #DOMAIN ROUTES

31
scripts/mailwolt-clamav Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Wrapper für ClamAV-Verwaltung via MailWolt UI (läuft als root via sudo)
set -euo pipefail
CMD="${1:-status}"
case "$CMD" in
enable)
systemctl enable clamav-daemon
systemctl start clamav-daemon
echo "ClamAV aktiviert und gestartet."
;;
disable)
systemctl stop clamav-daemon || true
systemctl disable clamav-daemon
echo "ClamAV gestoppt und deaktiviert."
;;
freshclam)
systemctl stop clamav-freshclam 2>/dev/null || true
freshclam --quiet
systemctl start clamav-freshclam 2>/dev/null || true
echo "Signaturen aktualisiert."
;;
status)
systemctl is-active clamav-daemon 2>/dev/null && echo "active" || echo "inactive"
;;
*)
echo "Usage: mailwolt-clamav {enable|disable|freshclam|status}" >&2
exit 1
;;
esac

View File

@ -183,10 +183,12 @@ ensure_system(){
_sbin_install "${APP_DIR}/scripts/mailwolt-apply-domains" /usr/local/sbin/mailwolt-apply-domains _sbin_install "${APP_DIR}/scripts/mailwolt-apply-domains" /usr/local/sbin/mailwolt-apply-domains
_sbin_install "${APP_DIR}/scripts/mailwolt-fetch-tags" /usr/local/sbin/mailwolt-fetch-tags _sbin_install "${APP_DIR}/scripts/mailwolt-fetch-tags" /usr/local/sbin/mailwolt-fetch-tags
_sbin_install "${APP_DIR}/scripts/update.sh" /usr/local/sbin/mailwolt-update _sbin_install "${APP_DIR}/scripts/update.sh" /usr/local/sbin/mailwolt-update
_sbin_install "${APP_DIR}/scripts/mailwolt-clamav" /usr/local/sbin/mailwolt-clamav
# Sudoers-Regeln nachrüsten # Sudoers-Regeln nachrüsten
for f in /etc/sudoers.d/mailwolt-certbot /etc/sudoers.d/mailwolt-dkim; do for f in /etc/sudoers.d/mailwolt-certbot /etc/sudoers.d/mailwolt-dkim; do
_sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-fetch-tags' _sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-fetch-tags'
_sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update --check-only' _sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update --check-only'
_sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-clamav'
_sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/*/fullchain.pem' _sudoers_add "$f" 'www-data ALL=(root) NOPASSWD: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/*/fullchain.pem'
done done
# Laravel Scheduler cron (schedule:run muss jede Minute laufen) # Laravel Scheduler cron (schedule:run muss jede Minute laufen)