Files
deskhub/Plugins/plugins/virenscan/pages/virenscan.php
T
2026-10-24 20:15:16 +00:00

211 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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; ?>