tigerstyle-scent/includes/class-security-logger.php
Ryan Malloy 120f0b616d Add release tooling and update for v1.0.0 release
- Add .distignore (operator-private files excluded)
- Add build.sh for WordPress-installable release ZIPs
- Update CLAUDE.md references (now operator-private only)
2026-05-27 14:32:07 -06:00

644 lines
23 KiB
PHP

<?php
/**
* TigerStyle Scent Security Logging System
* Comprehensive security event logging and monitoring for threat detection
*
* @package TigerStyle Scent
*/
defined('ABSPATH') or die('Direct access forbidden.');
class TigerStyleScent_SecurityLogger {
/**
* Security event severity levels
*/
const SEVERITY_LOW = 1;
const SEVERITY_MEDIUM = 2;
const SEVERITY_HIGH = 3;
const SEVERITY_CRITICAL = 4;
/**
* Security event types
*/
const EVENT_AUTH_FAILURE = 'auth_failure';
const EVENT_AUTH_SUCCESS = 'auth_success';
const EVENT_RATE_LIMIT = 'rate_limit_exceeded';
const EVENT_VALIDATION_FAILURE = 'validation_failure';
const EVENT_INJECTION_ATTEMPT = 'injection_attempt';
const EVENT_SUSPICIOUS_REQUEST = 'suspicious_request';
const EVENT_TOKEN_ISSUED = 'token_issued';
const EVENT_TOKEN_REVOKED = 'token_revoked';
const EVENT_CLIENT_CREATED = 'client_created';
const EVENT_SECURITY_VIOLATION = 'security_violation';
/**
* 🔐 SECURITY: Log comprehensive security event
*
* @param string $event_type Type of security event
* @param int $severity Severity level (1-4)
* @param string $message Human-readable message
* @param array $context Additional context data
* @param string $client_id OAuth2 client ID if applicable
* @param int $user_id WordPress user ID if applicable
*/
public static function log_security_event(
string $event_type,
int $severity,
string $message,
array $context = [],
string $client_id = null,
int $user_id = null
): void {
// Prepare comprehensive event data
$event_data = [
'timestamp' => current_time('mysql'),
'timestamp_utc' => gmdate('Y-m-d H:i:s'),
'event_type' => $event_type,
'severity' => $severity,
'severity_name' => self::get_severity_name($severity),
'message' => $message,
'context' => $context,
'client_id' => $client_id,
'user_id' => $user_id,
'session_id' => self::get_session_id(),
'request_data' => self::get_sanitized_request_data(),
'client_info' => self::get_client_info(),
'wordpress_info' => self::get_wordpress_info(),
];
// Store in database for structured querying
self::store_security_event($event_data);
// Log to WordPress error log for immediate visibility
self::log_to_wordpress($event_data);
// Send real-time alerts for critical events
if ($severity >= self::SEVERITY_HIGH) {
self::send_security_alert($event_data);
}
// Trigger WordPress action for external integrations
do_action('tigerstyle_scent_security_event', $event_data);
// Check for attack patterns and auto-block threats
self::analyze_attack_patterns($event_data);
// Real-time threat analysis
self::perform_threat_analysis($event_data);
}
/**
* 🔐 SECURITY: Store security event in database
*/
private static function store_security_event(array $event_data): void {
global $wpdb;
$table_name = $wpdb->prefix . 'tigerstyle_scent_security_log';
// Create table if it doesn't exist
self::ensure_security_log_table();
$wpdb->insert(
$table_name,
[
'timestamp' => $event_data['timestamp'],
'event_type' => $event_data['event_type'],
'severity' => $event_data['severity'],
'message' => $event_data['message'],
'context_data' => json_encode($event_data['context']),
'client_id' => $event_data['client_id'],
'user_id' => $event_data['user_id'],
'session_id' => $event_data['session_id'],
'client_ip' => $event_data['client_info']['ip'],
'user_agent' => substr($event_data['client_info']['user_agent'], 0, 255),
'request_uri' => $event_data['request_data']['uri'],
'request_method' => $event_data['request_data']['method'],
'full_event_data' => json_encode($event_data)
],
[
'%s', '%s', '%d', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s'
]
);
}
/**
* 🔐 SECURITY: Ensure security log table exists
*/
private static function ensure_security_log_table(): void {
global $wpdb;
$table_name = $wpdb->prefix . 'tigerstyle_scent_security_log';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
timestamp datetime NOT NULL,
event_type varchar(50) NOT NULL,
severity tinyint(1) NOT NULL,
message text NOT NULL,
context_data longtext,
client_id varchar(255),
user_id bigint(20) unsigned,
session_id varchar(64),
client_ip varchar(45) NOT NULL,
user_agent varchar(255),
request_uri varchar(255),
request_method varchar(10),
full_event_data longtext,
created_at timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY event_type (event_type),
KEY severity (severity),
KEY timestamp (timestamp),
KEY client_id (client_id),
KEY user_id (user_id),
KEY client_ip (client_ip)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
/**
* 🔐 SECURITY: Log to WordPress error log
*/
private static function log_to_wordpress(array $event_data): void {
if (defined('WP_DEBUG') && WP_DEBUG) {
$log_message = sprintf(
'[TigerStyle Scent Security] %s [%s] %s - IP: %s, UA: %s',
$event_data['severity_name'],
$event_data['event_type'],
$event_data['message'],
$event_data['client_info']['ip'],
substr($event_data['client_info']['user_agent'], 0, 50)
);
error_log($log_message);
}
}
/**
* 🔐 SECURITY: Send real-time security alerts
*/
private static function send_security_alert(array $event_data): void {
// Email alerts for critical security events
if ($event_data['severity'] >= self::SEVERITY_CRITICAL) {
$admin_email = get_option('admin_email');
$site_name = get_bloginfo('name');
$subject = "[CRITICAL] Security Alert - {$site_name}";
$message = "Critical security event detected:\n\n";
$message .= "Event: {$event_data['event_type']}\n";
$message .= "Message: {$event_data['message']}\n";
$message .= "Time: {$event_data['timestamp']}\n";
$message .= "IP: {$event_data['client_info']['ip']}\n";
$message .= "User Agent: {$event_data['client_info']['user_agent']}\n\n";
$message .= "Please review your security logs immediately.";
wp_mail($admin_email, $subject, $message);
}
// WordPress admin notices for high severity events
if ($event_data['severity'] >= self::SEVERITY_HIGH) {
set_transient('tigerstyle_scent_security_alert', $event_data, 3600);
}
}
/**
* 🔐 SECURITY: Analyze attack patterns
*/
private static function analyze_attack_patterns(array $event_data): void {
global $wpdb;
$table_name = $wpdb->prefix . 'tigerstyle_scent_security_log';
$client_ip = $event_data['client_info']['ip'];
// Check for repeated attacks from same IP in last hour
$recent_attacks = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $table_name
WHERE client_ip = %s
AND severity >= %d
AND timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
$client_ip,
self::SEVERITY_MEDIUM
)
);
// Auto-block IPs with excessive attacks
if ($recent_attacks >= 5) {
self::log_security_event(
self::EVENT_SECURITY_VIOLATION,
self::SEVERITY_CRITICAL,
"IP auto-blocked for repeated security violations: {$client_ip}",
['attack_count' => $recent_attacks, 'auto_blocked' => true],
null,
$event_data['user_id']
);
// Store in transient for rate limiter to check
set_transient("tigerstyle_scent_auto_block_{$client_ip}", time(), DAY_IN_SECONDS);
}
}
/**
* 🔐 SECURITY: Get sanitized client information
*/
private static function get_client_info(): array {
return [
'ip' => self::get_client_ip(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'accept_language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
'accept_encoding' => $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '',
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
'connection' => $_SERVER['HTTP_CONNECTION'] ?? '',
];
}
/**
* 🔐 SECURITY: Get client IP address
*/
private static function get_client_ip(): string {
$ip_headers = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
];
foreach ($ip_headers as $header) {
if (!empty($_SERVER[$header])) {
$ip = $_SERVER[$header];
if (strpos($ip, ',') !== false) {
$ip = trim(explode(',', $ip)[0]);
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
/**
* 🔐 SECURITY: Get sanitized request data
*/
private static function get_sanitized_request_data(): array {
return [
'method' => $_SERVER['REQUEST_METHOD'] ?? '',
'uri' => $_SERVER['REQUEST_URI'] ?? '',
'query_string' => $_SERVER['QUERY_STRING'] ?? '',
'protocol' => $_SERVER['SERVER_PROTOCOL'] ?? '',
'https' => is_ssl(),
'port' => $_SERVER['SERVER_PORT'] ?? '',
];
}
/**
* 🔐 SECURITY: Get WordPress environment info
*/
private static function get_wordpress_info(): array {
return [
'wp_version' => get_bloginfo('version'),
'php_version' => PHP_VERSION,
'plugin_version' => TIGERSTYLE_SCENT_VERSION,
'is_admin' => is_admin(),
'is_user_logged_in' => is_user_logged_in(),
'current_user_id' => get_current_user_id(),
];
}
/**
* 🔐 SECURITY: Get session identifier
*/
private static function get_session_id(): string {
if (session_id()) {
return session_id();
}
// Create session identifier from request characteristics
$session_data = [
self::get_client_ip(),
$_SERVER['HTTP_USER_AGENT'] ?? '',
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
date('Y-m-d H') // Hour-based session
];
return substr(hash('sha256', json_encode($session_data)), 0, 32);
}
/**
* 🔐 SECURITY: Get severity name
*/
private static function get_severity_name(int $severity): string {
switch ($severity) {
case self::SEVERITY_LOW: return 'LOW';
case self::SEVERITY_MEDIUM: return 'MEDIUM';
case self::SEVERITY_HIGH: return 'HIGH';
case self::SEVERITY_CRITICAL: return 'CRITICAL';
default: return 'UNKNOWN';
}
}
/**
* 🔐 SECURITY: Get security events from database
*
* @param array $filters Filtering options
* @param int $limit Number of events to retrieve
* @param int $offset Offset for pagination
* @return array Security events
*/
public static function get_security_events(array $filters = [], int $limit = 50, int $offset = 0): array {
global $wpdb;
$table_name = $wpdb->prefix . 'tigerstyle_scent_security_log';
$where_conditions = ['1=1'];
$where_values = [];
if (!empty($filters['severity'])) {
$where_conditions[] = 'severity >= %d';
$where_values[] = $filters['severity'];
}
if (!empty($filters['event_type'])) {
$where_conditions[] = 'event_type = %s';
$where_values[] = $filters['event_type'];
}
if (!empty($filters['client_ip'])) {
$where_conditions[] = 'client_ip = %s';
$where_values[] = $filters['client_ip'];
}
if (!empty($filters['date_from'])) {
$where_conditions[] = 'timestamp >= %s';
$where_values[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where_conditions[] = 'timestamp <= %s';
$where_values[] = $filters['date_to'];
}
$where_clause = implode(' AND ', $where_conditions);
$query = "SELECT * FROM $table_name WHERE $where_clause ORDER BY timestamp DESC LIMIT %d OFFSET %d";
$where_values[] = $limit;
$where_values[] = $offset;
if (!empty($where_values)) {
$prepared_query = $wpdb->prepare($query, $where_values);
} else {
$prepared_query = $query;
}
return $wpdb->get_results($prepared_query, ARRAY_A);
}
/**
* 🔐 SECURITY: Real-time threat analysis with automatic blocking
*/
private static function perform_threat_analysis(array $event_data): void {
$client_ip = $event_data['client_info']['ip'];
$event_type = $event_data['event_type'];
$severity = $event_data['severity'];
// Immediate threat indicators
$immediate_threats = [
self::EVENT_INJECTION_ATTEMPT,
self::EVENT_SECURITY_VIOLATION
];
if (in_array($event_type, $immediate_threats)) {
self::initiate_emergency_response($event_data);
return;
}
// Analyze recent activity patterns
$recent_violations = self::get_recent_violations($client_ip, 60); // Last 60 minutes
if (count($recent_violations) >= 5) {
// Escalate to high severity and trigger auto-block
self::escalate_threat_level($event_data, $recent_violations);
}
// Check for distributed attack patterns
self::analyze_distributed_threats($event_data);
}
/**
* 🔐 SECURITY: Enhanced attack pattern analysis
*/
private static function analyze_attack_patterns(array $event_data): void {
$client_ip = $event_data['client_info']['ip'];
// Define attack patterns
$attack_patterns = [
'brute_force' => [
'events' => [self::EVENT_AUTH_FAILURE],
'threshold' => 10,
'window' => 300, // 5 minutes
'action' => 'temporary_block'
],
'rate_limit_abuse' => [
'events' => [self::EVENT_RATE_LIMIT],
'threshold' => 3,
'window' => 600, // 10 minutes
'action' => 'escalated_block'
],
'validation_bombing' => [
'events' => [self::EVENT_VALIDATION_FAILURE],
'threshold' => 20,
'window' => 300, // 5 minutes
'action' => 'investigation_required'
]
];
foreach ($attack_patterns as $pattern_name => $pattern) {
if (in_array($event_data['event_type'], $pattern['events'])) {
$violations = self::get_recent_violations_by_type(
$client_ip,
$pattern['events'],
$pattern['window']
);
if (count($violations) >= $pattern['threshold']) {
self::trigger_attack_response($pattern_name, $pattern['action'], $event_data, $violations);
}
}
}
}
/**
* 🔐 SECURITY: Initiate emergency response for critical threats
*/
private static function initiate_emergency_response(array $event_data): void {
$client_ip = $event_data['client_info']['ip'];
// Immediate IP blocking
self::emergency_block_ip($client_ip, 'Critical security violation detected');
// Send immediate critical alert
self::send_critical_security_alert($event_data);
// Log emergency response
error_log(sprintf(
'[TIGERSTYLE SCENT EMERGENCY] Critical threat from IP %s: %s',
$client_ip,
$event_data['message']
));
}
/**
* 🔐 SECURITY: Emergency IP blocking
*/
private static function emergency_block_ip(string $ip, string $reason): void {
// Store in transient for immediate effect
set_transient("tigerstyle_scent_emergency_block_{$ip}", [
'blocked_at' => current_time('timestamp'),
'reason' => $reason,
'duration' => 24 * HOUR_IN_SECONDS, // 24 hours
'threat_level' => 'CRITICAL'
], 24 * HOUR_IN_SECONDS);
// Trigger WordPress action for external integrations (firewall plugins, etc.)
do_action('tigerstyle_scent_emergency_block', $ip, $reason);
}
/**
* 🔐 SECURITY: Enhanced security alerting
*/
private static function send_security_alert(array $event_data): void {
// Skip alerts in development mode unless critical
if (defined('WP_DEBUG') && WP_DEBUG && $event_data['severity'] < self::SEVERITY_CRITICAL) {
return;
}
$admin_email = get_option('admin_email');
if (!$admin_email) return;
$subject = sprintf(
'[%s] %s Security Alert - %s',
get_bloginfo('name'),
strtoupper($event_data['severity_name']),
$event_data['event_type']
);
$message = self::format_security_alert_email($event_data);
// Use WordPress mail function with security headers
$headers = [
'Content-Type: text/html; charset=UTF-8',
'X-Priority: 1',
'X-MSMail-Priority: High'
];
wp_mail($admin_email, $subject, $message, $headers);
}
/**
* 🔐 SECURITY: Format security alert email
*/
private static function format_security_alert_email(array $event_data): string {
$threat_level_colors = [
1 => '#28a745', // Green
2 => '#ffc107', // Yellow
3 => '#fd7e14', // Orange
4 => '#dc3545' // Red
];
$color = $threat_level_colors[$event_data['severity']] ?? '#6c757d';
return sprintf('
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: %s; color: white; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
<h2 style="margin: 0;">🚨 TigerStyle Scent Security Alert</h2>
<p style="margin: 5px 0 0 0;">Severity: <strong>%s</strong></p>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
<h3>Event Details</h3>
<ul>
<li><strong>Event Type:</strong> %s</li>
<li><strong>Time:</strong> %s</li>
<li><strong>Message:</strong> %s</li>
<li><strong>Client IP:</strong> %s</li>
<li><strong>User Agent:</strong> %s</li>
<li><strong>Request URI:</strong> %s</li>
</ul>
</div>
<div style="background: #e9ecef; padding: 15px; border-radius: 5px;">
<h3>Recommended Actions</h3>
<ul>
<li>Review security logs for patterns</li>
<li>Consider blocking suspicious IPs</li>
<li>Monitor for escalated threats</li>
<li>Update security configurations if needed</li>
</ul>
</div>
<p style="margin-top: 20px; font-size: 12px; color: #6c757d;">
This alert was generated by TigerStyle Scent OAuth2 Security Monitor
</p>
</div>
</body>
</html>',
$color,
$event_data['severity_name'],
$event_data['event_type'],
$event_data['timestamp'],
esc_html($event_data['message']),
$event_data['client_info']['ip'],
esc_html(substr($event_data['client_info']['user_agent'], 0, 100)),
esc_html($event_data['request_data']['uri'])
);
}
/**
* 🔐 SECURITY: Get recent security violations
*/
private static function get_recent_violations(string $client_ip, int $minutes): array {
global $wpdb;
$table_name = $wpdb->prefix . 'tigerstyle_scent_security_log';
return $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table_name
WHERE client_ip = %s
AND timestamp >= DATE_SUB(NOW(), INTERVAL %d MINUTE)
AND severity >= %d
ORDER BY timestamp DESC",
$client_ip,
$minutes,
self::SEVERITY_MEDIUM
), ARRAY_A);
}
/**
* 🔐 SECURITY: Clean up old security logs
*/
public static function cleanup_old_logs(int $days_to_keep = 90): void {
global $wpdb;
$table_name = $wpdb->prefix . 'tigerstyle_scent_security_log';
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)",
$days_to_keep
)
);
}
}