prefix . 'checker_security_logs'; // Ensure table exists self::maybe_create_table(); // Prepare data $log_entry = [ 'event_type' => $event_type, 'checker_id' => $checker_id, 'ip_address' => self::mask_ip(self::get_client_ip()), 'user_agent' => substr(sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), 'event_data' => json_encode($event_data), 'level' => $level, 'created_at' => current_time('mysql') ]; // Insert log entry $result = $wpdb->insert($table_name, $log_entry); // Also log to WordPress debug log if enabled if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { $message = sprintf( '[Sheet Data Checker] Security Event: %s - Checker ID: %d - IP: %s - Data: %s', $event_type, $checker_id, self::mask_ip(self::get_client_ip()), json_encode($event_data) ); error_log($message); } return $result !== false; } /** * Log rate limit block * * @param int $checker_id Checker post ID * @param string $ip IP address * @param array $limit_config Rate limit configuration * @return bool Success status */ public static function log_rate_limit_block($checker_id, $ip, $limit_config) { return self::log_event( 'rate_limit', $checker_id, [ 'ip' => $ip, 'max_attempts' => $limit_config['max_attempts'] ?? 5, 'time_window' => $limit_config['time_window'] ?? 15, 'block_duration' => $limit_config['block_duration'] ?? 60 ], 'warning' ); } /** * Log CAPTCHA verification failure * * @param int $checker_id Checker post ID * @param string $captcha_type Type of CAPTCHA (recaptcha, turnstile) * @param array $verification_data Verification result data * @return bool Success status */ public static function log_captcha_failure($checker_id, $captcha_type, $verification_data) { return self::log_event( $captcha_type, $checker_id, [ 'success' => false, 'score' => $verification_data['score'] ?? null, 'error_codes' => $verification_data['error_codes'] ?? [] ], 'warning' ); } /** * Log nonce verification failure * * @param int $checker_id Checker post ID * @param string $nonce_value Nonce value that failed verification * @return bool Success status */ public static function log_nonce_failure($checker_id, $nonce_value) { return self::log_event( 'nonce', $checker_id, [ 'nonce' => substr($nonce_value, 0, 10) . '...' // Only log first 10 chars for security ], 'error' ); } /** * Get recent security logs * * @param array $args Query arguments * @return array Log entries */ public static function get_logs($args = []) { global $wpdb; $table_name = $wpdb->prefix . 'checker_security_logs'; // Default arguments $defaults = [ 'limit' => 50, 'offset' => 0, 'event_type' => null, 'level' => null, 'checker_id' => null, 'date_from' => null, 'date_to' => null ]; $args = wp_parse_args($args, $defaults); // Build WHERE clause $where = ['1=1']; $placeholders = []; if ($args['event_type']) { $where[] = 'event_type = %s'; $placeholders[] = $args['event_type']; } if ($args['level']) { $where[] = 'level = %s'; $placeholders[] = $args['level']; } if ($args['checker_id']) { $where[] = 'checker_id = %d'; $placeholders[] = $args['checker_id']; } if ($args['date_from']) { $where[] = 'created_at >= %s'; $placeholders[] = $args['date_from']; } if ($args['date_to']) { $where[] = 'created_at <= %s'; $placeholders[] = $args['date_to']; } $where_clause = implode(' AND ', $where); // Prepare query $query = $wpdb->prepare( "SELECT * FROM $table_name WHERE $where_clause ORDER BY created_at DESC LIMIT %d OFFSET %d", array_merge($placeholders, [$args['limit'], $args['offset']]) ); return $wpdb->get_results($query); } /** * Get security statistics * * @param array $args Filter arguments * @return array Statistics */ public static function get_statistics($args = []) { global $wpdb; $table_name = $wpdb->prefix . 'checker_security_logs'; // Default date range (last 30 days) $defaults = [ 'date_from' => date('Y-m-d', strtotime('-30 days')), 'date_to' => date('Y-m-d') ]; $args = wp_parse_args($args, $defaults); // Prepare query $query = $wpdb->prepare( "SELECT event_type, COUNT(*) as count, level FROM $table_name WHERE created_at BETWEEN %s AND %s GROUP BY event_type, level ORDER BY count DESC", [$args['date_from'], $args['date_to']] ); $results = $wpdb->get_results($query); // Structure the results $stats = [ 'rate_limit' => ['total' => 0, 'warning' => 0, 'error' => 0], 'recaptcha' => ['total' => 0, 'warning' => 0, 'error' => 0], 'turnstile' => ['total' => 0, 'warning' => 0, 'error' => 0], 'nonce' => ['total' => 0, 'warning' => 0, 'error' => 0], 'total' => 0 ]; foreach ($results as $row) { if (isset($stats[$row->event_type])) { $stats[$row->event_type]['total'] += $row->count; $stats[$row->event_type][$row->level] = $row->count; $stats['total'] += $row->count; } } return $stats; } /** * Clean up old log entries * * @param int $days Keep logs for this many days (default: 90) * @return int Number of rows deleted */ public static function cleanup_old_logs($days = 90) { global $wpdb; $table_name = $wpdb->prefix . 'checker_security_logs'; $cutoff_date = date('Y-m-d H:i:s', strtotime("-$days days")); return $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE created_at < %s", $cutoff_date ) ); } /** * Create the security logs table if it doesn't exist */ private static function maybe_create_table() { global $wpdb; $table_name = $wpdb->prefix . 'checker_security_logs'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, event_type varchar(50) NOT NULL, checker_id bigint(20) unsigned NOT NULL, ip_address varchar(45) NOT NULL, user_agent varchar(255) DEFAULT NULL, event_data longtext DEFAULT NULL, level varchar(10) NOT NULL DEFAULT 'info', created_at datetime NOT NULL, PRIMARY KEY (id), KEY event_type (event_type), KEY checker_id (checker_id), KEY created_at (created_at), KEY level (level) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } /** * Get client IP address (reuses method from Security class) * * @return string IP address */ private static function get_client_ip() { // Check for Cloudflare first if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) { return sanitize_text_field($_SERVER['HTTP_CF_CONNECTING_IP']); } // Check various proxy headers $ip_headers = [ 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ]; foreach ($ip_headers as $header) { if (!empty($_SERVER[$header])) { $ips = explode(',', $_SERVER[$header]); $ip = trim($ips[0]); // Validate IP if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } return !empty($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '0.0.0.0'; } /** * Mask IP address for privacy * * @param string $ip IP address * @return string Masked IP address */ private static function mask_ip($ip) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $parts = explode('.', $ip); return $parts[0] . '.' . $parts[1] . '.***.***'; } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $parts = explode(':', $ip); return $parts[0] . ':' . $parts[1] . '::***'; } return $ip; } }