ID ) ? $u->user_login : __( 'the system', 'vidal-security-ai' ); } /* --- 1) Ajustes sensibles ------------------------------------------------------- */ add_action( 'updated_option', function ( $option, $old, $new ) { if ( ! is_admin() || wp_doing_ajax() && ! is_user_logged_in() ) { return; } if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { return; // Solo vigilamos cambios hechos por personas en el admin. } $labels = array( 'admin_email' => __( 'the site’s admin email', 'vidal-security-ai' ), 'siteurl' => __( 'the site URL (siteurl)', 'vidal-security-ai' ), 'home' => __( 'the main URL (home)', 'vidal-security-ai' ), 'users_can_register' => __( 'whether anyone can register', 'vidal-security-ai' ), 'default_role' => __( 'the default role for new users', 'vidal-security-ai' ), 'permalink_structure' => __( 'the permalink structure', 'vidal-security-ai' ), ); $what = $labels[ $option ] ?? null; if ( ! $what && class_exists( 'WooCommerce' ) && 0 === strpos( $option, 'woocommerce_' ) && '_settings' === substr( $option, -9 ) ) { $gid = substr( $option, strlen( 'woocommerce_' ), -9 ); if ( WC()->payment_gateways() ) { foreach ( WC()->payment_gateways()->payment_gateways() as $gw ) { if ( $gw->id === $gid ) { $what = sprintf( /* translators: %s: name of the payment gateway. */ __( 'the %s payment gateway settings', 'vidal-security-ai' ), $gw->get_method_title() ); break; } } } } if ( ! $what || $old === $new ) { return; } vsai_watch_alert( 'opt_' . md5( $option ), __( 'A sensitive setting on your site changed', 'vidal-security-ai' ), array( sprintf( /* translators: 1: the setting that changed. 2: username of the account that made the change. */ __( '%1$s changed. It was done by the account “%2$s”.', 'vidal-security-ai' ), $what, vsai_watch_who() ), __( 'These are the settings an intruder touches to divert payments or take control. If it was you, ignore this notice. If you do not recognise the change, go to the panel now, review that setting and change your password.', 'vidal-security-ai' ), ) ); }, 10, 3 ); add_action( 'set_user_role', function ( $user_id, $role, $old_roles ) { if ( 'administrator' !== $role || in_array( 'administrator', (array) $old_roles, true ) ) { return; } $u = get_userdata( $user_id ); vsai_watch_alert( 'role_' . $user_id, __( 'A user is now an ADMINISTRATOR', 'vidal-security-ai' ), array( sprintf( /* translators: 1: username of the new administrator. 2: their email address. 3: username of the account that assigned the role. */ __( 'The account “%1$s” (%2$s) just received the administrator role, assigned by “%3$s”.', 'vidal-security-ai' ), $u ? $u->user_login : '#' . $user_id, $u ? $u->user_email : '', vsai_watch_who() ), __( 'An administrator can do everything on your site. If it was not you, remove the role immediately from Users.', 'vidal-security-ai' ), ) ); }, 10, 3 ); /* --- 2) Vigilancia de archivos ---------------------------------------------------- */ /** Huella rápida de un directorio (recursiva, con tope de archivos). */ function vsai_files_dir_hash( $dir, $limit = 4000 ) { if ( ! is_dir( $dir ) || ! is_readable( $dir ) ) { return ''; } $complete = true; if ( isset( $GLOBALS['vsai_watch_started'] ) && microtime( true ) - ( $GLOBALS['vsai_watch_started'] ?? microtime( true ) ) > 12 ) { return ''; } $paths = vsai_deep_list_files( $dir, $limit + 1, $complete ); if ( ! $complete || count( $paths ) > $limit ) { return ''; // Incomplete: the caller retains the previous baseline. } $ctx = hash_init( 'sha256' ); foreach ( $paths as $path ) { if ( microtime( true ) - ( $GLOBALS['vsai_watch_started'] ?? microtime( true ) ) > 12 || ! is_readable( $path ) || filesize( $path ) > 10485760 ) { return ''; } $hash = hash_file( 'sha256', $path ); if ( ! $hash ) { return ''; } hash_update( $ctx, $path . ':' . $hash . ';' ); } return hash_final( $ctx ); } /** Ruta absoluta de un archivo a partir de su ruta relativa a la raíz. */ function vsai_watch_abspath( $rel ) { return vsai_path_resolve( $rel ); } /** Construye la foto actual: archivos críticos + plugins + temas. */ function vsai_files_manifest() { $GLOBALS['vsai_watch_started'] = microtime( true ); $m = array( 'files' => array(), 'plugins' => array(), 'themes' => array() ); $sentinels = array( 'wp-config.php', '.htaccess', 'index.php', 'wp-login.php', 'xmlrpc.php' ); foreach ( $sentinels as $rel ) { $p = '.htaccess' === $rel ? vsai_hardening_htaccess_path() : vsai_watch_abspath( $rel ); if ( file_exists( $p ) ) { $m['files'][ $rel ] = hash_file( 'sha256', $p ); } } // PHP suelto en wp-content y en mu-plugins: zonas favoritas del malware. $content = WP_CONTENT_DIR; foreach ( glob( $content . '/*.php' ) ?: array() as $p ) { $m['files'][ vsai_path_relative( $p ) ] = hash_file( 'sha256', $p ); } $mu = defined( 'WPMU_PLUGIN_DIR' ) ? WPMU_PLUGIN_DIR : $content . '/mu-plugins'; if ( is_dir( $mu ) ) { $m['files']['#mu-plugins'] = vsai_files_dir_hash( $mu ); } if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $dir_hashes = array(); foreach ( get_plugins() as $file => $data ) { $dir = WP_PLUGIN_DIR . '/' . dirname( $file ); if ( '.' !== dirname( $file ) && ! isset( $dir_hashes[ $dir ] ) ) { $dir_hashes[ $dir ] = vsai_files_dir_hash( $dir ); } $m['plugins'][ $file ] = array( 'v' => $data['Version'] ?? '', 'h' => '.' === dirname( $file ) ? hash_file( 'sha256', WP_PLUGIN_DIR . '/' . $file ) : $dir_hashes[ $dir ], ); } $active_theme = get_stylesheet(); foreach ( wp_get_themes() as $slug => $theme ) { $m['themes'][ $slug ] = array( 'v' => $theme->get( 'Version' ), 'h' => $slug === $active_theme ? vsai_files_dir_hash( $theme->get_stylesheet_directory() ) : hash_file( 'sha256', $theme->get_stylesheet_directory() . '/style.css' ), ); } return $m; } /** Compara la foto guardada con la actual y avisa de lo inexplicado. */ function vsai_files_check_run() { $lease = vsai_lock_acquire( 'watch' ); if ( ! $lease ) { return; } try { vsai_files_check_locked(); } finally { vsai_lock_release( 'watch', $lease ); } } function vsai_files_check_locked() { $prev = get_option( 'vsai_file_manifest', null ); $now = vsai_files_manifest(); foreach ( $now as $group ) { foreach ( $group as $item ) { if ( empty( is_array( $item ) ? $item['h'] : $item ) ) { update_option( 'vsai_file_last', array( 'ts' => time(), 'issues' => array(), 'partial' => true ), false ); return; } } } update_option( 'vsai_file_manifest', $now, false ); if ( ! is_array( $prev ) ) { update_option( 'vsai_file_last', array( 'ts' => time(), 'issues' => array(), 'baseline' => true ), false ); return; // Primera foto: línea base en silencio. } $issues = array(); $expected = 0; foreach ( $now['files'] as $rel => $h ) { if ( ! isset( $prev['files'][ $rel ] ) ) { $issues[] = sprintf( /* translators: %s: path of the new file. */ __( 'New file: %s', 'vidal-security-ai' ), $rel ); } elseif ( $prev['files'][ $rel ] !== $h ) { $issues[] = sprintf( /* translators: %s: path of the changed file. */ __( 'File changed: %s', 'vidal-security-ai' ), $rel ); } } foreach ( (array) ( $prev['files'] ?? array() ) as $rel => $h ) { if ( ! isset( $now['files'][ $rel ] ) ) { $issues[] = sprintf( /* translators: %s: path of the deleted file. */ __( 'File deleted: %s', 'vidal-security-ai' ), $rel ); } } foreach ( $now['plugins'] as $file => $info ) { if ( ! isset( $prev['plugins'][ $file ] ) ) { $expected++; // Plugin instalado: cambio explicado. continue; } $p = $prev['plugins'][ $file ]; if ( $p['h'] !== $info['h'] ) { if ( $p['v'] !== $info['v'] ) { $expected++; // Actualización de plugin: normal. } else { $issues[] = sprintf( /* translators: 1: plugin file path. 2: plugin version. */ __( 'The plugin %1$s changed internally WITHOUT updating (still on version %2$s).', 'vidal-security-ai' ), $file, $info['v'] ); } } } foreach ( $now['themes'] as $slug => $info ) { if ( isset( $prev['themes'][ $slug ] ) && $prev['themes'][ $slug ]['h'] !== $info['h'] && $prev['themes'][ $slug ]['v'] === $info['v'] ) { $issues[] = sprintf( /* translators: %s: name of the theme. */ __( 'The theme %s changed internally without a version update.', 'vidal-security-ai' ), $slug ); } } update_option( 'vsai_file_last', array( 'ts' => time(), 'issues' => $issues, 'expected' => $expected ), false ); if ( $issues ) { $lines = array( __( 'I detected file changes that NO update explains:', 'vidal-security-ai' ), '' ); foreach ( array_slice( $issues, 0, 10 ) as $i ) { $lines[] = '• ' . $i; } $lines[] = ''; $lines[] = __( 'A file changed without an update is the classic sign of injected code. If it was not you (or your design company) editing files, treat the site as compromised: change passwords and get it reviewed.', 'vidal-security-ai' ); vsai_watch_alert( 'files', __( 'Files changed without explanation', 'vidal-security-ai' ), $lines, 12 * HOUR_IN_SECONDS ); if ( function_exists( 'vsai_premium_log_event' ) ) { vsai_premium_log_event( 'file_change', array( 'path' => $issues[0], 'detail' => implode( ' | ', array_slice( $issues, 0, 5 ) ) ) ); } } } add_action( 'vsai_file_watch', 'vsai_files_check_run' ); add_action( 'init', function () { if ( ! wp_next_scheduled( 'vsai_file_watch' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'vsai_file_watch' ); } if ( ! wp_next_scheduled( 'vsai_digest' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'vsai_digest' ); } }, 21 ); /* --- 3) Parte semanal ------------------------------------------------------------- */ add_action( 'vsai_digest', function () { // v1.9.0: el reporte completo por email sustituye a este parte sencillo. if ( function_exists( 'vsai_report_maybe_send' ) ) { vsai_report_maybe_send(); return; } if ( function_exists( 'vsai_get_settings' ) ) { $s = vsai_get_settings(); if ( isset( $s['weekly_digest'] ) && ! $s['weekly_digest'] ) { return; } } $to = get_option( 'admin_email' ); if ( ! $to ) { return; } $lines = array(); $scan = (array) get_option( 'vsai_scan', array() ); $lines[] = sprintf( /* translators: %s: current security score. */ __( 'Security score: %s/100.', 'vidal-security-ai' ), $scan['score'] ?? '—' ); $bs = (array) get_option( 'vsai_bot_stats', array() ); $lines[] = sprintf( /* translators: 1: blocks applied by the bot shield. 2: IP addresses being watched. */ __( 'Bot shield: %1$d blocks applied and %2$d IPs watched for misbehaviour.', 'vidal-security-ai' ), (int) ( $bs['blocks'] ?? 0 ), count( (array) get_option( 'vsai_bot_ledger', array() ) ) ); $last = (array) get_option( 'vsai_file_last', array() ); $lines[] = ! empty( $last['issues'] ) ? sprintf( /* translators: %d: number of unexplained file changes this week. */ __( '⚠️ Files: %d unexplained change(s) this week — check the panel.', 'vidal-security-ai' ), count( $last['issues'] ) ) : __( 'Files: no unexplained changes. Everything in its place.', 'vidal-security-ai' ); if ( function_exists( 'vsai_premium_get_report' ) ) { $lines[] = ''; $lines[] = __( '— AI Guardian report —', 'vidal-security-ai' ); foreach ( vsai_premium_get_report() as $l ) { $lines[] = '• ' . $l; } } $lines[] = ''; $lines[] = __( 'Full panel:', 'vidal-security-ai' ) . ' ' . admin_url( 'admin.php?page=vidal-security-ai' ); wp_mail( $to, sprintf( '🛡️ [%s] %s', wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), __( 'Your week with Vidal Security AI', 'vidal-security-ai' ) ), implode( "\n", $lines ) ); } ); /* --- Tarjeta del panel -------------------------------------------------------------- */ function vsai_watch_panel_html() { $last = (array) get_option( 'vsai_file_last', array() ); ob_start(); ?>

🗂️