query('SELECT sha256, analysis_id FROM plugin_virustotal_files LIMIT 1'); } catch (Throwable $e) { try { plugin_run_migrations(get_db(), VT); } catch (Throwable $e2) { error_log('virustotal schema: ' . $e2->getMessage()); } } } function vt_config(): array { $key = ''; $enc = (string) plugin_setting(VT, 'key_enc', ''); if ($enc !== '') { try { $key = decrypt_secret($enc); } catch (Throwable $e) { $key = ''; } } $unknown = (string) plugin_setting(VT, 'unknown', 'hold'); return [ 'key' => $key, 'threshold' => max(1, min(30, (int) plugin_setting(VT, 'threshold', '3'))), 'unknown' => in_array($unknown, ['hold', 'allow'], true) ? $unknown : 'hold', 'upload' => plugin_setting(VT, 'upload', '0') === '1', 'notify' => plugin_setting(VT, 'notify', '1') === '1', 'on_error' => plugin_setting(VT, 'on_error', 'hold') === 'allow' ? 'allow' : 'hold', ]; } function vt_uploads_dir(): string { return rtrim(str_replace('\\', '/', (string) (config()['uploads']['path'] ?? dirname(__DIR__, 2) . '/uploads')), '/'); } /** Ordner für zurückgehaltene und gesperrte Dateien (nicht von außen erreichbar). */ function vt_hold_dir(): string { $dir = vt_uploads_dir() . '/vt-pruefung'; if (!is_dir($dir)) { @mkdir($dir, 0750, true); @file_put_contents($dir . '/.htaccess', "Require all denied\nDeny from all\n"); @file_put_contents($dir . '/index.html', ''); } return $dir; } /** * HTTP-Aufruf zu VirusTotal (für Tests austauschbar über $GLOBALS['virustotal_http']). Nur Adressen der VirusTotal-Schnittstelle. * @param array{file?:string,name?:string} $opts bei POST: Datei für den Upload * @return array{status:int,body:string} */ function vt_http(string $method, string $path, string $key, array $opts = []): array { $url = VT_API . '/' . ltrim($path, '/'); if (isset($GLOBALS['virustotal_http']) && is_callable($GLOBALS['virustotal_http'])) return ($GLOBALS['virustotal_http'])($method, $url, $key, $opts); if (!function_exists('curl_init')) return ['status' => 0, 'body' => '']; $body = ''; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => $method === 'POST' ? 120 : 12, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_HTTPHEADER => ['x-apikey: ' . $key, 'Accept: application/json', 'User-Agent: DeskHub-VirusTotal'], CURLOPT_WRITEFUNCTION => function ($c, string $data) use (&$body): int { $body .= $data; return strlen($body) > 1048576 ? 0 : strlen($data); }, ]); if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => new CURLFile((string) ($opts['file'] ?? ''), 'application/octet-stream', (string) ($opts['name'] ?? 'datei.bin'))]); } curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return ['status' => $status, 'body' => $body]; } /** Wertet Scan-Zahlen aus. @return array{verdict:string,malicious:int,engines:int} verdict: bad, clean oder none (noch keine Analyse) */ function vt_verdict(array $stats, int $threshold): array { $malicious = (int) ($stats['malicious'] ?? 0); $engines = 0; foreach (['malicious', 'suspicious', 'undetected', 'harmless'] as $k) $engines += (int) ($stats[$k] ?? 0); if ($engines === 0) return ['verdict' => 'none', 'malicious' => 0, 'engines' => 0]; return ['verdict' => $malicious >= $threshold ? 'bad' : 'clean', 'malicious' => $malicious, 'engines' => $engines]; } /** Fragt den Fingerabdruck einer Datei ab. @return array{result:string,detail:?string,malicious:int,engines:int} result: bad, clean, unknown, none, limit, key, error */ function vt_lookup(string $sha256, string $key, int $threshold): array { $r = vt_http('GET', 'files/' . $sha256, $key); $base = ['detail' => null, 'malicious' => 0, 'engines' => 0]; if ($r['status'] === 404) return ['result' => 'unknown'] + $base; if ($r['status'] === 429) return ['result' => 'limit'] + $base; if ($r['status'] === 401 || $r['status'] === 403) return ['result' => 'key', 'detail' => 'Der VirusTotal-Schlüssel wird abgelehnt.'] + $base; if ($r['status'] !== 200) return ['result' => 'error', 'detail' => 'VirusTotal nicht erreichbar (Status ' . $r['status'] . ').'] + $base; $j = json_decode($r['body'], true); $stats = $j['data']['attributes']['last_analysis_stats'] ?? null; if (!is_array($stats)) return ['result' => 'error', 'detail' => 'Unerwartete Antwort von VirusTotal.'] + $base; $v = vt_verdict($stats, $threshold); $label = (string) ($j['data']['attributes']['popular_threat_classification']['suggested_threat_label'] ?? ''); $detail = $v['verdict'] === 'bad' ? 'VirusTotal: ' . $v['malicious'] . ' von ' . $v['engines'] . ' Scannern melden Schadsoftware' . ($label !== '' ? ' (' . mb_substr($label, 0, 60) . ')' : '') : ($v['verdict'] === 'clean' ? 'VirusTotal: ' . $v['malicious'] . ' von ' . $v['engines'] . ' Scannern melden etwas' : null); return ['result' => $v['verdict'], 'detail' => $detail, 'malicious' => $v['malicious'], 'engines' => $v['engines']]; } /** Lädt eine Datei zur Analyse hoch. @return array{ok:bool,id:?string,detail:?string} */ function vt_upload(string $path, string $name, string $key): array { $r = vt_http('POST', 'files', $key, ['file' => $path, 'name' => $name]); if ($r['status'] === 429) return ['ok' => false, 'id' => null, 'detail' => 'limit']; if ($r['status'] === 401 || $r['status'] === 403) return ['ok' => false, 'id' => null, 'detail' => 'Der VirusTotal-Schlüssel wird abgelehnt.']; $j = json_decode($r['body'], true); $id = (string) ($j['data']['id'] ?? ''); if ($r['status'] !== 200 || $id === '') return ['ok' => false, 'id' => null, 'detail' => 'Upload zu VirusTotal fehlgeschlagen (Status ' . $r['status'] . ').']; return ['ok' => true, 'id' => mb_substr($id, 0, 250), 'detail' => null]; } /** Fragt eine Analyse nach einem Upload ab. @return array{result:string,detail:?string,malicious:int,engines:int} result: bad, clean, wait, none, limit, key, error */ function vt_analysis(string $id, string $key, int $threshold): array { $r = vt_http('GET', 'analyses/' . rawurlencode($id), $key); $base = ['detail' => null, 'malicious' => 0, 'engines' => 0]; if ($r['status'] === 429) return ['result' => 'limit'] + $base; if ($r['status'] === 401 || $r['status'] === 403) return ['result' => 'key', 'detail' => 'Der VirusTotal-Schlüssel wird abgelehnt.'] + $base; if ($r['status'] !== 200) return ['result' => 'error', 'detail' => 'VirusTotal nicht erreichbar (Status ' . $r['status'] . ').'] + $base; $j = json_decode($r['body'], true); $attr = $j['data']['attributes'] ?? null; if (!is_array($attr)) return ['result' => 'error', 'detail' => 'Unerwartete Antwort von VirusTotal.'] + $base; if (($attr['status'] ?? '') !== 'completed') return ['result' => 'wait'] + $base; $v = vt_verdict((array) ($attr['stats'] ?? []), $threshold); return ['result' => $v['verdict'], 'detail' => $v['verdict'] === 'bad' ? 'VirusTotal: ' . $v['malicious'] . ' von ' . $v['engines'] . ' Scannern melden Schadsoftware' : null, 'malicious' => $v['malicious'], 'engines' => $v['engines']]; } // ── Anhänge zurückhalten ─────────────────────────────────────────────────── /** Ab welchem Anhang wird geprüft? Bestand vor dem Einschalten bleibt unberührt. */ function vt_start_id(PDO $pdo): int { $v = plugin_setting(VT, 'start_id', null); if ($v === null) { $v = (string) (int) $pdo->query('SELECT COALESCE(MAX(id), 0) FROM ticket_attachments')->fetchColumn(); plugin_set_setting(VT, 'start_id', $v); } return (int) $v; } /** Anhänge ohne Eintrag (noch nicht zurückgehalten), optional nur eines Tickets. */ function vt_unseen(PDO $pdo, int $limit = 25, ?int $ticketId = null): array { $sql = "SELECT a.id, a.original_name, a.stored_name, a.filesize, t.ticket_id FROM ticket_attachments a JOIN ticket_threads t ON t.id = a.thread_id WHERE a.id > ? AND a.stored_name <> 'gesperrt' AND a.id NOT IN (SELECT attachment_id FROM plugin_virustotal_files)"; $params = [vt_start_id($pdo)]; if ($ticketId !== null) { $sql .= ' AND t.ticket_id = ?'; $params[] = $ticketId; } $stmt = $pdo->prepare($sql . ' ORDER BY a.id LIMIT ' . max(1, $limit)); $stmt->execute($params); return $stmt->fetchAll(); } /** Hält einen Anhang zurück (Datei in den Prüfordner, Anhang nicht mehr herunterladbar) und legt den Eintrag an. @return string pending oder error */ function vt_hold(PDO $pdo, array $att): string { $now = db_now(); $from = vt_uploads_dir() . '/' . basename((string) $att['stored_name']); $insert = fn(string $status, ?string $sha, ?string $detail) => $pdo->prepare('INSERT INTO plugin_virustotal_files (attachment_id, ticket_id, original_name, stored_name, filesize, sha256, status, detail, tries, next_try, created_at) VALUES (?,?,?,?,?,?,?,?,0,?,?)') ->execute([(int) $att['id'], $att['ticket_id'] ?? null, mb_substr((string) $att['original_name'], 0, 255), mb_substr((string) $att['stored_name'], 0, 255), (int) ($att['filesize'] ?? 0), $sha, $status, $detail, $now, $now]); if (!is_file($from)) { $insert('error', null, 'Datei nicht gefunden.'); return 'error'; } $sha = hash_file('sha256', $from); $to = vt_hold_dir() . '/' . basename((string) $att['stored_name']); if (!@rename($from, $to) && (!@copy($from, $to) || !@unlink($from))) { // Ohne Zurückhalten gäbe es keine Sperre: ehrlich vermerken statt so zu tun $insert('error', $sha, 'Die Datei konnte nicht zurückgehalten werden (Schreibrechte im Ordner uploads prüfen).'); return 'error'; } $pdo->prepare("UPDATE ticket_attachments SET stored_name = 'gesperrt', original_name = ? WHERE id = ?")->execute([mb_substr('[IN PRÜFUNG] ' . $att['original_name'], 0, 255), (int) $att['id']]); $insert('pending', $sha, null); return 'pending'; } /** Hält die neuen Anhänge eines Tickets zurück und vermerkt es einmal im Ticket. @return int Anzahl */ function vt_hold_ticket(PDO $pdo, int $ticketId): int { $n = 0; $names = []; foreach (vt_unseen($pdo, 25, $ticketId) as $att) { if (vt_hold($pdo, $att) === 'pending') { $n++; $names[] = (string) $att['original_name']; } } if ($n > 0) { $pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?, 'system', ?, 1, ?)") ->execute([$ticketId, e('Anhang' . ($n > 1 ? 'e' : '') . ' wird bei VirusTotal geprüft und ist bis dahin gesperrt: ' . implode(', ', array_map(fn($x) => '„' . $x . '“', array_slice($names, 0, 5))) . ($n > 5 ? ' …' : '')), db_now()]); log_activity($pdo, $ticketId, null, 'VirusTotal: ' . $n . ' Anhang/Anhänge zur Prüfung zurückgehalten'); } return $n; } function vt_row(PDO $pdo, int $id): ?array { $s = $pdo->prepare('SELECT * FROM plugin_virustotal_files WHERE id = ?'); $s->execute([$id]); return $s->fetch() ?: null; } /** Gibt eine zurückgehaltene Datei frei (sauber, bewusst freigegeben oder zugelassen). */ function vt_release(PDO $pdo, array $row, string $status, string $detail): bool { $from = vt_hold_dir() . '/' . basename((string) $row['stored_name']); $to = vt_uploads_dir() . '/' . basename((string) $row['stored_name']); if (is_file($from) && !@rename($from, $to) && (!@copy($from, $to) || !@unlink($from))) return false; $pdo->prepare('UPDATE ticket_attachments SET stored_name = ?, original_name = ? WHERE id = ?')->execute([$row['stored_name'], $row['original_name'], (int) $row['attachment_id']]); $pdo->prepare('UPDATE plugin_virustotal_files SET status = ?, detail = ?, checked_at = ? WHERE id = ?')->execute([$status, mb_substr($detail, 0, 250), db_now(), (int) $row['id']]); if ($row['ticket_id']) log_activity($pdo, (int) $row['ticket_id'], null, 'VirusTotal: Anhang freigegeben: ' . $row['original_name'] . ' (' . $detail . ')'); return true; } /** Sperrt eine Datei dauerhaft (bleibt im Prüfordner), vermerkt es im Ticket und benachrichtigt den Administrator. */ function vt_block(PDO $pdo, array $row, string $detail, int $malicious = 0, int $engines = 0): void { $pdo->prepare("UPDATE ticket_attachments SET stored_name = 'gesperrt', original_name = ? WHERE id = ?")->execute([mb_substr('[GESPERRT] ' . $row['original_name'], 0, 255), (int) $row['attachment_id']]); $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'bad', detail = ?, malicious = ?, engines = ?, checked_at = ? WHERE id = ?")->execute([mb_substr($detail, 0, 250), $malicious, $engines, db_now(), (int) $row['id']]); $ticketId = (int) ($row['ticket_id'] ?? 0); if ($ticketId > 0) { $pdo->prepare("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES (?, 'system', ?, 1, ?)") ->execute([$ticketId, e('Der Anhang „' . $row['original_name'] . '“ wurde von VirusTotal als gefährlich eingestuft (' . $detail . ') und bleibt gesperrt. Er liegt in der Quarantäne und lässt sich unter VirusTotal prüfen oder löschen.'), db_now()]); log_activity($pdo, $ticketId, null, 'VirusTotal: Anhang gesperrt: ' . $row['original_name'] . ' (' . $detail . ')'); if (function_exists('apply_tag_to_ticket')) apply_tag_to_ticket($pdo, $ticketId, 'Virus-Verdacht'); } if (vt_config()['notify']) vt_notify($pdo, $ticketId, (string) $row['original_name'], $detail); } function vt_notify(PDO $pdo, int $ticketId, string $name, string $detail): void { $to = (string) (get_setting('admin_email', '') ?: (config()['site']['admin_email'] ?? '')); if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) return; $t = $pdo->prepare('SELECT ticket_number FROM tickets WHERE id = ?'); $t->execute([$ticketId]); $number = (string) $t->fetchColumn(); try { send_mail($to, 'VirusTotal: Anhang in Ticket #' . $number . ' gesperrt', '

