'Already-banned IP retry', 'brute-force-threshold' => 'Brute-force login threshold', 'login-elevated' => 'Elevated login failure rate', 'exception-ip' => 'Allowlisted IP', 'xmlrpc_abuse' => 'XML-RPC abuse', 'rest_abuse' => 'REST API user enumeration', 'waf-no-hits' => 'No WAF signal', 'waf-low-severity' => 'Low-severity WAF signal', 'no-match' => 'No policy rule matched', ); const ACCOUNT_TRIGGERS = array( 'active-ban', 'brute-force-threshold', 'login-elevated', 'exception-ip' ); const API_TRIGGERS = array( 'xmlrpc_abuse', 'rest_abuse' ); public static function protection_state() { if ( ! Argus_Settings::get( 'waf_enabled', true ) ) { return 'action_required'; } $open = Argus_Findings::open_counts(); if ( ( $open['critical'] ?? 0 ) > 0 || ( $open['high'] ?? 0 ) > 0 ) { return 'action_required'; } if ( class_exists( 'Argus_MU_Installer' ) ) { $activated_at = get_option( 'argus_wpd_activated_at' ); $just_activated = $activated_at && ( time() - strtotime( $activated_at . ' UTC' ) ) < 5 * MINUTE_IN_SECONDS; if ( $just_activated && 'never' === Argus_MU_Installer::execution_status() ) { return 'checking'; } } return 'protected'; } public static function blocked_today() { $since = gmdate( 'Y-m-d 00:00:00' ); return self::count_actions_since( $since, 'block' ); } public static function blocked_since_days( $days ) { $since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', true ) - ( (int) $days * DAY_IN_SECONDS ) ); return self::count_actions_since( $since, 'block' ); } protected static function count_actions_since( $since_mysql, $action = null ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); if ( $action ) { return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE action = %s AND created_at >= %s", $action, $since_mysql ) // phpcs:ignore ); } return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s", $since_mysql ) // phpcs:ignore ); } public static function malicious_ip_count() { global $wpdb; $table = Argus_DB::table( 'bans' ); $now = current_time( 'mysql', true ); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT ip) FROM {$table} WHERE lifted_at IS NULL AND (expires_at IS NULL OR expires_at > %s)", // phpcs:ignore $now ) ); } public static function security_events_count( $days = 30 ) { global $wpdb; $table = Argus_DB::table( 'events' ); $since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', true ) - ( (int) $days * DAY_IN_SECONDS ) ); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s AND severity IN ('critical','high','medium')", // phpcs:ignore $since ) ); } public static function blocked_series( $range ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); switch ( $range ) { case '30d': $buckets = 30; $bucket_secs = DAY_IN_SECONDS; $fmt = 'M j'; break; case '7d': $buckets = 7; $bucket_secs = DAY_IN_SECONDS; $fmt = 'D'; break; case '24h': default: $buckets = 24; $bucket_secs = HOUR_IN_SECONDS; $fmt = 'ga'; break; } $now = (int) current_time( 'timestamp', true ); $since_epoch = $now - ( $buckets * $bucket_secs ); $since_mysql = gmdate( 'Y-m-d H:i:s', $since_epoch ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT created_at FROM {$table} WHERE action = %s AND created_at >= %s", // phpcs:ignore 'block', $since_mysql ) ); $series = array(); for ( $i = $buckets - 1; $i >= 0; $i-- ) { $bucket_start = $now - ( $i * $bucket_secs ); $key = (int) floor( $bucket_start / $bucket_secs ); $series[ $key ] = array( 'label' => gmdate( $fmt, $bucket_start ), 'value' => 0, ); } foreach ( $rows as $row ) { $ts = strtotime( $row->created_at . ' UTC' ); $key = (int) floor( $ts / $bucket_secs ); if ( isset( $series[ $key ] ) ) { $series[ $key ]['value']++; } } return array_values( $series ); } public static function blocked_per_minute_series( $minutes = 30 ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $now = (int) current_time( 'timestamp', true ); $since_epoch = $now - ( $minutes * MINUTE_IN_SECONDS ); $since_mysql = gmdate( 'Y-m-d H:i:s', $since_epoch ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT created_at FROM {$table} WHERE action = %s AND created_at >= %s", // phpcs:ignore 'block', $since_mysql ) ); $series = array(); for ( $i = $minutes - 1; $i >= 0; $i-- ) { $bucket_start = $now - ( $i * MINUTE_IN_SECONDS ); $key = (int) floor( $bucket_start / MINUTE_IN_SECONDS ); $series[ $key ] = array( 'label' => gmdate( 'H:i', $bucket_start ), 'value' => 0, ); } foreach ( $rows as $row ) { $ts = strtotime( $row->created_at . ' UTC' ); $key = (int) floor( $ts / MINUTE_IN_SECONDS ); if ( isset( $series[ $key ] ) ) { $series[ $key ]['value']++; } } return array_values( $series ); } public static function live_feed_time_label( $created_at_utc_mysql ) { $ts = strtotime( $created_at_utc_mysql . ' UTC' ); if ( wp_date( 'Y-m-d', $ts ) === wp_date( 'Y-m-d' ) ) { return wp_date( 'H:i:s', $ts ); } return wp_date( 'M j, H:i', $ts ); } public static function live_feed_description( $flag, $ip, $type ) { return sprintf( __( 'Blocked %3$s from %1$s %2$s', 'argus-wordpress-defence' ), $flag, $ip, $type ); } public static function latest_blocked_events( $limit = 10 ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT ip, rule_name, created_at FROM {$table} WHERE action = 'block' ORDER BY created_at DESC LIMIT %d", // phpcs:ignore $limit ) ); $out = array(); foreach ( $rows as $row ) { $type = self::rule_label( self::first_rule_id( $row->rule_name ) ); $out[] = array( 'ip' => $row->ip, 'flag' => Argus_GeoIP::icon( $row->ip ), 'origin' => Argus_GeoIP::label( $row->ip ), 'type' => $type, 'desc' => self::live_feed_description( Argus_GeoIP::icon( $row->ip ), $row->ip, $type ), 'created_at' => $row->created_at, ); } return $out; } const ACTIVITY_PER_PAGE = 10; public static function recent_activity_paginated( $page = 1, $per_page = self::ACTIVITY_PER_PAGE ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $page = max( 1, (int) $page ); $offset = ( $page - 1 ) * $per_page; $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE action != 'allow'" ); // phpcs:ignore $rows = $wpdb->get_results( $wpdb->prepare( "SELECT ip, trigger_type, rule_name, action, created_at FROM {$table} WHERE action != 'allow' ORDER BY created_at DESC LIMIT %d OFFSET %d", // phpcs:ignore $per_page, $offset ) ); foreach ( $rows as &$row ) { $row->label = self::rule_label( self::first_rule_id( $row->rule_name ) ); } return array( 'rows' => $rows, 'total' => $total, 'total_pages' => max( 1, (int) ceil( $total / $per_page ) ), 'page' => $page, ); } protected static function first_rule_id( $rule_name ) { $parts = explode( ',', (string) $rule_name ); return trim( $parts[0] ); } public static function firewall_rule_hits( $hours = 24 ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', true ) - ( (int) $hours * HOUR_IN_SECONDS ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT rule_name, action, COUNT(*) AS cnt FROM {$table} WHERE created_at >= %s AND action != 'allow' AND rule_name IS NOT NULL AND rule_name != '' GROUP BY rule_name, action", // phpcs:ignore $since ) ); $tally = array(); foreach ( $rows as $row ) { foreach ( explode( ',', $row->rule_name ) as $rule_id ) { $rule_id = trim( $rule_id ); if ( '' === $rule_id ) { continue; } $key = $rule_id . '|' . $row->action; $tally[ $key ] = ( $tally[ $key ] ?? 0 ) + (int) $row->cnt; } } arsort( $tally ); $tally = array_slice( $tally, 0, 10, true ); $out = array(); foreach ( $tally as $key => $cnt ) { list( $rule_id, $action ) = explode( '|', $key, 2 ); $out[] = array( 'rule_id' => $rule_id, 'label' => self::rule_label( $rule_id ), 'category' => self::rule_category_label( $rule_id ), 'action' => $action, 'hits' => $cnt, ); } return $out; } public static function attack_category_totals( $hours = 24 ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', true ) - ( (int) $hours * HOUR_IN_SECONDS ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT rule_name, COUNT(*) AS cnt FROM {$table} WHERE created_at >= %s AND action != 'allow' AND rule_name IS NOT NULL AND rule_name != '' GROUP BY rule_name", // phpcs:ignore $since ) ); $totals = array(); foreach ( $rows as $row ) { foreach ( explode( ',', $row->rule_name ) as $rule_id ) { $rule_id = trim( $rule_id ); if ( '' === $rule_id ) { continue; } $label = self::rule_category_label( $rule_id ); $totals[ $label ] = ( $totals[ $label ] ?? 0 ) + (int) $row->cnt; } } arsort( $totals ); return $totals; } public static function security_action_totals( $hours = 24 ) { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', true ) - ( (int) $hours * HOUR_IN_SECONDS ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT action, COUNT(*) AS cnt FROM {$table} WHERE created_at >= %s AND action != 'allow' GROUP BY action", // phpcs:ignore $since ) ); $labels = array( 'block' => __( 'Blocked', 'argus-wordpress-defence' ), 'challenge' => __( 'Challenged', 'argus-wordpress-defence' ), 'rate_limit' => __( 'Rate Limited', 'argus-wordpress-defence' ), ); $totals = array(); foreach ( $rows as $row ) { $label = $labels[ $row->action ] ?? ucfirst( $row->action ); $totals[ $label ] = ( $totals[ $label ] ?? 0 ) + (int) $row->cnt; } arsort( $totals ); return $totals; } protected static function waf_rule_map() { static $map = null; if ( null === $map ) { $map = array(); foreach ( Argus_WAF_Rules::corpus() as $rule ) { $map[ $rule['id'] ] = $rule; } } return $map; } public static function rule_label( $rule_id ) { if ( isset( self::waf_rule_map()[ $rule_id ] ) || isset( self::NON_WAF_LABELS[ $rule_id ] ) ) { return self::NON_WAF_LABELS[ $rule_id ] ?? self::humanize_rule_id( $rule_id ); } return self::humanize_rule_id( $rule_id ); } protected static function humanize_rule_id( $rule_id ) { return ucwords( str_replace( '-', ' ', $rule_id ) ); } public static function rule_category_label( $rule_id ) { $map = self::waf_rule_map(); if ( isset( $map[ $rule_id ] ) ) { return self::waf_category_label( $map[ $rule_id ]['category'] ); } if ( in_array( $rule_id, self::ACCOUNT_TRIGGERS, true ) ) { return __( 'Account', 'argus-wordpress-defence' ); } if ( in_array( $rule_id, self::API_TRIGGERS, true ) ) { return __( 'API Abuse', 'argus-wordpress-defence' ); } return __( 'Policy', 'argus-wordpress-defence' ); } public static function waf_category_label( $category ) { $labels = array( 'sql_injection' => __( 'SQL Injection', 'argus-wordpress-defence' ), 'xss' => __( 'Cross-Site Scripting', 'argus-wordpress-defence' ), 'rce' => __( 'Remote Code Execution', 'argus-wordpress-defence' ), 'file_access' => __( 'Path Traversal / LFI', 'argus-wordpress-defence' ), 'protocol_anomaly' => __( 'Protocol Anomaly', 'argus-wordpress-defence' ), ); return $labels[ $category ] ?? ucwords( str_replace( '_', ' ', $category ) ); } }