344 lines
10 KiB
PHP
344 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Security Logger for Sheet Data Checker Pro
|
|
*
|
|
* Handles logging of security events including rate limit blocks,
|
|
* CAPTCHA verification failures, and other security-related events
|
|
*
|
|
* @since 1.5.0
|
|
*/
|
|
|
|
class CHECKER_SECURITY_LOGGER {
|
|
|
|
/**
|
|
* Log a security event
|
|
*
|
|
* @param string $event_type Type of event (rate_limit, recaptcha, turnstile, nonce)
|
|
* @param int $checker_id Checker post ID
|
|
* @param array $event_data Event-specific data
|
|
* @param string $level Log level (info, warning, error)
|
|
* @return bool Success status
|
|
*/
|
|
public static function log_event($event_type, $checker_id, $event_data = [], $level = 'info') {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->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;
|
|
}
|
|
}
|