Files
dw-sheet-data-checker/includes/logs/class-Security-Logger.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;
}
}