211 lines
17 KiB
PHP
211 lines
17 KiB
PHP
<?php
|
||
// Seite des Virenscan-Plugins mit Reitern (Übersicht, Quarantäne, Geprüft, Einstellungen). Verfügbar: $staff, $pdo.
|
||
$isAdmin = ($staff['role'] ?? '') === 'admin';
|
||
if (!$isAdmin) { echo '<h1>Virenscan</h1><div class="flash error">Der Virenscan ist nur für Administratoren zugänglich.</div>'; return; }
|
||
$message = null;
|
||
$error = null;
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
verify_csrf();
|
||
if (!$isAdmin) {
|
||
$error = 'Änderungen sind Administratoren vorbehalten.';
|
||
} else {
|
||
$action = $_POST['action'] ?? '';
|
||
if ($action === 'save') {
|
||
$mode = in_array($_POST['mode'] ?? '', ['auto', 'clamdscan', 'clamscan', 'builtin'], true) ? $_POST['mode'] : 'auto';
|
||
$binary = trim((string) ($_POST['binary'] ?? ''));
|
||
if ($binary !== '' && (preg_match('/[\r\n\0]/', $binary) || !is_file($binary) || !is_executable($binary))) {
|
||
$error = 'Der angegebene Pfad ist keine ausführbare Datei.';
|
||
} else {
|
||
plugin_set_setting(VIRENSCAN, 'mode', $mode);
|
||
plugin_set_setting(VIRENSCAN, 'binary', $binary);
|
||
plugin_set_setting(VIRENSCAN, 'timeout', (string) max(5, min(300, (int) ($_POST['timeout'] ?? 60))));
|
||
plugin_set_setting(VIRENSCAN, 'max_mb', (string) max(1, min(500, (int) ($_POST['max_mb'] ?? 25))));
|
||
plugin_set_setting(VIRENSCAN, 'block_on_error', !empty($_POST['block_on_error']) ? '1' : '0');
|
||
plugin_set_setting(VIRENSCAN, 'notify', !empty($_POST['notify']) ? '1' : '0');
|
||
foreach (['analysis', 'block_macros', 'block_encrypted', 'block_unscannable'] as $k) plugin_set_setting(VIRENSCAN, $k, !empty($_POST[$k]) ? '1' : '0');
|
||
plugin_set_setting(VIRENSCAN, 'vt_threshold', (string) max(1, min(30, (int) ($_POST['vt_threshold'] ?? 3))));
|
||
$newKey = trim((string) ($_POST['vt_key'] ?? ''));
|
||
if (!empty($_POST['vt_clear'])) plugin_set_setting(VIRENSCAN, 'vt_key', '');
|
||
elseif ($newKey !== '') {
|
||
if (!preg_match('/^[a-f0-9]{64}$/i', $newKey)) $error = 'Der VirusTotal-Schlüssel besteht aus 64 Zeichen (0–9, a–f).';
|
||
else { plugin_set_setting(VIRENSCAN, 'vt_key', encrypt_secret($newKey)); plugin_set_setting(VIRENSCAN, 'vt_error', ''); }
|
||
}
|
||
if (!$error) $message = 'Einstellungen gespeichert.';
|
||
}
|
||
} elseif ($action === 'test') {
|
||
$tmp = tempnam(sys_get_temp_dir(), 'vsc');
|
||
file_put_contents($tmp, virenscan_eicar());
|
||
$res = virenscan_scan_file($tmp);
|
||
@unlink($tmp);
|
||
$message = $res['status'] === 'infected'
|
||
? 'Test bestanden: Die Standard-Testdatei (EICAR) wurde erkannt' . ($res['signature'] ? ' (' . $res['signature'] . ')' : '') . '.'
|
||
: null;
|
||
if ($message === null) $error = 'Test nicht bestanden: Die Testdatei wurde nicht erkannt (' . ($res['message'] ?? $res['status']) . ').';
|
||
} elseif ($action === 'backlog') {
|
||
plugin_set_setting(VIRENSCAN, 'start_id', '0');
|
||
$message = 'Der gesamte Bestand wird jetzt nach und nach geprüft (jeweils bis zu 25 Dateien alle paar Minuten).';
|
||
} elseif ($action === 'release') {
|
||
$message = virenscan_release($pdo, (int) ($_POST['id'] ?? 0)) ? 'Datei freigegeben. Sie ist im Ticket wieder herunterladbar.' : null;
|
||
if ($message === null) $error = 'Die Datei konnte nicht freigegeben werden.';
|
||
} elseif ($action === 'delete') {
|
||
$message = virenscan_delete($pdo, (int) ($_POST['id'] ?? 0)) ? 'Datei endgültig gelöscht.' : null;
|
||
if ($message === null) $error = 'Die Datei konnte nicht gelöscht werden.';
|
||
}
|
||
}
|
||
}
|
||
|
||
$cfg = virenscan_config();
|
||
$status = virenscan_status();
|
||
$stats = virenscan_stats($pdo);
|
||
$labels = ['clean' => 'Sauber', 'infected' => 'Verseucht', 'error' => 'Fehler', 'skipped' => 'Übersprungen', 'blocked' => 'Gesperrt (Fehler)', 'released' => 'Freigegeben', 'deleted' => 'Gelöscht'];
|
||
|
||
// Reiter: Übersicht, Quarantäne (mit Anzahl), Geprüft, Einstellungen
|
||
$tabs = ['uebersicht' => ['Übersicht', 0], 'quarantaene' => ['Quarantäne', (int) $stats['quarantined']], 'geprueft' => ['Geprüft', 0], 'einstellungen' => ['Einstellungen', 0]];
|
||
$tab = isset($tabs[$_GET['tab'] ?? '']) ? (string) $_GET['tab'] : 'uebersicht';
|
||
|
||
// Geprüft: Filter und Seiten
|
||
$filter = isset($labels[$_GET['status'] ?? '']) ? (string) $_GET['status'] : '';
|
||
$perPage = 50;
|
||
$pageNo = max(1, (int) ($_GET['seite'] ?? 1));
|
||
if ($tab === 'geprueft') {
|
||
$cnt = $pdo->prepare('SELECT COUNT(*) FROM plugin_virenscan_files' . ($filter !== '' ? ' WHERE status = ?' : ''));
|
||
$cnt->execute($filter !== '' ? [$filter] : []);
|
||
$totalChecked = (int) $cnt->fetchColumn();
|
||
$pages = max(1, (int) ceil($totalChecked / $perPage));
|
||
$pageNo = min($pageNo, $pages);
|
||
$q = $pdo->prepare('SELECT f.*, t.ticket_number FROM plugin_virenscan_files f LEFT JOIN tickets t ON t.id = f.ticket_id' . ($filter !== '' ? ' WHERE f.status = ?' : '') . ' ORDER BY f.id DESC LIMIT ' . $perPage . ' OFFSET ' . (($pageNo - 1) * $perPage));
|
||
$q->execute($filter !== '' ? [$filter] : []);
|
||
$checked = $q->fetchAll();
|
||
}
|
||
if ($tab === 'quarantaene') {
|
||
$quarantine = $pdo->query('SELECT f.*, t.ticket_number FROM plugin_virenscan_files f LEFT JOIN tickets t ON t.id = f.ticket_id WHERE f.quarantined = 1 ORDER BY f.id DESC LIMIT 200')->fetchAll();
|
||
}
|
||
?>
|
||
<h1>Virenscan</h1>
|
||
<?php if ($message): ?><div class="flash success"><?= e($message) ?></div><?php endif; ?>
|
||
<?php if ($error): ?><div class="flash error"><?= e($error) ?></div><?php endif; ?>
|
||
<?= virenscan_tabs($tab, $tabs) ?>
|
||
|
||
<?php if ($tab === 'uebersicht'): ?>
|
||
<div class="card" style="max-width:820px;margin-bottom:16px">
|
||
<h3>Zustand</h3>
|
||
<p>
|
||
<?php if ($cfg['analysis']): ?><strong style="color:#15803d">✓ Dateianalyse aktiv</strong><?php else: ?><strong style="color:#b45309">Dateianalyse ausgeschaltet</strong><?php endif; ?>
|
||
· <?php if ($status['ok']): ?><strong style="color:#15803d">ClamAV bereit</strong> (<?= e($status['mode']) ?>, <?= e((string) $status['version']) ?>)<?php else: ?><span class="muted">ClamAV nicht vorhanden (optional)</span><?php endif; ?>
|
||
· <?php if ($cfg['vt_key'] !== ''): ?><strong style="color:#15803d">✓ Hash-Abfrage aktiv</strong> (heute <?= plugin_setting(VIRENSCAN, 'vt_day', '') === date('Y-m-d') ? (int) plugin_setting(VIRENSCAN, 'vt_count', '0') : 0 ?> von 450 Abfragen)<?php else: ?><span class="muted">Hash-Abfrage aus (kein Schlüssel)</span><?php endif; ?>
|
||
</p>
|
||
<p class="muted" style="font-size:.88rem"><?= e($status['message']) ?></p>
|
||
<?php if ($cfg['vt_key'] !== '' && plugin_setting(VIRENSCAN, 'vt_error', '') !== ''): ?><p style="color:#b91c1c;font-size:.88rem">Hash-Abfrage: <?= e((string) plugin_setting(VIRENSCAN, 'vt_error', '')) ?></p><?php endif; ?>
|
||
<p style="font-size:.9rem">Geprüft: <strong><?= (int) $stats['clean'] ?></strong> sauber · <strong style="color:#b91c1c"><?= (int) $stats['infected'] ?></strong> verseucht · <strong><?= (int) $stats['error'] ?></strong> Fehler · <strong><?= (int) $stats['skipped'] ?></strong> übersprungen · in Quarantäne: <strong><?= (int) $stats['quarantined'] ?></strong></p>
|
||
<p class="muted" style="font-size:.82rem">Letzter Hintergrundlauf: <?= e(plugin_setting(VIRENSCAN, 'last_run', '') ?: 'noch nicht') ?></p>
|
||
<?php if ((int) $stats['quarantined'] > 0): ?><p><a class="btn" href="<?= e(virenscan_tab_url('quarantaene')) ?>"><?= (int) $stats['quarantined'] ?> Datei(en) in Quarantäne prüfen</a></p><?php endif; ?>
|
||
</div>
|
||
|
||
<div class="card" style="max-width:820px;margin-bottom:16px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
|
||
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="test"><button type="submit" class="btn secondary">Prüfung testen (EICAR-Testdatei)</button></form>
|
||
<form method="post" onsubmit="return confirm('Alle bisherigen Anhänge nachträglich prüfen? Das läuft im Hintergrund und kann etwas dauern.')"><?= csrf_field() ?><input type="hidden" name="action" value="backlog"><button type="submit" class="btn secondary">Bestand nachträglich prüfen</button></form>
|
||
<span class="muted" style="font-size:.82rem">Ohne diesen Knopf werden nur Anhänge geprüft, die nach dem Einschalten des Plugins eingehen.</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($tab === 'quarantaene'): ?>
|
||
<div class="card" style="margin-bottom:16px">
|
||
<h3>Quarantäne</h3>
|
||
<?php if (!$quarantine): ?><p class="muted">Keine gesperrten Dateien. Alles in Ordnung.</p><?php else: ?>
|
||
<table style="width:100%">
|
||
<thead><tr><th>Datei</th><th>Fund</th><th>Ticket</th><th>Wann</th><th></th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($quarantine as $q): ?>
|
||
<tr>
|
||
<td><?= e($q['original_name']) ?></td>
|
||
<td><?= e((string) ($q['signature'] ?: $q['message'])) ?></td>
|
||
<td><?php if ($q['ticket_number']): ?><a href="/staff/ticket_view.php?n=<?= e(urlencode((string) $q['ticket_number'])) ?>">#<?= e((string) $q['ticket_number']) ?></a><?php endif; ?></td>
|
||
<td class="muted"><?= e((string) $q['scanned_at']) ?></td>
|
||
<td style="white-space:nowrap">
|
||
<form method="post" style="display:inline" onsubmit="return confirm('Diese Datei wirklich freigeben? Sie war als verseucht erkannt.')"><?= csrf_field() ?><input type="hidden" name="action" value="release"><input type="hidden" name="id" value="<?= (int) $q['id'] ?>"><button class="btn secondary" type="submit">Freigeben</button></form>
|
||
<form method="post" style="display:inline" onsubmit="return confirm('Datei endgültig löschen?')"><?= csrf_field() ?><input type="hidden" name="action" value="delete"><input type="hidden" name="id" value="<?= (int) $q['id'] ?>"><button class="btn danger" type="submit">Löschen</button></form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($tab === 'geprueft'): ?>
|
||
<div class="card">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px">
|
||
<h3 style="margin:0">Geprüfte Dateien <span class="muted" style="font-weight:400;font-size:.85rem">(<?= (int) $totalChecked ?><?= $filter !== '' ? ' mit Ergebnis „' . e($labels[$filter]) . '“' : '' ?>)</span></h3>
|
||
<form method="get" style="display:flex;gap:6px;align-items:center">
|
||
<input type="hidden" name="p" value="<?= e(VIRENSCAN) ?>"><input type="hidden" name="page" value="virenscan"><input type="hidden" name="tab" value="geprueft">
|
||
<select name="status" onchange="this.form.submit()">
|
||
<option value="">Alle Ergebnisse</option>
|
||
<?php foreach ($labels as $k => $l): ?><option value="<?= e($k) ?>" <?= $filter === $k ? 'selected' : '' ?>><?= e($l) ?> (<?= (int) ($stats[$k] ?? 0) ?>)</option><?php endforeach; ?>
|
||
</select>
|
||
<noscript><button class="btn secondary" type="submit">Filtern</button></noscript>
|
||
</form>
|
||
</div>
|
||
<?php if (!$checked): ?><p class="muted">Keine Einträge.</p><?php else: ?>
|
||
<table style="width:100%"><thead><tr><th>Datei</th><th>Ergebnis</th><th>Hinweis</th><th>Ticket</th><th>Wann</th></tr></thead><tbody>
|
||
<?php foreach ($checked as $r): ?>
|
||
<tr>
|
||
<td><?= e($r['original_name']) ?></td>
|
||
<td><?= e($labels[$r['status']] ?? $r['status']) ?></td>
|
||
<td class="muted"><?= e((string) ($r['signature'] ?: $r['message'])) ?></td>
|
||
<td><?php if ($r['ticket_number']): ?><a href="/staff/ticket_view.php?n=<?= e(urlencode((string) $r['ticket_number'])) ?>">#<?= e((string) $r['ticket_number']) ?></a><?php endif; ?></td>
|
||
<td class="muted"><?= e((string) $r['scanned_at']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody></table>
|
||
<?php if ($pages > 1): ?>
|
||
<div style="display:flex;gap:6px;align-items:center;margin-top:12px;font-size:.88rem">
|
||
<?php if ($pageNo > 1): ?><a class="btn secondary" href="<?= e(virenscan_tab_url('geprueft', ['status' => $filter, 'seite' => $pageNo - 1])) ?>">← Neuere</a><?php endif; ?>
|
||
<span class="muted">Seite <?= (int) $pageNo ?> von <?= (int) $pages ?></span>
|
||
<?php if ($pageNo < $pages): ?><a class="btn secondary" href="<?= e(virenscan_tab_url('geprueft', ['status' => $filter, 'seite' => $pageNo + 1])) ?>">Ältere →</a><?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($tab === 'einstellungen'): ?>
|
||
<form method="post" class="card" style="max-width:820px;margin-bottom:16px">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save">
|
||
<h3>Einstellungen</h3>
|
||
<h4 style="margin:6px 0 4px">Dateianalyse (braucht nichts auf dem Server)</h4>
|
||
<p class="muted" style="font-size:.82rem">Sperrt Programme und Skripte (.exe, .js, .bat …), getarnte Dateien (zum Beispiel ein Programm als .pdf), doppelte Endungen, PDFs mit automatischem JavaScript, HTML/SVG mit Skripten und Archive mit solchen Dateien. Sie erkennt keine <em>neuen</em> Viren, die wie normale Dokumente aussehen; dafür dient die Hash-Abfrage unten oder ClamAV.</p>
|
||
<label style="display:block"><input type="checkbox" name="analysis" value="1" <?= $cfg['analysis'] ? 'checked' : '' ?>> Dateianalyse einschalten</label>
|
||
<label style="display:block"><input type="checkbox" name="block_macros" value="1" <?= $cfg['block_macros'] ? 'checked' : '' ?>> Office-Dateien mit Makros sperren (Word, Excel, PowerPoint)</label>
|
||
<label style="display:block"><input type="checkbox" name="block_encrypted" value="1" <?= $cfg['block_encrypted'] ? 'checked' : '' ?>> Passwortgeschützte ZIP-Dateien sperren (Inhalt lässt sich nicht prüfen)</label>
|
||
<label style="display:block"><input type="checkbox" name="block_unscannable" value="1" <?= $cfg['block_unscannable'] ? 'checked' : '' ?>> RAR-, 7z- und ähnliche Archive sperren (lassen sich nicht öffnen)</label>
|
||
|
||
<h4 style="margin:16px 0 4px">Hash-Abfrage bei VirusTotal (optional, kostenlos)</h4>
|
||
<p class="muted" style="font-size:.82rem">Es wird nur der Fingerabdruck (SHA-256) einer Datei gesendet, nie die Datei selbst oder Ticketinhalte. So werden bekannte Schadprogramme auch erkannt, wenn sie wie ein normales Dokument aussehen. Läuft im Hintergrund (höchstens 4 Abfragen pro Lauf, 450 pro Tag), die Sperre kann also einige Minuten nach dem Eingang kommen. Den kostenlosen Schlüssel gibt es nach der Anmeldung bei virustotal.com unter „API key“; die kostenlose Nutzung ist laut VirusTotal nicht für den gewerblichen Einsatz gedacht.</p>
|
||
<label for="vt_key">API-Schlüssel <?= $cfg['vt_key'] !== '' ? '– gespeichert, leer lassen zum Behalten' : '' ?></label>
|
||
<input type="password" id="vt_key" name="vt_key" value="" autocomplete="new-password">
|
||
<?php if ($cfg['vt_key'] !== ''): ?><label style="font-size:.82rem"><input type="checkbox" name="vt_clear" value="1"> Schlüssel entfernen und Abfrage ausschalten</label><?php endif; ?>
|
||
<div style="margin-top:8px"><label for="vt_threshold">Sperren ab so vielen Scannern, die Schadsoftware melden</label><input type="number" id="vt_threshold" name="vt_threshold" min="1" max="30" value="<?= (int) $cfg['vt_threshold'] ?>" style="max-width:120px"></div>
|
||
|
||
<h4 style="margin:16px 0 4px">ClamAV (optional, nur wenn schon auf dem Server vorhanden)</h4>
|
||
<label for="mode">Prüfprogramm</label>
|
||
<select id="mode" name="mode">
|
||
<option value="auto" <?= $cfg['mode'] === 'auto' ? 'selected' : '' ?>>Automatisch (clamdscan, sonst clamscan, sonst nur Dateianalyse)</option>
|
||
<option value="clamdscan" <?= $cfg['mode'] === 'clamdscan' ? 'selected' : '' ?>>clamdscan (ClamAV-Dienst, schnell)</option>
|
||
<option value="clamscan" <?= $cfg['mode'] === 'clamscan' ? 'selected' : '' ?>>clamscan (einzelnes Programm, langsamer)</option>
|
||
<option value="builtin" <?= $cfg['mode'] === 'builtin' ? 'selected' : '' ?>>Kein ClamAV verwenden</option>
|
||
</select>
|
||
<label for="binary" style="margin-top:10px">Eigener Pfad zum Programm (optional)</label>
|
||
<input type="text" id="binary" name="binary" value="<?= e($cfg['binary']) ?>" placeholder="/usr/bin/clamdscan">
|
||
<div style="display:flex;gap:14px;margin-top:10px;flex-wrap:wrap">
|
||
<div><label for="timeout">Zeitlimit je Datei (Sekunden)</label><input type="number" id="timeout" name="timeout" min="5" max="300" value="<?= (int) $cfg['timeout'] ?>" style="max-width:140px"></div>
|
||
<div><label for="max_mb">Größere Dateien nicht prüfen (MB)</label><input type="number" id="max_mb" name="max_mb" min="1" max="500" value="<?= (int) $cfg['max_mb'] ?>" style="max-width:140px"></div>
|
||
</div>
|
||
<label style="display:block;margin-top:10px"><input type="checkbox" name="block_on_error" value="1" <?= $cfg['block_on_error'] ? 'checked' : '' ?>> Datei sperren, wenn die Prüfung fehlschlägt (sicherer, kann aber auch saubere Dateien sperren)</label>
|
||
<label style="display:block"><input type="checkbox" name="notify" value="1" <?= $cfg['notify'] ? 'checked' : '' ?>> E-Mail an den Administrator, wenn eine Datei gesperrt wird</label>
|
||
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
|
||
</form>
|
||
<?php endif; ?>
|