query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn(); $prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY sort_order LIMIT 1')->fetchColumn(); $pdo->prepare("INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES ('VT-1','Test','K','k@example.test',?,?,'open','web',?,?)")->execute([$dept, $prio, $now, $now]); $ticketId = (int) $pdo->lastInsertId(); $pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?, 'customer', 'Anhang anbei', 0, ?)")->execute([$ticketId, $now]); $threadId = (int) $pdo->lastInsertId(); $created = []; $attach = function (string $name, string $content) use ($pdo, $threadId, $uploads, &$created, $now): int { $stored = 'vttest-' . bin2hex(random_bytes(4)) . '.bin'; file_put_contents($uploads . '/' . $stored, $content); $created[] = $stored; $pdo->prepare('INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)')->execute([$threadId, $name, $stored, strlen($content), $now]); return (int) $pdo->lastInsertId(); }; $old = $attach('alt.bin', 'Bestand von früher'); // ── Installation ── $src = plugin_source_dir_or_skip('virustotal', 'plugin_virustotal'); $zipPath = sys_get_temp_dir() . '/vt-' . bin2hex(random_bytes(3)) . '.zip'; $zip = new ZipArchive(); $zip->open($zipPath, ZipArchive::CREATE); foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS)) as $f) { $zip->addFile($f->getPathname(), str_replace('\\', '/', substr($f->getPathname(), strlen($src) + 1))); } $zip->close(); $r = plugin_install_zip($pdo, $zipPath); $check($r['ok'] && $r['slug'] === 'virustotal', 'Plugin lässt sich installieren: ' . json_encode($r)); $check(!plugin_set_active($pdo, 'virustotal', true)['ok'], 'Aktivierung ohne Bestätigung der Berechtigungen wird abgelehnt'); $act = plugin_set_active($pdo, 'virustotal', true, ['hooks.tickets', 'hooks.cron', 'ui.menu', 'db.migrations']); $check($act['ok'], 'Plugin lässt sich aktivieren: ' . json_encode($act)); $state = &plugin_state(); $state['booted'] = false; plugins_boot(); $check(isset(plugin_state()['loaded']['virustotal']), 'Plugin wird geladen'); $check(count(plugin_admin_menu_items(['id' => 1, 'role' => 'admin'])) === 1 && count(plugin_admin_menu_items(['id' => 2, 'role' => 'agent'])) === 0, 'Menüpunkt nur für Administratoren im Adminportal'); $check((int) $pdo->query('SELECT COUNT(*) FROM plugin_virustotal_files')->fetchColumn() === 0, 'Tabelle wurde angelegt'); $check(vt_start_id($pdo) === $old, 'Die Startmarke steht beim Laden auf dem letzten vorhandenen Anhang'); // ── Ohne Schlüssel wird nichts zurückgehalten ── $noKey = $attach('ohne-schluessel.bin', 'ohne Schlüssel'); plugin_hook('ticket.created', ['ticketId' => $ticketId]); $stored = (string) $pdo->query("SELECT stored_name FROM ticket_attachments WHERE id = $noKey")->fetchColumn(); $check($stored !== 'gesperrt' && is_file($uploads . '/' . $stored) && vt_tick($pdo) === 0, 'Ohne Schlüssel bleibt der Anhang unberührt (sonst bliebe er für immer gesperrt)'); // ── VirusTotal nachgebaut ── $vt = ['files' => [], 'analyses' => [], 'calls' => [], 'uploads' => [], 'default' => [404, '{}'], 'upload' => [200, json_encode(['data' => ['type' => 'analysis', 'id' => 'ana-1']])]]; $stats = fn(int $mal, int $harm = 60, int $und = 10, int $susp = 0) => json_encode(['data' => ['attributes' => ['last_analysis_stats' => ['malicious' => $mal, 'suspicious' => $susp, 'undetected' => $und, 'harmless' => $harm]]]]); $GLOBALS['virustotal_http'] = function (string $method, string $url, string $key, array $opts) use (&$vt): array { $vt['calls'][] = "$method " . substr($url, strlen(VT_API) + 1); if ($key !== 'a' . str_repeat('1', 63)) return ['status' => 401, 'body' => '{}']; $path = substr($url, strlen(VT_API) + 1); if ($method === 'POST' && $path === 'files') { $vt['uploads'][] = $opts; return ['status' => $vt['upload'][0], 'body' => $vt['upload'][1]]; } if (str_starts_with($path, 'files/')) { $e = $vt['files'][substr($path, 6)] ?? $vt['default']; return ['status' => $e[0], 'body' => $e[1]]; } if (str_starts_with($path, 'analyses/')) { $e = $vt['analyses'][rawurldecode(substr($path, 9))] ?? [404, '{}']; return ['status' => $e[0], 'body' => $e[1]]; } return ['status' => 404, 'body' => '{}']; }; $key = 'a' . str_repeat('1', 63); plugin_set_setting('virustotal', 'key_enc', encrypt_secret($key)); $check(vt_config()['key'] === $key && !str_contains((string) get_setting('plugin.virustotal.key_enc', ''), $key), 'Schlüssel wird verschlüsselt gespeichert und gelesen'); $tick = function () use ($pdo) { plugin_set_setting('virustotal', 'tick_at', '0'); return vt_tick($pdo); }; $rowOf = fn(int $attId) => $pdo->query("SELECT * FROM plugin_virustotal_files WHERE attachment_id = $attId")->fetch(); $attOf = fn(int $attId) => $pdo->query("SELECT * FROM ticket_attachments WHERE id = $attId")->fetch(); // ── Der vorhandene und der schlüssellose Anhang werden jetzt (der schlüssellose nachträglich) erfasst, der alte nicht ── $hold = vt_unseen($pdo); $check(count($hold) === 1 && (int) $hold[0]['id'] === $noKey, 'Nachträglich erfasst wird nur, was nach dem Einschalten kam (nicht der Bestand)'); vt_hold($pdo, $hold[0]); $sha = hash('sha256', 'ohne Schlüssel'); $vt['files'][$sha] = [200, $stats(0)]; $check($tick() === 1 && $rowOf($noKey)['status'] === 'clean', 'Der nachträglich zurückgehaltene Anhang wird geprüft und freigegeben'); // ── Sauber: zurückhalten, dann freigeben ── $a1 = $attach('sauber.pdf', 'sauberer Inhalt'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $att = $attOf($a1); $row = $rowOf($a1); $check($att['stored_name'] === 'gesperrt' && str_starts_with($att['original_name'], '[IN PRÜFUNG]') && !is_file($uploads . '/' . $row['stored_name']) && is_file(vt_hold_dir() . '/' . $row['stored_name']), 'Neuer Anhang ist sofort gesperrt, umbenannt und aus dem Download-Ordner entfernt'); $check($row['status'] === 'pending' && $row['sha256'] === hash('sha256', 'sauberer Inhalt'), 'Eintrag wartet und enthält den SHA-256-Fingerabdruck'); $notes = (string) $pdo->query("SELECT GROUP_CONCAT(message, ' | ') FROM ticket_threads WHERE ticket_id = $ticketId AND author_type = 'system'")->fetchColumn(); $check(str_contains($notes, 'wird bei VirusTotal geprüft') && str_contains($notes, 'sauber.pdf'), 'Im Ticket steht ein Hinweis „wird geprüft“'); $vt['calls'] = []; $vt['files'][hash('sha256', 'sauberer Inhalt')] = [200, $stats(0)]; $check($tick() === 1, 'Ein Lauf fragt den Fingerabdruck ab'); $check($vt['calls'] === ['GET files/' . hash('sha256', 'sauberer Inhalt')], 'Abgefragt wird nur der Fingerabdruck, keine Datei hochgeladen: ' . json_encode($vt['calls'])); $att = $attOf($a1); $row = $rowOf($a1); $check($row['status'] === 'clean' && $att['stored_name'] === $row['stored_name'] && $att['original_name'] === 'sauber.pdf' && is_file($uploads . '/' . $row['stored_name']) && !is_file(vt_hold_dir() . '/' . $row['stored_name']), 'Saubere Datei ist freigegeben: Name und Datei zurück, Download wieder möglich'); $check((int) $row['engines'] === 70 && (int) $row['malicious'] === 0, 'Anzahl der Scanner wird vermerkt'); // ── Schadsoftware ── $a2 = $attach('rechnung.exe', 'boeser Inhalt'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'boeser Inhalt')] = [200, $stats(41, 20, 9)]; $tick(); $att = $attOf($a2); $row = $rowOf($a2); $check($row['status'] === 'bad' && $att['stored_name'] === 'gesperrt' && str_starts_with($att['original_name'], '[GESPERRT]') && is_file(vt_hold_dir() . '/' . $row['stored_name']) && !is_file($uploads . '/' . $row['stored_name']), 'Schadsoftware bleibt gesperrt und in der Quarantäne'); $check(str_contains((string) $row['detail'], '41 von 70'), 'Meldung nennt die Zahl der Scanner: ' . $row['detail']); $tags = (string) $pdo->query("SELECT GROUP_CONCAT(tg.name) FROM ticket_tags tt JOIN tags tg ON tg.id = tt.tag_id WHERE tt.ticket_id = $ticketId")->fetchColumn(); $check(str_contains($tags, 'Virus-Verdacht'), 'Das Ticket bekommt den Tag „Virus-Verdacht“'); $notes = (string) $pdo->query("SELECT GROUP_CONCAT(message, ' | ') FROM ticket_threads WHERE ticket_id = $ticketId AND author_type = 'system'")->fetchColumn(); $check(str_contains($notes, 'als gefährlich eingestuft'), 'Im Ticket steht ein Hinweis auf die Sperre'); // ── Schwelle ── $a3 = $attach('grenzfall.bin', 'grenzfall'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'grenzfall')] = [200, $stats(2)]; $tick(); $check($rowOf($a3)['status'] === 'clean', 'Zwei Funde bei Schwelle 3 sind kein Grund zur Sperre (Fehlalarme)'); $a4 = $attach('grenzfall2.bin', 'grenzfall2'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); plugin_set_setting('virustotal', 'threshold', '2'); $vt['files'][hash('sha256', 'grenzfall2')] = [200, $stats(2)]; $tick(); $check($rowOf($a4)['status'] === 'bad', 'Bei Schwelle 2 werden zwei Funde gesperrt'); plugin_set_setting('virustotal', 'threshold', '3'); // ── Unbekannte Dateien ── $a5 = $attach('neu.bin', 'ganz neu'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['calls'] = []; $tick(); $row = $rowOf($a5); $check($row['status'] === 'unknown' && $attOf($a5)['stored_name'] === 'gesperrt' && $vt['uploads'] === [], 'Unbekannte Datei bleibt zurückgehalten und wird nicht hochgeladen (Standard)'); // Von Hand freigeben $check(vt_release_manual($pdo, (int) $row['id']) && $rowOf($a5)['status'] === 'released' && $attOf($a5)['original_name'] === 'neu.bin' && is_file($uploads . '/' . $row['stored_name']), 'Von Hand freigegeben: Datei und Name sind zurück'); // Erneut prüfen $a6 = $attach('neu2.bin', 'ganz neu 2'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $tick(); $check($rowOf($a6)['status'] === 'unknown', 'Zweite unbekannte Datei wird zurückgehalten'); $vt['files'][hash('sha256', 'ganz neu 2')] = [200, $stats(0)]; $check(vt_recheck($pdo, (int) $rowOf($a6)['id']) && $rowOf($a6)['status'] === 'pending', 'Erneut prüfen setzt auf „wartet“'); $tick(); $check($rowOf($a6)['status'] === 'clean', 'Nach der erneuten Prüfung (inzwischen bekannt) wird sie freigegeben'); // Freigeben per Einstellung plugin_set_setting('virustotal', 'unknown', 'allow'); $a7 = $attach('neu3.bin', 'ganz neu 3'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $tick(); $check($rowOf($a7)['status'] === 'released' && $attOf($a7)['stored_name'] !== 'gesperrt', 'Mit „Unbekannte freigeben“ wird sie ohne Warten freigegeben'); plugin_set_setting('virustotal', 'unknown', 'hold'); // Datei ohne Analyse (VirusTotal kennt den Hash, hat aber noch nichts ausgewertet) $a8 = $attach('leer.bin', 'ohne analyse'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'ohne analyse')] = [200, json_encode(['data' => ['attributes' => ['last_analysis_stats' => ['malicious' => 0, 'suspicious' => 0, 'undetected' => 0, 'harmless' => 0]]]])]; $tick(); $check($rowOf($a8)['status'] === 'unknown', 'Ein Treffer ohne Analyse-Ergebnis zählt nicht als sauber, sondern als unbekannt'); // ── Hochladen (ausdrücklich eingeschaltet) ── plugin_set_setting('virustotal', 'upload', '1'); $a9 = $attach('hochladen.bin', 'bitte analysieren'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['uploads'] = []; $tick(); $row = $rowOf($a9); $check($row['status'] === 'analyzing' && $row['analysis_id'] === 'ana-1' && count($vt['uploads']) === 1 && $vt['uploads'][0]['name'] === 'hochladen.bin' && str_starts_with(str_replace('\\', '/', $vt['uploads'][0]['file']), str_replace('\\', '/', vt_hold_dir())), 'Unbekannte Datei wird (eingeschaltet) hochgeladen und wartet auf die Analyse: ' . json_encode($row['status'])); $due = fn() => $pdo->exec("UPDATE plugin_virustotal_files SET next_try = '2000-01-01 00:00:00' WHERE status IN ('pending','analyzing')"); $vt['analyses']['ana-1'] = [200, json_encode(['data' => ['attributes' => ['status' => 'queued']]])]; $due(); $tick(); $check($rowOf($a9)['status'] === 'analyzing', 'Läuft die Analyse noch, wird weiter gewartet'); $vt['analyses']['ana-1'] = [200, json_encode(['data' => ['attributes' => ['status' => 'completed', 'stats' => ['malicious' => 0, 'suspicious' => 0, 'undetected' => 12, 'harmless' => 55]]]])]; $due(); $tick(); $check($rowOf($a9)['status'] === 'clean' && $attOf($a9)['original_name'] === 'hochladen.bin', 'Fertige Analyse ohne Funde: freigegeben'); $a10 = $attach('hochladen-boese.bin', 'bitte analysieren boese'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['upload'] = [200, json_encode(['data' => ['type' => 'analysis', 'id' => 'ana-2']])]; $tick(); $vt['analyses']['ana-2'] = [200, json_encode(['data' => ['attributes' => ['status' => 'completed', 'stats' => ['malicious' => 9, 'suspicious' => 0, 'undetected' => 12, 'harmless' => 40]]]])]; $due(); $tick(); $check($rowOf($a10)['status'] === 'bad', 'Fertige Analyse mit Funden: gesperrt'); // Zu groß für den Upload $a11 = $attach('riesig.bin', 'klein'); $pdo->prepare('UPDATE ticket_attachments SET filesize = ? WHERE id = ?')->execute([40 * 1048576, $a11]); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['uploads'] = []; $p = vt_hold_dir() . '/' . $rowOf($a11)['stored_name']; $fh = fopen($p, 'wb'); ftruncate($fh, VT_UPLOAD_MAX + 1024); fclose($fh); $tick(); $check($rowOf($a11)['status'] === 'unknown' && $vt['uploads'] === [] && str_contains((string) $rowOf($a11)['detail'], '32 MB'), 'Dateien über 32 MB werden nicht hochgeladen, sondern zurückgehalten'); plugin_set_setting('virustotal', 'upload', '0'); // ── Limits und Fehler ── // 4 pro Lauf $ids = []; for ($i = 0; $i < 6; $i++) { $ids[] = $attach("viele$i.bin", "viele $i"); $vt['files'][hash('sha256', "viele $i")] = [200, $stats(0)]; } plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $check($tick() === 4, 'Höchstens 4 Abfragen pro Lauf (Grenze des kostenlosen Schlüssels)'); $check($tick() === 2, 'Der nächste Lauf erledigt den Rest'); // Minutenlimit (429): Lauf endet, Eintrag bleibt $a12 = $attach('limit.bin', 'limit'); $a13 = $attach('limit2.bin', 'limit2'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'limit')] = [429, '{}']; $vt['calls'] = []; $tick(); $check($rowOf($a12)['status'] === 'pending' && count($vt['calls']) === 1 && strtotime((string) $rowOf($a12)['next_try']) > time() + 30, 'Bei 429 endet der Lauf und die Datei wird in einer Minute noch einmal versucht'); $vt['files'][hash('sha256', 'limit')] = [200, $stats(0)]; $vt['files'][hash('sha256', 'limit2')] = [200, $stats(0)]; $pdo->exec("UPDATE plugin_virustotal_files SET next_try = '2000-01-01 00:00:00'"); $tick(); $check($rowOf($a12)['status'] === 'clean' && $rowOf($a13)['status'] === 'clean', 'Danach klappt es'); // Schlüssel abgelehnt $a14 = $attach('schluessel.bin', 'schluessel'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); plugin_set_setting('virustotal', 'key_enc', encrypt_secret('b' . str_repeat('2', 63))); $tick(); $check($rowOf($a14)['status'] === 'pending' && str_contains((string) plugin_setting('virustotal', 'last_error', ''), 'abgelehnt'), 'Ein abgelehnter Schlüssel hält die Datei zurück und wird gemeldet'); plugin_set_setting('virustotal', 'key_enc', encrypt_secret($key)); plugin_set_setting('virustotal', 'last_error', ''); $vt['files'][hash('sha256', 'schluessel')] = [200, $stats(0)]; $tick(); $check($rowOf($a14)['status'] === 'clean', 'Mit dem richtigen Schlüssel geht es weiter'); // Fehler: Wiederholungen, dann zurückhalten $a15 = $attach('fehler.bin', 'fehler'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'fehler')] = [500, '{}']; for ($i = 0; $i < 6; $i++) { $pdo->exec("UPDATE plugin_virustotal_files SET next_try = '2000-01-01 00:00:00' WHERE status = 'pending'"); $tick(); if ($i === 0) $check($rowOf($a15)['status'] === 'pending' && (int) $rowOf($a15)['tries'] === 1, 'Ein Fehler wird später wiederholt'); } $check($rowOf($a15)['status'] === 'error' && $attOf($a15)['stored_name'] === 'gesperrt', 'Nach sechs Fehlern bleibt die Datei zurückgehalten (Einstellung „zurückhalten“)'); $a16 = $attach('fehler2.bin', 'fehler2'); plugin_set_setting('virustotal', 'on_error', 'allow'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'fehler2')] = [500, '{}']; for ($i = 0; $i < 6; $i++) { $pdo->exec("UPDATE plugin_virustotal_files SET next_try = '2000-01-01 00:00:00' WHERE status = 'pending'"); $tick(); } $check($rowOf($a16)['status'] === 'released', 'Mit „bei Fehler freigeben“ wird die Datei nach den Versuchen freigegeben'); plugin_set_setting('virustotal', 'on_error', 'hold'); // Tageslimit plugin_set_setting('virustotal', 'day', date('Y-m-d')); plugin_set_setting('virustotal', 'day_count', (string) VT_DAY_LIMIT); $a17 = $attach('tageslimit.bin', 'tageslimit'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'tageslimit')] = [200, $stats(0)]; $vt['calls'] = []; $check($tick() === 0 && $vt['calls'] === [] && $rowOf($a17)['status'] === 'pending' && str_contains((string) plugin_setting('virustotal', 'last_error', ''), 'Tageslimit'), 'Ist das Tageslimit erreicht, wird nicht mehr abgefragt (und es wird gemeldet)'); plugin_set_setting('virustotal', 'day_count', '0'); $check($tick() === 1 && $rowOf($a17)['status'] === 'clean', 'Am nächsten Tag geht es weiter'); // Zwei Läufe überlappen nicht $a18 = $attach('doppelt.bin', 'doppelt'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $vt['files'][hash('sha256', 'doppelt')] = [200, $stats(0)]; plugin_set_setting('virustotal', 'tick_at', '0'); $check(vt_tick($pdo) === 1 && vt_tick($pdo) === 0, 'Ein zweiter Lauf direkt danach tut nichts (keine Überlappung)'); // ── Dateien, die nicht zurückgehalten werden können ── $gone = $attach('weg.bin', 'weg'); @unlink($uploads . '/' . $attOf($gone)['stored_name']); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $check($rowOf($gone)['status'] === 'error' && str_contains((string) $rowOf($gone)['detail'], 'nicht gefunden'), 'Fehlt die Datei, steht ein Fehler im Eintrag (und es geht nichts kaputt)'); // ── Löschen ── $bad = $rowOf($a2); $check(vt_delete($pdo, (int) $bad['id']) && $rowOf($a2)['status'] === 'deleted' && !is_file(vt_hold_dir() . '/' . $bad['stored_name']) && str_starts_with($attOf($a2)['original_name'], '[GELÖSCHT]'), 'Löschen entfernt die Datei und vermerkt es'); $check(!vt_delete($pdo, (int) $rowOf($a1)['id']) && !vt_release_manual($pdo, (int) $rowOf($a1)['id']), 'Freigegebene Dateien lassen sich nicht noch einmal löschen oder freigeben'); // ── Schlüsseltest ── $vt['files'][VT_EICAR_SHA256] = [200, $stats(60, 5, 5)]; $t = vt_test_key($key, 3); $check($t['ok'] && str_contains($t['message'], 'EICAR') && str_contains($t['message'], '60 von 70'), 'Der Schlüsseltest erkennt EICAR bei VirusTotal'); $check(!vt_test_key('b' . str_repeat('2', 63), 3)['ok'] && !vt_test_key('', 3)['ok'], 'Falscher oder fehlender Schlüssel: Test schlägt fehl'); // ── Seite ── $render = function (array $staffRow, array $post = []) use ($pluginDir) { global $staff; $staff = $staffRow; $_SERVER['REQUEST_METHOD'] = $post ? 'POST' : 'GET'; $_POST = $post ? $post + ['csrf_token' => csrf_token()] : []; ob_start(); include $pluginDir . '/virustotal/pages/virustotal.php'; return (string) ob_get_clean(); }; $admin = ['id' => 1, 'role' => 'admin', 'name' => 'Admin']; $page = $render($admin); $check(str_contains($page, 'VirusTotal') && str_contains($page, 'Aktiv') && str_contains($page, 'Zurückgehaltene und gesperrte Anhänge') && !str_contains($page, $key), 'Seite zeigt den Zustand und nennt den Schlüssel nicht'); $denied = $render(['id' => 2, 'role' => 'agent', 'name' => 'A']); $check(str_contains($denied, 'Keine Berechtigung') && !str_contains($denied, 'Einstellungen'), 'Die Seite ist nur für Administratoren'); $page = $render($admin, ['action' => 'save', 'key' => 'zu-kurz', 'threshold' => '3', 'unknown' => 'hold']); $check(str_contains($page, '64 Zeichen') && vt_config()['key'] === $key, 'Ein ungültiger Schlüssel wird abgelehnt, der alte bleibt'); $page = $render($admin, ['action' => 'save', 'key' => '', 'threshold' => '99', 'unknown' => 'allow', 'on_error' => 'allow', 'upload' => '1', 'notify' => '1']); $check(vt_config()['threshold'] === 30 && vt_config()['unknown'] === 'allow' && vt_config()['upload'] === true && vt_config()['key'] === $key, 'Speichern setzt die Einstellungen (Schwelle begrenzt) und lässt den Schlüssel unverändert'); $check(str_contains((string) $pdo->query("SELECT GROUP_CONCAT(action, ' | ') FROM admin_audit_log WHERE action LIKE 'VirusTotal: Einstellungen%'")->fetchColumn(), 'Upload unbekannter Dateien an'), 'Das Einschalten des Uploads steht im Admin-Protokoll'); plugin_set_setting('virustotal', 'unknown', 'hold'); plugin_set_setting('virustotal', 'upload', '0'); plugin_set_setting('virustotal', 'threshold', '3'); $page = $render($admin, ['action' => 'test']); $check(str_contains($page, 'Der Schlüssel funktioniert'), 'Die Seite führt den Schlüsseltest aus'); $page = $render($admin, ['action' => 'release', 'id' => (string) $rowOf($a5)['id']]); $check(str_contains($page, 'ließ sich nicht freigeben'), 'Schon freigegebene Dateien lassen sich über die Seite nicht noch einmal freigeben'); $a19 = $attach('seite.bin', 'seite'); plugin_hook('ticket.replied', ['ticketId' => $ticketId]); $tick(); $page = $render($admin); $check(str_contains($page, 'seite.bin') && str_contains($page, 'Unbekannt (zurückgehalten)') && str_contains($page, 'value="release"'), 'Zurückgehaltene Dateien erscheinen mit Knöpfen'); $page = $render($admin, ['action' => 'release', 'id' => (string) $rowOf($a19)['id']]); $check(str_contains($page, 'Die Datei ist freigegeben') && $rowOf($a19)['status'] === 'released', 'Freigeben über die Seite'); $check(str_contains((string) $pdo->query("SELECT GROUP_CONCAT(action, ' | ') FROM admin_audit_log WHERE action LIKE 'VirusTotal: Anhang%'")->fetchColumn(), 'von Hand freigegeben'), 'Freigabe steht im Admin-Protokoll'); foreach ($created as $f) { @unlink($uploads . '/' . $f); @unlink(vt_hold_dir() . '/' . $f); } foreach (scandir($pluginDir) ?: [] as $entry) { if ($entry === '.' || $entry === '..') continue; is_dir($pluginDir . '/' . $entry) ? plugin_rmdir($pluginDir . '/' . $entry) : @unlink($pluginDir . '/' . $entry); } @rmdir($pluginDir); @unlink($zipPath); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "VirusTotal plugin checks passed.\n";