= $threshold ) { // Check if we already reported this incident recently. $recent_incidents = Security_Events_Entity::count_events( Security_Events_Entity::EVENT_INCIDENT, null, 60 // Last 60 minutes. ); if ( $recent_incidents > 0 ) { return; // Already reported. } Security_Events_Entity::log_event( Security_Events_Entity::EVENT_INCIDENT, Security_Events_Entity::SEVERITY_CRITICAL, '', sprintf( 'DDoS attack detected: %d unique IPs triggered security events in %d minutes', $total_unique, $window ), array( 'unique_blocked_ips' => $unique_blocked_ips, 'unique_dos_ips' => $unique_dos_ips, 'threshold' => $threshold, 'window_minutes' => $window, 'incident_type' => 'ddos', ) ); self::trigger_response( 'ddos', array( 'unique_ips' => $total_unique, 'window' => $window, ) ); } } /** * Detect distributed brute force campaigns. * * Multiple IPs attempting logins across the same time window suggests * a coordinated credential stuffing or brute force attack. * * @return void * * @since 5.2.0 */ private static function detect_brute_force_campaign(): void { $threshold = (int) ( Settings::get_option( 'security_brute_force_threshold' ) ?: self::DEFAULT_BRUTE_FORCE_THRESHOLD ); $window = (int) ( Settings::get_option( 'security_brute_force_window_minutes' ) ?: self::DEFAULT_BRUTE_FORCE_WINDOW_MINUTES ); $brute_force_count = Security_Events_Entity::count_events( Security_Events_Entity::EVENT_BRUTE_FORCE, null, $window ); $unique_ips = Security_Events_Entity::count_unique_ips( Security_Events_Entity::EVENT_BRUTE_FORCE, $window ); // Distributed: many events from many IPs. if ( $brute_force_count >= $threshold && $unique_ips >= 3 ) { $recent = Security_Events_Entity::count_events( Security_Events_Entity::EVENT_INCIDENT, null, 60 // Last 60 minutes. ); if ( $recent > 0 ) { return; } Security_Events_Entity::log_event( Security_Events_Entity::EVENT_INCIDENT, Security_Events_Entity::SEVERITY_CRITICAL, '', sprintf( 'Distributed brute force campaign detected: %d attempts from %d unique IPs in %d minutes', $brute_force_count, $unique_ips, $window ), array( 'attempt_count' => $brute_force_count, 'unique_ips' => $unique_ips, 'threshold' => $threshold, 'window_minutes' => $window, 'incident_type' => 'brute_force_campaign', ) ); self::trigger_response( 'brute_force', array( 'attempts' => $brute_force_count, 'unique_ips' => $unique_ips, 'window' => $window, ) ); } } /** * Detect privilege escalation chains. * * Multiple escalation events in a short window may indicate * a compromised account or an active exploitation attempt. * * @return void * * @since 5.2.0 */ private static function detect_escalation_chain(): void { $threshold = (int) ( Settings::get_option( 'security_escalation_threshold' ) ?: self::DEFAULT_ESCALATION_THRESHOLD ); $window = (int) ( Settings::get_option( 'security_escalation_window_minutes' ) ?: self::DEFAULT_ESCALATION_WINDOW_MINUTES ); $escalation_count = Security_Events_Entity::count_events( Security_Events_Entity::EVENT_PRIV_ESCALATION, null, $window ); if ( $escalation_count >= $threshold ) { $recent = Security_Events_Entity::count_events( Security_Events_Entity::EVENT_INCIDENT, null, 60 // Last 60 minutes. ); if ( $recent > 0 ) { return; } Security_Events_Entity::log_event( Security_Events_Entity::EVENT_INCIDENT, Security_Events_Entity::SEVERITY_CRITICAL, '', sprintf( 'Privilege escalation chain detected: %d escalation events in %d minutes', $escalation_count, $window ), array( 'escalation_count' => $escalation_count, 'threshold' => $threshold, 'window_minutes' => $window, 'incident_type' => 'escalation_chain', ) ); self::trigger_response( 'escalation', array( 'count' => $escalation_count, 'window' => $window, ) ); } } // ─── Automated Response ───────────────────────────────────────── /** * Trigger an automated incident response. * * @param string $incident_type Incident type (ddos, brute_force, escalation). * @param array $context Context data for the response. * * @return void * * @since 5.2.0 */ private static function trigger_response( string $incident_type, array $context = array() ): void { // 1. Activate lockdown if configured. if ( Settings::get_option( 'advana_security_auto_lockdown' ) ) { self::activate_lockdown( $incident_type ); } // 2. Send admin notification. if ( Settings::get_option( 'advana_security_email_alerts' ) ) { self::send_alert_email( $incident_type, $context ); } // 3. Extensibility: allow other plugins/modules to respond. \do_action( 'advan_security_incident_detected', $incident_type, $context ); } /** * Activate site lockdown. * * Temporarily tightens all rate limits and enables additional protections. * * @param string $reason Reason for lockdown. * * @return void * * @since 5.2.0 */ private static function activate_lockdown( string $reason ): void { $duration = (int) ( Settings::get_option( 'security_lockdown_duration_minutes' ) ?: self::DEFAULT_LOCKDOWN_DURATION_MINUTES ); $lockdown_data = array( 'activated_at' => time(), 'expires_at' => time() + ( $duration * 60 ), 'reason' => $reason, ); \set_transient( self::LOCKDOWN_TRANSIENT, $lockdown_data, $duration * 60 ); Security_Events_Entity::log_event( Security_Events_Entity::EVENT_INCIDENT, Security_Events_Entity::SEVERITY_CRITICAL, '', sprintf( 'Auto-lockdown activated for %d minutes due to: %s', $duration, $reason ), $lockdown_data ); } /** * Check if lockdown is currently active. * * @return bool * * @since 5.2.0 */ public static function is_lockdown_active(): bool { $data = \get_transient( self::LOCKDOWN_TRANSIENT ); if ( ! is_array( $data ) || ! isset( $data['expires_at'] ) ) { return false; } return time() < (int) $data['expires_at']; } /** * Get lockdown info. * * @return array|false Lockdown data or false. * * @since 5.2.0 */ public static function get_lockdown_info() { $data = \get_transient( self::LOCKDOWN_TRANSIENT ); if ( ! is_array( $data ) ) { return false; } return $data; } /** * Manually deactivate lockdown. * * @return void * * @since 5.2.0 */ public static function deactivate_lockdown(): void { \delete_transient( self::LOCKDOWN_TRANSIENT ); Security_Events_Entity::log_event( Security_Events_Entity::EVENT_INCIDENT, Security_Events_Entity::SEVERITY_MEDIUM, '', 'Lockdown manually deactivated by admin', array( 'deactivated_at' => time() ) ); } /** * Apply lockdown rate limits (filter callback). * * During lockdown, rate limits are reduced by 75% and burst limits by 80%. * * @param array $limits Current rate limits. * * @return array Modified limits. * * @since 5.2.0 */ public static function apply_lockdown_limits( array $limits ): array { $limits['rate_limit'] = max( 10, (int) ( ( $limits['rate_limit'] ?? 60 ) * 0.25 ) ); $limits['burst_limit'] = max( 3, (int) ( ( $limits['burst_limit'] ?? 15 ) * 0.20 ) ); return $limits; } /** * Send alert email to site admin. * * @param string $incident_type Incident type. * @param array $context Context. * * @return void * * @since 5.2.0 */ private static function send_alert_email( string $incident_type, array $context ): void { // Rate limit email notifications (max 1 per 15 minutes per incident type). $email_key = ADVAN_PREFIX . 'security_email_' . $incident_type; if ( false !== \get_transient( $email_key ) ) { return; } \set_transient( $email_key, 1, 900 ); $admin_email = \get_option( 'admin_email' ); if ( empty( $admin_email ) ) { return; } $site_name = \get_bloginfo( 'name' ); $site_url = \home_url(); $type_labels = array( 'ddos' => __( 'DDoS Attack', '0-day-analytics' ), 'brute_force' => __( 'Brute Force Campaign', '0-day-analytics' ), 'escalation' => __( 'Privilege Escalation Chain', '0-day-analytics' ), ); $type_label = $type_labels[ $incident_type ] ?? ucfirst( str_replace( '_', ' ', $incident_type ) ); $subject = sprintf( /* translators: 1: site name, 2: incident type */ __( '[%1$s] Security Alert: %2$s Detected', '0-day-analytics' ), $site_name, $type_label ); $message = sprintf( /* translators: 1: incident type, 2: site URL, 3: context details, 4: timestamp */ __( "A security incident has been detected on your site.\n\n" . "Incident Type: %1\$s\n" . "Site: %2\$s\n" . "Time: %3\$s (UTC)\n\n" . "Details:\n%4\$s\n\n" . "Recommended Actions:\n" . "- Review the Security Dashboard in 0 Day Analytics\n" . "- Check recent user activity and role changes\n" . "- Verify no unauthorized changes were made\n\n" . "This is an automated alert from the 0 Day Analytics Security Module.", '0-day-analytics' ), $type_label, $site_url, gmdate( 'Y-m-d H:i:s' ), self::format_context( $context ) ); \wp_mail( $admin_email, $subject, $message ); } /** * Format context array for email display. * * @param array $context Context data. * * @return string * * @since 5.2.0 */ private static function format_context( array $context ): string { $lines = array(); foreach ( $context as $key => $value ) { $label = ucfirst( str_replace( '_', ' ', $key ) ); $display = is_scalar( $value ) ? (string) $value : wp_json_encode( $value ); $lines[] = sprintf( ' %s: %s', $label, $display ); } return implode( "\n", $lines ); } // ─── Cron management ──────────────────────────────────────────── /** * Schedule the analysis cron. * * @return void * * @since 5.2.0 */ private static function schedule_cron(): void { if ( ! \wp_next_scheduled( self::CRON_ANALYSIS_HOOK ) ) { \wp_schedule_event( time(), 'advan_five_minutes', self::CRON_ANALYSIS_HOOK ); } } /** * Add custom cron schedules. * * @param array $schedules Existing schedules. * * @return array * * @since 5.2.0 */ public static function add_cron_schedules( array $schedules ): array { if ( ! isset( $schedules['advan_five_minutes'] ) ) { $schedules['advan_five_minutes'] = array( 'interval' => 300, 'display' => \__( 'Every 5 minutes', '0-day-analytics' ), ); } return $schedules; } /** * Unschedule the analysis cron. * * @return void * * @since 5.2.0 */ public static function unschedule_cron(): void { $timestamp = \wp_next_scheduled( self::CRON_ANALYSIS_HOOK ); if ( $timestamp ) { \wp_unschedule_event( $timestamp, self::CRON_ANALYSIS_HOOK ); } } } }