Der Anhang ' . e($name) . ' in Ticket #' . e($number) . ' wurde gesperrt (' . e($detail) . ').

Er liegt in der Quarantäne. Prüfen oder löschen kannst du ihn unter VirusTotal im Mitarbeiterbereich.

'); } catch (Throwable $e) { // Mailfehler dürfen das Sperren nie verhindern } } /** Löscht eine zurückgehaltene oder gesperrte Datei endgültig (der Anhang bleibt als „gelöscht“ im Ticket vermerkt). */ function vt_delete(PDO $pdo, int $rowId): bool { $row = vt_row($pdo, $rowId); if (!$row || !in_array($row['status'], ['bad', 'unknown', 'error', 'pending', 'analyzing'], true)) return false; $file = vt_hold_dir() . '/' . basename((string) $row['stored_name']); if (is_file($file)) @unlink($file); $pdo->prepare('UPDATE ticket_attachments SET original_name = ? WHERE id = ?')->execute([mb_substr('[GELÖSCHT] ' . $row['original_name'], 0, 255), (int) $row['attachment_id']]); $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'deleted', detail = ?, checked_at = ? WHERE id = ?")->execute(['Gelöscht am ' . db_now(), db_now(), $rowId]); return true; } /** Von Hand freigeben (zum Beispiel bei Fehlalarm oder unbekannter Datei). */ function vt_release_manual(PDO $pdo, int $rowId): bool { $row = vt_row($pdo, $rowId); if (!$row || !in_array($row['status'], ['bad', 'unknown', 'error', 'pending', 'analyzing'], true)) return false; return vt_release($pdo, $row, 'released', 'Von Hand freigegeben am ' . db_now()); } /** Noch einmal prüfen lassen (zum Beispiel nach einem Fehler). */ function vt_recheck(PDO $pdo, int $rowId): bool { $row = vt_row($pdo, $rowId); if (!$row || !in_array($row['status'], ['unknown', 'error'], true)) return false; $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'pending', tries = 0, next_try = ?, detail = NULL, analysis_id = NULL WHERE id = ?")->execute([db_now(), $rowId]); return true; } // ── Hintergrundlauf ──────────────────────────────────────────────────────── function vt_day_used(): int { $today = date('Y-m-d'); if (plugin_setting(VT, 'day', '') !== $today) { plugin_set_setting(VT, 'day', $today); plugin_set_setting(VT, 'day_count', '0'); } return (int) plugin_setting(VT, 'day_count', '0'); } function vt_day_add(int $n = 1): void { plugin_set_setting(VT, 'day_count', (string) (vt_day_used() + $n)); } /** * Entscheidung nach einer Abfrage. @return bool false = Lauf beenden (Tageslimit, Minutenlimit oder Schlüsselproblem) */ function vt_apply(PDO $pdo, array $row, array $res, array $cfg): bool { $soon = fn(int $sec) => date('Y-m-d H:i:s', time() + $sec); switch ($res['result']) { case 'limit': $pdo->prepare('UPDATE plugin_virustotal_files SET next_try = ? WHERE id = ?')->execute([$soon(60), $row['id']]); return false; case 'key': plugin_set_setting(VT, 'last_error', db_now() . ': ' . $res['detail']); return false; case 'error': plugin_set_setting(VT, 'last_error', db_now() . ': ' . $res['detail']); $tries = (int) $row['tries'] + 1; if ($tries >= 6) { if ($cfg['on_error'] === 'allow') vt_release($pdo, $row, 'released', 'VirusTotal nicht erreichbar, freigegeben (Einstellung)'); else $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'error', detail = ?, tries = ?, checked_at = ? WHERE id = ?")->execute([mb_substr((string) $res['detail'], 0, 250), $tries, db_now(), $row['id']]); } else { $pdo->prepare('UPDATE plugin_virustotal_files SET tries = ?, next_try = ?, detail = ? WHERE id = ?')->execute([$tries, $soon(min(3600, 60 * (2 ** $tries))), mb_substr((string) $res['detail'], 0, 250), $row['id']]); } return true; case 'bad': plugin_set_setting(VT, 'last_error', ''); vt_block($pdo, $row, (string) $res['detail'], (int) $res['malicious'], (int) $res['engines']); return true; case 'clean': plugin_set_setting(VT, 'last_error', ''); vt_release($pdo, $row, 'clean', (string) ($res['detail'] ?? 'VirusTotal: keine Funde')); $pdo->prepare('UPDATE plugin_virustotal_files SET malicious = ?, engines = ? WHERE id = ?')->execute([(int) $res['malicious'], (int) $res['engines'], $row['id']]); return true; case 'wait': if (time() - (int) strtotime((string) $row['created_at']) > 3600) { $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'error', detail = ?, checked_at = ? WHERE id = ?")->execute(['Die Analyse bei VirusTotal wurde nicht rechtzeitig fertig.', db_now(), $row['id']]); } else { $pdo->prepare('UPDATE plugin_virustotal_files SET next_try = ? WHERE id = ?')->execute([$soon(60), $row['id']]); } return true; default: // unknown oder none: VirusTotal kennt die Datei (noch) nicht $path = vt_hold_dir() . '/' . basename((string) $row['stored_name']); $size = is_file($path) ? (int) filesize($path) : 0; if ($cfg['upload'] && $size > 0 && $size <= VT_UPLOAD_MAX && $row['status'] === 'pending') { $up = vt_upload($path, (string) $row['original_name'], $cfg['key']); vt_day_add(); if (!$up['ok']) { if ($up['detail'] === 'limit') { $pdo->prepare('UPDATE plugin_virustotal_files SET next_try = ? WHERE id = ?')->execute([$soon(60), $row['id']]); return false; } return vt_apply($pdo, $row, ['result' => 'error', 'detail' => $up['detail'], 'malicious' => 0, 'engines' => 0], $cfg); } $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'analyzing', analysis_id = ?, next_try = ?, detail = 'Zur Analyse bei VirusTotal hochgeladen' WHERE id = ?")->execute([$up['id'], $soon(60), $row['id']]); return true; } if ($cfg['unknown'] === 'allow') { vt_release($pdo, $row, 'released', 'Bei VirusTotal unbekannt, freigegeben (Einstellung)'); return true; } $why = $cfg['upload'] && $size > VT_UPLOAD_MAX ? 'Bei VirusTotal unbekannt und zu groß für den Upload (über 32 MB).' : 'Bei VirusTotal unbekannt: bitte selbst prüfen und freigeben oder löschen.'; $pdo->prepare("UPDATE plugin_virustotal_files SET status = 'unknown', detail = ?, checked_at = ? WHERE id = ?")->execute([$why, db_now(), $row['id']]); if ((int) $row['ticket_id'] > 0) log_activity($pdo, (int) $row['ticket_id'], null, 'VirusTotal: Anhang bleibt gesperrt (unbekannt): ' . $row['original_name']); return true; } } /** * Hintergrundlauf (jede Minute oder alle 5 Minuten): zurückgehaltene Anhänge abfragen, höchstens 4 pro Lauf (Grenze des kostenlosen * Schlüssels) und 480 pro Tag. @return int Zahl der Abfragen */ function vt_tick(PDO $pdo): int { $cfg = vt_config(); if ($cfg['key'] === '') return 0; // Zwei Auslöser (jede Minute und alle 5 Minuten) sollen sich nicht überlappen if (time() - (int) plugin_setting(VT, 'tick_at', '0') < 25) return 0; plugin_set_setting(VT, 'tick_at', (string) time()); // Anhänge, die kein Ereignis gemeldet hat (zum Beispiel aus E-Mails), nachträglich zurückhalten foreach (vt_unseen($pdo, 25) as $att) vt_hold($pdo, $att); $rows = $pdo->prepare("SELECT * FROM plugin_virustotal_files WHERE status IN ('pending','analyzing') AND next_try <= ? ORDER BY id LIMIT 4"); $rows->execute([db_now()]); $done = 0; foreach ($rows->fetchAll() as $row) { if (vt_day_used() >= VT_DAY_LIMIT) { plugin_set_setting(VT, 'last_error', db_now() . ': Das Tageslimit des VirusTotal-Schlüssels ist erreicht, es geht morgen weiter.'); break; } $res = $row['status'] === 'analyzing' && $row['analysis_id'] ? vt_analysis((string) $row['analysis_id'], $cfg['key'], $cfg['threshold']) : vt_lookup((string) $row['sha256'], $cfg['key'], $cfg['threshold']); vt_day_add(); $done++; $pdo->prepare('UPDATE plugin_virustotal_files SET checked_at = ? WHERE id = ?')->execute([db_now(), $row['id']]); if (!vt_apply($pdo, $row, $res, $cfg)) break; } if ($done > 0) plugin_set_setting(VT, 'last_run', db_now()); return $done; } /** Zahlen für die Seite. @return array */ function vt_stats(PDO $pdo): array { $out = ['pending' => 0, 'analyzing' => 0, 'clean' => 0, 'bad' => 0, 'unknown' => 0, 'error' => 0, 'released' => 0, 'deleted' => 0]; foreach ($pdo->query('SELECT status, COUNT(*) AS n FROM plugin_virustotal_files GROUP BY status')->fetchAll() as $r) $out[$r['status']] = (int) $r['n']; return $out; } /** Prüft, ob der Schlüssel funktioniert (Abfrage des Fingerabdrucks der Standard-Testdatei EICAR). @return array{ok:bool,message:string} */ function vt_test_key(string $key, int $threshold): array { if ($key === '') return ['ok' => false, 'message' => 'Bitte zuerst einen Schlüssel eintragen und speichern.']; $r = vt_lookup(VT_EICAR_SHA256, $key, $threshold); if ($r['result'] === 'bad') return ['ok' => true, 'message' => 'Der Schlüssel funktioniert: Die Standard-Testdatei (EICAR) wurde von VirusTotal als gefährlich erkannt (' . $r['malicious'] . ' von ' . $r['engines'] . ' Scannern).']; if ($r['result'] === 'limit') return ['ok' => false, 'message' => 'Das Abfrage-Limit von VirusTotal ist gerade erreicht. Bitte in einer Minute noch einmal versuchen.']; if ($r['result'] === 'clean' || $r['result'] === 'unknown' || $r['result'] === 'none') return ['ok' => true, 'message' => 'Der Schlüssel wird angenommen, die Testdatei wurde aber unerwartet bewertet (' . $r['result'] . ').']; return ['ok' => false, 'message' => (string) ($r['detail'] ?? 'Die Abfrage ist fehlgeschlagen.')]; } plugin_add_hook('plugin.uninstall.' . VT, function () { try { get_db()->exec('DROP TABLE IF EXISTS plugin_virustotal_files'); } catch (Throwable $e) { // Die Dateien im Ordner uploads/vt-pruefung bleiben liegen } });