get_results( $wpdb->prepare( "SELECT * FROM {$events_table} WHERE event_type IN ('ban','policy_observed','login_failed') AND severity IN ('high','critical') AND ip IS NOT NULL AND created_at >= %s ORDER BY created_at ASC", // phpcs:ignore $since ) ); if ( empty( $suspicious ) ) { update_option( self::WATERMARK_OPTION, $now, false ); return 0; } $file_events = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$events_table} WHERE event_type IN ('integrity_new_file','integrity_changed_file') AND created_at >= %s ORDER BY created_at ASC", // phpcs:ignore $since ) ); $by_ip = array(); foreach ( $suspicious as $event ) { $by_ip[ $event->ip ][] = $event; } $correlated = 0; foreach ( $by_ip as $ip => $ip_events ) { $request_event = $ip_events[0]; $request_time = strtotime( $request_event->created_at . ' UTC' ); $best_delta = null; $best_file = null; $best_payload = null; foreach ( $file_events as $file_event ) { $payload = json_decode( $file_event->payload, true ); $file_mtime = isset( $payload['file_mtime'] ) ? strtotime( $payload['file_mtime'] . ' UTC' ) : null; if ( null === $file_mtime ) { continue; } $delta = $file_mtime - $request_time; if ( $delta < 0 || $delta > self::WINDOW_SECONDS ) { continue; } if ( null === $best_delta || $delta < $best_delta ) { $best_delta = $delta; $best_file = $file_event; $best_payload = $payload; } } if ( null !== $best_file ) { self::record_correlated_finding( $request_event, $best_file, $best_payload, $best_delta ); $correlated++; } } update_option( self::WATERMARK_OPTION, $now, false ); return $correlated; } protected static function record_correlated_finding( $request_event, $file_event, array $file_payload, $delta_seconds ) { Argus_Findings::record( 'correlation', 'critical', array( 'what_happened' => sprintf( 'A high-severity request from %s was followed %d seconds later by a %s: %s', $request_event->ip, $delta_seconds, 'integrity_new_file' === $file_event->event_type ? 'new file' : 'changed file', $file_payload['file_path'] ?? 'unknown' ), 'why_it_matters' => 'A suspicious request closely followed by a filesystem change is a much stronger signal than either event alone -- this is the pattern a successful exploit-then-webshell-drop typically produces.', 'what_argus_found' => sprintf( 'Request event: %s (%s, severity %s) at %s. File event: %s at %s.', $request_event->event_type, $request_event->summary, $request_event->severity, $request_event->created_at, $file_event->summary, $file_event->created_at ), 'when_it_happened' => $file_event->created_at, 'why_suspicious' => sprintf( 'The file change happened within %d seconds of the suspicious request -- close enough in time to be a plausible cause-and-effect, though ARGUS cannot prove the same actor performed both (a filesystem write carries no IP attribution).', self::WINDOW_SECONDS ), 'what_could_be_affected' => 'If this represents a successful exploit, the new/changed file may itself be malicious code with the same access as your web application.', 'what_should_you_do' => 'Review the file named above immediately, and treat the source IP as a confirmed active threat rather than a routine block.', ), array( 'request_event_id' => $request_event->id, 'file_event_id' => $file_event->id, 'ip' => $request_event->ip, 'delta_seconds' => $delta_seconds, ) ); Argus_Ban_Engine::ban( $request_event->ip, Argus_Ban_Engine::SOURCE_POLICY_ENGINE, 'Correlated with a filesystem change ' . $delta_seconds . 's later', array( 'request_event_id' => $request_event->id, 'file_event_id' => $file_event->id ), Argus_Ban_Engine::LEVEL_EXTENDED ); } }