fuera (poda automática diaria). * - Los puntos se reducen a la mitad tras 24 h sin actividad. * - Escrituras agrupadas: se guarda como mucho al final de la petición * y solo si cambió algo importante (bloqueo o +5 puntos). * * Sensores: login fallido +8 · sondeo de ruta sensible +15 · 404 genérico * +2 (máx. 10/día) · XML-RPC +5 solo si el dueño lo tiene desactivado. * Umbral 30 -> bloqueo de 1 h; reincidente -> 24 h. Nunca se vigila ni * bloquea a administradores conectados ni a IP privadas/reservadas. * * @package VidalSecurityAI */ if ( ! defined( 'ABSPATH' ) ) { exit; } define( 'VSAI_BOT_THRESHOLD', 30 ); define( 'VSAI_BOT_MAX_IPS', 200 ); /** ¿Escudo activo? */ function vsai_bot_shield_on() { if ( ! function_exists( 'vsai_get_settings' ) ) { return false; } $s = vsai_get_settings(); return ! isset( $s['bot_shield'] ) || ! empty( $s['bot_shield'] ); } /** IP del visitante (o '' si no vigilable). */ function vsai_bot_ip() { $ip = function_exists( 'vsai_premium_client_ip' ) ? vsai_premium_client_ip() : ( isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '' ); if ( ! $ip || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { return ''; } // Nunca IP privadas/reservadas (ni la del propio servidor). if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return ''; } // Ni las de la lista blanca del dueño. if ( vsai_ip_allowlisted( $ip ) ) { return ''; } return $ip; } /** ¿Es un administrador conectado? Ellos nunca se vigilan. */ function vsai_bot_is_admin() { return is_user_logged_in() && current_user_can( 'manage_options' ); } /* --- Bots buenos verificados ------------------------------------------------- * Separar a los buenos de los malos: los rastreadores que traen visitas * (Google, Bing, DuckDuckGo, Apple, Yandex, Meta) no deben sumar puntos * ni bloquearse jamas — bloquear a Google seria un suicidio SEO. * La verificacion es la oficial: el User-Agent SOLO abre la puerta a * comprobar; manda el DNS inverso (la IP debe resolver a un dominio del * buscador) confirmado hacia adelante (ese nombre debe resolver de * vuelta a la misma IP). Un bot FALSO disfrazado de Googlebot no pasa * la prueba y sigue sumando puntos como cualquier otro. */ /** Casas verificables: patron de UA y sufijos de dominio (PTR) validos. */ function vsai_bot_good_registry() { return array( 'Google' => array( 'ua' => 'Googlebot|Google-InspectionTool|Mediapartners-Google|AdsBot-Google|GoogleOther', 'ptr' => array( '.googlebot.com', '.google.com' ) ), 'Bing' => array( 'ua' => 'bingbot|BingPreview|msnbot', 'ptr' => array( '.search.msn.com' ) ), 'DuckDuckGo' => array( 'ua' => 'DuckDuckBot', 'ptr' => array( '.duckduckgo.com' ) ), 'Apple' => array( 'ua' => 'Applebot', 'ptr' => array( '.applebot.apple.com' ) ), 'Yandex' => array( 'ua' => 'YandexBot', 'ptr' => array( '.yandex.ru', '.yandex.net', '.yandex.com' ) ), 'Meta' => array( 'ua' => 'facebookexternalhit|Facebot|Meta-ExternalAgent', 'ptr' => array( '.facebook.com', '.fbcdn.net' ) ), ); } /** Confirmacion hacia adelante: el nombre debe resolver a la misma IP. */ function vsai_bot_forward_ok( $host, $ip ) { if ( empty( vsai_get_settings()['bot_dns_check'] ) ) { return false; } if ( false !== strpos( $ip, ':' ) ) { $recs = dns_get_record( $host, DNS_AAAA ); if ( is_array( $recs ) ) { foreach ( $recs as $r ) { if ( isset( $r['ipv6'] ) && strtolower( $r['ipv6'] ) === strtolower( $ip ) ) { return true; } } } return false; } return gethostbyname( $host ) === $ip; } /** * Nombre del bot bueno verificado para esta IP, o '' si no lo es. * Cacheado en un transient (7 dias si es bueno, 1 dia si no) para no * pagar DNS en cada peticion. */ function vsai_bot_good_name( $ip ) { if ( empty( vsai_get_settings()['bot_dns_check'] ) || ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return ''; } $key = 'vsai_gb_' . md5( $ip ); $cached = get_transient( $key ); if ( false !== $cached ) { if ( '' !== (string) $cached ) { vsai_bot_good_seen( $ip, (string) $cached ); } return (string) $cached; } $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $name = ''; if ( '' !== $ua ) { foreach ( vsai_bot_good_registry() as $label => $def ) { if ( ! preg_match( '/(' . $def['ua'] . ')/i', $ua ) ) { continue; } $host = gethostbyaddr( $ip ); if ( $host && $host !== $ip ) { $host = strtolower( rtrim( $host, '.' ) ); foreach ( $def['ptr'] as $suffix ) { if ( substr( $host, -strlen( $suffix ) ) === $suffix && vsai_bot_forward_ok( $host, $ip ) ) { $name = $label; break 2; } } } break; // El UA dice ser de esta casa: si el DNS no confirma, no es bueno. } } set_transient( $key, $name, '' !== $name ? 7 * DAY_IN_SECONDS : DAY_IN_SECONDS ); if ( '' !== $name ) { vsai_bot_good_seen( $ip, $name ); } return $name; } /** Registro de bots buenos vistos (una opcion, tope 100, poda 30 dias). */ function vsai_bot_good_seen( $ip, $name ) { static $done = array(); if ( isset( $done[ $ip ] ) ) { return; } $done[ $ip ] = true; $list = get_option( 'vsai_good_bots', array() ); $list = is_array( $list ) ? $list : array(); $now = time(); $e = isset( $list[ $ip ] ) ? $list[ $ip ] : array( 'name' => $name, 'first' => $now, 'last' => 0, 'hits' => 0 ); if ( $now - (int) $e['last'] < 6 * HOUR_IN_SECONDS && ( $e['name'] ?? '' ) === $name ) { return; // Anotado hace poco: no escribir en cada visita del rastreador. } $e['name'] = $name; $e['last'] = $now; $e['hits'] = (int) $e['hits'] + 1; $list[ $ip ] = $e; foreach ( $list as $k => $v ) { if ( (int) ( $v['last'] ?? 0 ) < $now - 30 * DAY_IN_SECONDS ) { unset( $list[ $k ] ); } } if ( count( $list ) > 100 ) { uasort( $list, function ( $a, $b ) { return ( $a['last'] ?? 0 ) <=> ( $b['last'] ?? 0 ); } ); $list = array_slice( $list, -100, null, true ); } update_option( 'vsai_good_bots', $list, false ); } /** Lista de bots buenos vistos (panel y reporte). */ function vsai_bot_good_list() { $list = get_option( 'vsai_good_bots', array() ); return is_array( $list ) ? $list : array(); } /** * Lista blanca del dueño: estas IP jamás se vigilan ni se bloquean, * ni siquiera por orden del Guardián IA. La última palabra es del dueño. */ function vsai_ip_allowlisted( $ip ) { if ( ! $ip ) { return false; } $list = get_option( 'vsai_allowlist', array() ); return is_array( $list ) && in_array( $ip, $list, true ); } /** Carga el libro de IP (caché estática por petición). */ function &vsai_bot_ledger() { static $ledger = null; if ( null === $ledger ) { $ledger = get_option( 'vsai_bot_ledger', array() ); if ( ! is_array( $ledger ) ) { $ledger = array(); } } return $ledger; } /** Marca el libro como modificado (guardado agrupado al final). */ function vsai_bot_touch( $force = false ) { static $hooked = false; $GLOBALS['vsai_bot_dirty'] = true; if ( $force ) { $GLOBALS['vsai_bot_force'] = true; } if ( ! $hooked ) { $hooked = true; add_action( 'shutdown', function () { if ( empty( $GLOBALS['vsai_bot_dirty'] ) ) { return; } $ledger = &vsai_bot_ledger(); vsai_bot_prune( $ledger ); update_option( 'vsai_bot_ledger', $ledger, false ); }, 99 ); } } /** Poda: fuera lo caducado; tope duro de IP (salen las más viejas). */ function vsai_bot_prune( &$ledger ) { $now = time(); foreach ( $ledger as $ip => $e ) { $until = isset( $e['until'] ) ? (int) $e['until'] : 0; $last = isset( $e['last'] ) ? (int) $e['last'] : 0; if ( $until < $now && $last < $now - 7 * DAY_IN_SECONDS ) { unset( $ledger[ $ip ] ); } } if ( count( $ledger ) > VSAI_BOT_MAX_IPS ) { uasort( $ledger, function ( $a, $b ) { return ( $a['last'] ?? 0 ) <=> ( $b['last'] ?? 0 ); } ); $ledger = array_slice( $ledger, -VSAI_BOT_MAX_IPS, null, true ); } } /** * Registra un golpe de comportamiento contra una IP. * * @param string $kind login|probe|xmlrpc|nf * @param int $points Puntos del golpe. */ function vsai_bot_hit( $kind, $points ) { if ( ! vsai_bot_shield_on() || vsai_bot_is_admin() ) { return; } $ip = vsai_bot_ip(); if ( ! $ip ) { return; } // Bot bueno verificado: no suma puntos jamas; si heredo puntos de // antes de existir la verificacion, se le limpia la ficha. if ( vsai_bot_good_name( $ip ) ) { $ledger0 = &vsai_bot_ledger(); if ( isset( $ledger0[ $ip ] ) ) { unset( $ledger0[ $ip ] ); vsai_bot_touch( true ); } return; } $ledger = &vsai_bot_ledger(); $now = time(); $e = isset( $ledger[ $ip ] ) ? $ledger[ $ip ] : array( 'p' => 0, 'last' => $now, 'blocks' => 0, 'until' => 0, 'h' => array(), ); // Ya bloqueada: no se acumula más (el 403 ya hace su trabajo). if ( (int) $e['until'] > $now ) { return; } // Decaimiento: 24 h sin actividad -> los puntos se parten a la mitad. if ( $now - (int) $e['last'] > DAY_IN_SECONDS ) { $e['p'] = (int) floor( (int) $e['p'] / 2 ); } // 404 genéricos: máximo de puntos al día por IP. if ( 'nf' === $kind ) { $today = gmdate( 'Ymd' ); if ( ( $e['nfd'] ?? '' ) !== $today ) { $e['nfd'] = $today; $e['nfn'] = 0; } if ( (int) ( $e['nfn'] ?? 0 ) >= 10 ) { return; } $e['nfn'] = (int) ( $e['nfn'] ?? 0 ) + 1; } $e['p'] = (int) $e['p'] + $points; $e['last'] = $now; $e['h'][ $kind ] = (int) ( $e['h'][ $kind ] ?? 0 ) + 1; $ledger[ $ip ] = $e; $important = $points >= 5; if ( (int) $e['p'] >= VSAI_BOT_THRESHOLD ) { // Bloqueo: 1 h la primera vez, 24 h si reincide. $hours = (int) $e['blocks'] >= 1 ? 24 : 1; if ( function_exists( 'vsai_observe_on' ) && vsai_observe_on() ) { // Modo observación: se anota el "habría bloqueado" y no se bloquea. vsai_observe_log( sprintf( /* translators: 1: IP address. 2: block duration, in hours. */ __( 'The shield would have blocked %1$s for %2$d h for accumulated misbehaviour.', 'vidal-security-ai' ), $ip, $hours ) ); $e['p'] = 0; $ledger[ $ip ] = $e; vsai_bot_touch( true ); return; } $e['until'] = $now + $hours * HOUR_IN_SECONDS; $e['blocks'] = (int) $e['blocks'] + 1; $e['p'] = 0; $ledger[ $ip ] = $e; vsai_bot_register_block_stat(); if ( function_exists( 'vsai_premium_log_event' ) ) { vsai_premium_log_event( 'scan_probe', array( 'ip' => $ip, 'detail' => sprintf( 'Escudo de bots: bloqueo local de %d h por mal comportamiento acumulado.', $hours ), ) ); } // Reincidente (24 h): es de lo urgente que sale por WhatsApp. if ( 24 === $hours && function_exists( 'vsai_premium_send_urgent' ) ) { vsai_premium_send_urgent( __( 'Attack blocked for 24 hours', 'vidal-security-ai' ), sprintf( /* translators: %s: IP address that was blocked. */ __( 'Repeat offender: the bot shield blocked the IP %s for 24 hours after repeated misbehaviour.', 'vidal-security-ai' ), $ip ), 'shield24_' . $ip ); } vsai_bot_touch( true ); return; } vsai_bot_touch( $important ); } /** Estadística mínima de bloqueos (solo marcas de tiempo recientes). */ function vsai_bot_register_block_stat() { $stats = get_option( 'vsai_bot_stats', array() ); $stats = is_array( $stats ) ? $stats : array(); $list = isset( $stats['blocks'] ) && is_array( $stats['blocks'] ) ? $stats['blocks'] : array(); $list[] = time(); $cutoff = time() - 30 * DAY_IN_SECONDS; $list = array_values( array_filter( $list, function ( $t ) use ( $cutoff ) { return (int) $t > $cutoff; } ) ); $stats['blocks'] = array_slice( $list, -100 ); update_option( 'vsai_bot_stats', $stats, false ); } /* --- Sensores -------------------------------------------------------------- */ add_action( 'wp_login_failed', function () { vsai_bot_hit( 'login', 8 ); } ); add_action( 'init', function () { if ( ! vsai_bot_shield_on() ) { return; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; // XML-RPC: solo cuenta si el dueño lo tiene desactivado (puerta cerrada). if ( false !== stripos( $uri, 'xmlrpc.php' ) ) { $s = function_exists( 'vsai_get_settings' ) ? vsai_get_settings() : array(); if ( ! empty( $s['xmlrpc_off'] ) ) { vsai_bot_hit( 'xmlrpc', 5 ); } } }, 5 ); add_action( 'template_redirect', function () { if ( ! function_exists( 'is_404' ) || ! is_404() || ! vsai_bot_shield_on() ) { return; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? strtolower( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : ''; $bad = array( '.env', 'wp-config', 'phpmyadmin', '/.git/', 'etc/passwd', 'wp-admin/install', 'vendor/phpunit', '.aws/', 'config.php.bak', 'db.sql', 'backup.zip' ); foreach ( $bad as $needle ) { if ( false !== strpos( $uri, $needle ) ) { vsai_bot_hit( 'probe', 15 ); return; } } vsai_bot_hit( 'nf', 2 ); }, 1 ); /* --- Ejecución del bloqueo -------------------------------------------------- */ add_action( 'init', function () { if ( ! vsai_bot_shield_on() || vsai_bot_is_admin() ) { return; } if ( function_exists( 'vsai_observe_on' ) && vsai_observe_on() ) { return; // Observando: no se ejecuta ningún bloqueo. } $ip = vsai_bot_ip(); if ( ! $ip ) { return; } if ( vsai_bot_good_name( $ip ) ) { return; // Un bot bueno verificado no se bloquea nunca. } $ledger = &vsai_bot_ledger(); if ( isset( $ledger[ $ip ] ) && (int) ( $ledger[ $ip ]['until'] ?? 0 ) > time() ) { status_header( 403 ); nocache_headers(); wp_die( esc_html__( 'Access temporarily blocked due to suspicious activity. If you are a person, come back in a little while.', 'vidal-security-ai' ), esc_html__( 'Temporarily blocked', 'vidal-security-ai' ), array( 'response' => 403 ) ); } }, 1 ); /* --- Poda diaria -------------------------------------------------------------- */ add_action( 'init', function () { if ( ! wp_next_scheduled( 'vsai_bot_prune' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'vsai_bot_prune' ); } }, 20 ); add_action( 'vsai_bot_prune', function () { $ledger = get_option( 'vsai_bot_ledger', array() ); if ( is_array( $ledger ) ) { vsai_bot_prune( $ledger ); update_option( 'vsai_bot_ledger', $ledger, false ); } } ); /* --- Acciones del dueño: desbloquear / confiar / lista blanca ------------------ */ function vsai_bot_guard_admin() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'No permission.', 'vidal-security-ai' ) ); } check_admin_referer( 'vsai_bot_action' ); } add_action( 'admin_post_vsai_bot_unblock', function () { vsai_bot_guard_admin(); $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : ''; $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : 'unblock'; if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) { $ledger = get_option( 'vsai_bot_ledger', array() ); if ( is_array( $ledger ) && isset( $ledger[ $ip ] ) ) { unset( $ledger[ $ip ] ); update_option( 'vsai_bot_ledger', $ledger, false ); } if ( 'trust' === $mode ) { $list = (array) get_option( 'vsai_allowlist', array() ); $list[] = $ip; update_option( 'vsai_allowlist', array_values( array_unique( $list ) ) ); } } wp_safe_redirect( admin_url( 'admin.php?page=vidal-security-ai¬ice=bot_updated' ) ); exit; } ); add_action( 'admin_post_vsai_allowlist', function () { vsai_bot_guard_admin(); $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : ''; $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : 'add'; $list = (array) get_option( 'vsai_allowlist', array() ); if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) { if ( 'remove' === $mode ) { $list = array_values( array_diff( $list, array( $ip ) ) ); } else { $list[] = $ip; $list = array_values( array_unique( $list ) ); // Al confiar en una IP, sale también del libro del escudo. $ledger = get_option( 'vsai_bot_ledger', array() ); if ( is_array( $ledger ) && isset( $ledger[ $ip ] ) ) { unset( $ledger[ $ip ] ); update_option( 'vsai_bot_ledger', $ledger, false ); } } update_option( 'vsai_allowlist', $list ); } wp_safe_redirect( admin_url( 'admin.php?page=vidal-security-ai¬ice=bot_updated' ) ); exit; } ); /* --- Tarjeta del panel --------------------------------------------------------- */ function vsai_bot_shield_panel_html() { $ledger = get_option( 'vsai_bot_ledger', array() ); $ledger = is_array( $ledger ) ? $ledger : array(); $now = time(); $blocked = array(); $watched = array(); foreach ( $ledger as $ip => $e ) { if ( (int) ( $e['until'] ?? 0 ) > $now ) { $blocked[ $ip ] = $e; } else { $watched[ $ip ] = $e; } } uasort( $watched, function ( $a, $b ) { return ( $b['p'] ?? 0 ) <=> ( $a['p'] ?? 0 ); } ); $stats = get_option( 'vsai_bot_stats', array() ); $blocks_7d = 0; if ( isset( $stats['blocks'] ) && is_array( $stats['blocks'] ) ) { foreach ( $stats['blocks'] as $t ) { if ( (int) $t > $now - 7 * DAY_IN_SECONDS ) { $blocks_7d++; } } } $labels = array( 'login' => __( 'failed logins', 'vidal-security-ai' ), 'probe' => __( 'sensitive-path probes', 'vidal-security-ai' ), 'xmlrpc' => __( 'XML-RPC hits', 'vidal-security-ai' ), 'nf' => __( 'nonexistent pages', 'vidal-security-ai' ), ); $reason = function ( $e ) use ( $labels ) { $best = ''; $max = -1; foreach ( (array) ( $e['h'] ?? array() ) as $k => $n ) { if ( (int) $n > $max ) { $max = (int) $n; $best = $k; } } return $best && isset( $labels[ $best ] ) ? $labels[ $best ] : __( 'suspicious activity', 'vidal-security-ai' ); }; ob_start(); ?>

🤖

( $a['last'] ?? 0 ); } ); ?>

✅


✅