Files
2026-11-05 21:23:54 +00:00

162 lines
12 KiB
PHP
Raw Permalink 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
declare(strict_types=1);
if (($staff['role'] ?? '') !== 'admin') {
http_response_code(403);
echo '<p>Keine Berechtigung.</p>';
return;
}
$pdo = get_db();
vt_ensure_schema();
$message = '';
$messageType = 'success';
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
verify_csrf();
$action = (string) ($_POST['action'] ?? '');
$id = (int) ($_POST['id'] ?? 0);
if ($action === 'save') {
$keyIn = trim((string) ($_POST['key'] ?? ''));
if ($keyIn !== '' && !preg_match('/^[a-f0-9]{64}$/i', $keyIn)) {
$message = 'Der VirusTotal-Schlüssel besteht aus 64 Zeichen (0–9, a–f).';
$messageType = 'error';
} else {
if ($keyIn !== '') plugin_set_setting(VT, 'key_enc', encrypt_secret($keyIn));
if (!empty($_POST['clear_key'])) plugin_set_setting(VT, 'key_enc', '');
plugin_set_setting(VT, 'threshold', (string) max(1, min(30, (int) ($_POST['threshold'] ?? 3))));
plugin_set_setting(VT, 'unknown', ($_POST['unknown'] ?? 'hold') === 'allow' ? 'allow' : 'hold');
plugin_set_setting(VT, 'on_error', ($_POST['on_error'] ?? 'hold') === 'allow' ? 'allow' : 'hold');
plugin_set_setting(VT, 'upload', !empty($_POST['upload']) ? '1' : '0');
plugin_set_setting(VT, 'notify', !empty($_POST['notify']) ? '1' : '0');
audit_log((int) $staff['id'], 'VirusTotal: Einstellungen geändert' . (!empty($_POST['upload']) ? ' (Upload unbekannter Dateien an)' : ''));
$message = 'Gespeichert.';
}
} elseif ($action === 'test') {
$t = vt_test_key(vt_config()['key'], vt_config()['threshold']);
$message = $t['message'];
$messageType = $t['ok'] ? 'success' : 'error';
} elseif ($action === 'release' && $id > 0) {
$ok = vt_release_manual($pdo, $id);
if ($ok) audit_log((int) $staff['id'], 'VirusTotal: Anhang #' . $id . ' von Hand freigegeben');
$message = $ok ? 'Die Datei ist freigegeben.' : 'Die Datei ließ sich nicht freigeben.';
$messageType = $ok ? 'success' : 'error';
} elseif ($action === 'delete' && $id > 0) {
$ok = vt_delete($pdo, $id);
if ($ok) audit_log((int) $staff['id'], 'VirusTotal: Anhang #' . $id . ' gelöscht');
$message = $ok ? 'Die Datei ist gelöscht.' : 'Die Datei ließ sich nicht löschen.';
$messageType = $ok ? 'success' : 'error';
} elseif ($action === 'recheck' && $id > 0) {
$ok = vt_recheck($pdo, $id);
$message = $ok ? 'Die Datei wird beim nächsten Lauf noch einmal geprüft.' : 'Das ging nicht.';
$messageType = $ok ? 'success' : 'error';
}
}
$cfg = vt_config();
$stats = vt_stats($pdo);
$used = vt_day_used();
$lastError = (string) plugin_setting(VT, 'last_error', '');
$lastRun = (string) plugin_setting(VT, 'last_run', '');
$held = $pdo->query("SELECT f.*, t.ticket_number FROM plugin_virustotal_files f LEFT JOIN tickets t ON t.id = f.ticket_id WHERE f.status IN ('pending','analyzing','unknown','error','bad') ORDER BY (CASE f.status WHEN 'bad' THEN 0 WHEN 'unknown' THEN 1 WHEN 'error' THEN 2 ELSE 3 END), f.id DESC LIMIT 100")->fetchAll();
$recent = $pdo->query("SELECT * FROM plugin_virustotal_files WHERE status IN ('clean','released','deleted') ORDER BY id DESC LIMIT 30")->fetchAll();
$labels = ['pending' => ['Wartet auf VirusTotal', '#b45309'], 'analyzing' => ['Wird bei VirusTotal analysiert', '#b45309'], 'unknown' => ['Unbekannt (zurückgehalten)', '#b45309'], 'error' => ['Fehler (zurückgehalten)', '#b91c1c'],
'bad' => ['Gesperrt (Schadsoftware)', '#b91c1c'], 'clean' => ['Sauber, freigegeben', '#15803d'], 'released' => ['Von Hand freigegeben', '#15803d'], 'deleted' => ['Gelöscht', '#64748b']];
$mb = fn($b) => $b >= 1048576 ? round($b / 1048576, 1) . ' MB' : max(1, (int) round($b / 1024)) . ' KB';
?>
<h1>VirusTotal</h1>
<?php if ($message !== ''): ?><div class="flash <?= $messageType === 'error' ? 'error' : 'success' ?>" role="status"><?= e($message) ?></div><?php endif; ?>
<section class="card" style="max-width:960px">
<h3>Zustand</h3>
<?php if ($cfg['key'] === ''): ?>
<p><strong style="color:#b45309">Noch kein Schlüssel.</strong> Ohne VirusTotal-Schlüssel werden Anhänge <strong>nicht</strong> zurückgehalten und nicht geprüft. Einen kostenlosen Schlüssel gibt es nach der Anmeldung bei virustotal.com im Profil unter „API Key“.</p>
<?php else: ?>
<p><strong style="color:#157a45">✓ Aktiv.</strong> Neue Anhänge sind gesperrt, bis VirusTotal sie geprüft hat.</p>
<?php endif; ?>
<p style="font-size:.9rem">
<strong><?= (int) ($stats['pending'] + $stats['analyzing']) ?></strong> warten ·
<strong style="color:#b91c1c"><?= (int) $stats['bad'] ?></strong> gesperrt ·
<strong style="color:#b45309"><?= (int) $stats['unknown'] ?></strong> unbekannt ·
<strong><?= (int) $stats['error'] ?></strong> mit Fehler ·
<strong><?= (int) ($stats['clean'] + $stats['released']) ?></strong> freigegeben
</p>
<p class="muted" style="font-size:.85rem">Abfragen heute: <?= (int) $used ?> von <?= (int) VT_DAY_LIMIT ?> (der kostenlose Schlüssel erlaubt 4 pro Minute und 500 pro Tag). Letzter Lauf: <?= $lastRun !== '' ? e(format_dt($lastRun)) : 'noch nicht' ?>.</p>
<?php if ($lastError !== ''): ?><p style="color:#b91c1c;font-size:.88rem"><?= e($lastError) ?></p><?php endif; ?>
<p class="muted" style="font-size:.85rem">Vorgehen: Jeder neue Anhang wird sofort zurückgehalten (nicht herunterladbar). Dann fragt das Plugin bei VirusTotal nur den <strong>Fingerabdruck (SHA-256)</strong> der Datei ab. Ist die Datei bekannt und sauber, wird sie freigegeben; melden mindestens <?= (int) $cfg['threshold'] ?> Scanner Schadsoftware, bleibt sie gesperrt. Anhänge, die schon vor dem Einschalten da waren, bleiben unberührt.</p>
</section>
<form method="post" class="card" style="max-width:960px;margin-top:16px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save">
<h3>Einstellungen</h3>
<label for="key">VirusTotal-Schlüssel (API Key)</label>
<input type="password" id="key" name="key" value="" placeholder="<?= $cfg['key'] !== '' ? '(unverändert)' : '64 Zeichen' ?>" autocomplete="new-password">
<?php if ($cfg['key'] !== ''): ?><label style="display:flex;gap:8px;align-items:center;font-weight:400;margin-top:6px"><input type="checkbox" name="clear_key" value="1" style="width:auto"> Schlüssel entfernen (Anhänge werden dann nicht mehr geprüft)</label><?php endif; ?>
<label for="threshold">Sperren ab so vielen Scannern, die Schadsoftware melden</label>
<input type="number" id="threshold" name="threshold" min="1" max="30" value="<?= (int) $cfg['threshold'] ?>" style="max-width:120px">
<label for="unknown">Dateien, die VirusTotal nicht kennt</label>
<select id="unknown" name="unknown" style="max-width:520px">
<option value="hold" <?= $cfg['unknown'] === 'hold' ? 'selected' : '' ?>>Zurückhalten, bis ein Administrator sie prüft und freigibt (sicher)</option>
<option value="allow" <?= $cfg['unknown'] === 'allow' ? 'selected' : '' ?>>Freigeben (bequem, aber neue Schadsoftware kennt VirusTotal oft noch nicht)</option>
</select>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin-top:12px"><input type="checkbox" name="upload" value="1" style="width:auto;margin-top:3px" <?= $cfg['upload'] ? 'checked' : '' ?>> <span><strong>Unbekannte Dateien zur Analyse bei VirusTotal hochladen (bis 32 MB).</strong> <span style="color:#b91c1c">Achtung Datenschutz:</span> Hochgeladene Dateien werden bei VirusTotal gespeichert und stehen Sicherheitsfirmen und zahlenden Kunden von VirusTotal zur Verfügung. Ticket-Anhänge können persönliche Inhalte enthalten. Ausgeschaltet verlässt <strong>keine Datei</strong> den Server, nur der Fingerabdruck wird abgefragt.</span></label>
<label for="on_error" style="margin-top:12px">Wenn VirusTotal nach mehreren Versuchen nicht erreichbar ist</label>
<select id="on_error" name="on_error" style="max-width:520px">
<option value="hold" <?= $cfg['on_error'] === 'hold' ? 'selected' : '' ?>>Zurückhalten (sicher)</option>
<option value="allow" <?= $cfg['on_error'] === 'allow' ? 'selected' : '' ?>>Freigeben</option>
</select>
<label style="display:flex;gap:8px;align-items:center;font-weight:400;margin-top:12px"><input type="checkbox" name="notify" value="1" style="width:auto" <?= $cfg['notify'] ? 'checked' : '' ?>> Mail an den Administrator, wenn ein Anhang gesperrt wird</label>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
<form method="post" class="card" style="max-width:960px;margin-top:16px">
<?= csrf_field() ?><input type="hidden" name="action" value="test">
<h3>Schlüssel testen</h3>
<p class="muted" style="font-size:.88rem">Fragt bei VirusTotal den Fingerabdruck der Standard-Testdatei (EICAR) ab. Es wird nichts hochgeladen. Vorher speichern.</p>
<button type="submit" class="btn secondary">Schlüssel testen</button>
</form>
<section class="card" style="margin-top:16px">
<h3>Zurückgehaltene und gesperrte Anhänge</h3>
<?php if (!$held): ?>
<p class="muted">Nichts zurückgehalten.</p>
<?php else: ?>
<div style="overflow-x:auto"><table style="width:100%">
<thead><tr><th>Datei</th><th>Ticket</th><th>Größe</th><th>Zustand</th><th>Hinweis</th><th></th></tr></thead>
<tbody>
<?php foreach ($held as $r): [$label, $color] = $labels[$r['status']] ?? [$r['status'], '#64748b']; ?>
<tr>
<td><?= e($r['original_name']) ?><div class="muted" style="font-size:.72rem;word-break:break-all"><?= e((string) $r['sha256']) ?></div></td>
<td><?php if (!empty($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><?= e($mb((int) $r['filesize'])) ?></td>
<td><strong style="color:<?= e($color) ?>"><?= e($label) ?></strong></td>
<td style="max-width:300px;font-size:.82rem"><?= e((string) $r['detail']) ?></td>
<td style="white-space:nowrap">
<?php foreach ([['release', 'Freigeben', 'Diese Datei freigeben? Sie wird für Mitarbeiter herunterladbar.'], ['recheck', 'Erneut prüfen', ''], ['delete', 'Löschen', 'Diese Datei endgültig löschen?']] as [$act, $text, $ask]):
if ($act === 'recheck' && !in_array($r['status'], ['unknown', 'error'], true)) continue; ?>
<form method="post" style="display:inline"<?= $ask !== '' ? ' data-confirm="' . e($ask) . '" onsubmit="return confirm(this.dataset.confirm)"' : '' ?>><?= csrf_field() ?><input type="hidden" name="action" value="<?= e($act) ?>"><input type="hidden" name="id" value="<?= (int) $r['id'] ?>"><button class="btn secondary" style="padding:3px 10px;font-size:.8rem" type="submit"><?= e($text) ?></button></form>
<?php endforeach; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
<?php endif; ?>
</section>
<section class="card" style="margin-top:16px">
<h3>Zuletzt geprüft</h3>
<?php if (!$recent): ?>
<p class="muted">Noch nichts geprüft.</p>
<?php else: ?>
<div style="overflow-x:auto"><table style="width:100%">
<thead><tr><th>Datei</th><th>Zustand</th><th>Ergebnis</th><th>Wann</th></tr></thead>
<tbody>
<?php foreach ($recent as $r): [$label, $color] = $labels[$r['status']] ?? [$r['status'], '#64748b']; ?>
<tr><td><?= e($r['original_name']) ?></td><td><strong style="color:<?= e($color) ?>"><?= e($label) ?></strong></td><td class="muted" style="font-size:.82rem"><?= e((string) $r['detail']) ?></td><td><?= e(format_dt((string) ($r['checked_at'] ?: $r['created_at']))) ?></td></tr>
<?php endforeach; ?>
</tbody>
</table></div>
<?php endif; ?>
</section>