162 lines
12 KiB
PHP
162 lines
12 KiB
PHP
<?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>
|