Automatic WordPress security: local firewall, malware and file-integrity scanning, vulnerability protection, quarantine, scheduled backups, an optional page cache, and automatic global threat intelligence. See README.md for installation, update, and uninstall instructions.
586 lines
20 KiB
PHP
586 lines
20 KiB
PHP
<?php
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class Argus_ANIS_Client {
|
|
|
|
const STATUS_OPTION = 'argus_wpd_anis_status';
|
|
const TIER_OPTION = 'argus_wpd_anis_tier';
|
|
const LAST_ERROR_OPTION = 'argus_wpd_anis_last_error';
|
|
const LAST_REGISTER_OPTION = 'argus_wpd_anis_last_register';
|
|
|
|
const LAST_SYNC_OPTION = 'argus_wpd_anis_last_sync';
|
|
const LAST_SUCCESS_OPTION = 'argus_wpd_anis_last_success';
|
|
const RETRY_STAGE_OPTION = 'argus_wpd_anis_retry_stage';
|
|
const NEXT_RETRY_AT_OPTION = 'argus_wpd_anis_next_retry_at';
|
|
const SYNC_WATERMARK_OPTION = 'argus_wpd_anis_sync_watermark';
|
|
const FEATURES_OPTION = 'argus_wpd_anis_features';
|
|
|
|
const SINCE_FORMAT = 'Y-m-d H:i:s';
|
|
|
|
const DELTA_RETENTION_DAYS = 7;
|
|
const SAFETY_MARGIN_DAYS = 6;
|
|
const DRIFT_CORRECTION_HOURS = 24;
|
|
|
|
const REPORT_SCENARIOS = array( 'http_crawl', 'brute_force', 'waf_block', 'scanner', 'exploit_attempt', 'spam' );
|
|
|
|
const CIRCUIT_OPTION = 'argus_wpd_anis_circuit';
|
|
const CIRCUIT_FAILURE_THRESHOLD = 3;
|
|
const CIRCUIT_COOLDOWN_SECS = 300;
|
|
|
|
const AUTO_BAN_CONFIDENCE_THRESHOLD = 70;
|
|
|
|
const MAX_LOCAL_REPUTATION_ROWS = 50000;
|
|
|
|
public static function init() {
|
|
add_action( 'argus_wpd_ip_banned', array( __CLASS__, 'maybe_report' ), 10, 4 );
|
|
}
|
|
|
|
public static function is_configured() {
|
|
return '' !== trim( self::base_url() );
|
|
}
|
|
|
|
public static function is_enabled() {
|
|
if ( defined( 'ARGUS_WPD_ANIS_DISABLED' ) && ARGUS_WPD_ANIS_DISABLED ) {
|
|
return false;
|
|
}
|
|
return (bool) Argus_Settings::get( 'anis_enabled', true ) && self::is_configured();
|
|
}
|
|
|
|
public static function is_connected() {
|
|
return self::is_enabled() && 'registered' === get_option( self::STATUS_OPTION, '' );
|
|
}
|
|
|
|
protected static function circuit_state() {
|
|
return wp_parse_args(
|
|
get_option( self::CIRCUIT_OPTION, array() ),
|
|
array( 'state' => 'closed', 'failures' => 0, 'opened_at' => 0 )
|
|
);
|
|
}
|
|
|
|
protected static function circuit_allows_request() {
|
|
$circuit = self::circuit_state();
|
|
|
|
if ( 'closed' === $circuit['state'] ) {
|
|
return true;
|
|
}
|
|
|
|
if ( 'open' === $circuit['state'] ) {
|
|
if ( time() - (int) $circuit['opened_at'] >= self::CIRCUIT_COOLDOWN_SECS ) {
|
|
|
|
$circuit['state'] = 'half_open';
|
|
update_option( self::CIRCUIT_OPTION, $circuit, false );
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected static function circuit_record_success() {
|
|
update_option( self::CIRCUIT_OPTION, array( 'state' => 'closed', 'failures' => 0, 'opened_at' => 0 ), false );
|
|
}
|
|
|
|
protected static function circuit_record_failure() {
|
|
$circuit = self::circuit_state();
|
|
$failures = (int) $circuit['failures'] + 1;
|
|
|
|
if ( 'half_open' === $circuit['state'] || $failures >= self::CIRCUIT_FAILURE_THRESHOLD ) {
|
|
if ( 'open' !== $circuit['state'] ) {
|
|
Argus_Events::record( 'anis_circuit_open', 'medium', 'ANIS appears unreachable -- pausing outbound requests temporarily.', array( 'failures' => $failures ) );
|
|
}
|
|
update_option( self::CIRCUIT_OPTION, array( 'state' => 'open', 'failures' => $failures, 'opened_at' => time() ), false );
|
|
return;
|
|
}
|
|
|
|
update_option( self::CIRCUIT_OPTION, array( 'state' => 'closed', 'failures' => $failures, 'opened_at' => 0 ), false );
|
|
}
|
|
|
|
public static function circuit_is_open() {
|
|
return 'open' === self::circuit_state()['state'];
|
|
}
|
|
|
|
protected static function base_url() {
|
|
if ( defined( 'ARGUS_WPD_ANIS_BASE_URL' ) && ARGUS_WPD_ANIS_BASE_URL ) {
|
|
return untrailingslashit( ARGUS_WPD_ANIS_BASE_URL );
|
|
}
|
|
return untrailingslashit( (string) Argus_Settings::get( 'anis_base_url', '' ) );
|
|
}
|
|
|
|
protected static function license_key() {
|
|
if ( defined( 'ARGUS_WPD_ANIS_LICENSE_KEY' ) && ARGUS_WPD_ANIS_LICENSE_KEY ) {
|
|
return trim( ARGUS_WPD_ANIS_LICENSE_KEY );
|
|
}
|
|
return trim( (string) Argus_Settings::get( 'anis_license_key', '' ) );
|
|
}
|
|
|
|
const INSTALL_SECRET_OPTION = 'argus_wpd_anis_install_secret';
|
|
|
|
public static function instance_hash() {
|
|
$secret = get_option( self::INSTALL_SECRET_OPTION );
|
|
if ( ! $secret ) {
|
|
$secret = bin2hex( random_bytes( 24 ) );
|
|
update_option( self::INSTALL_SECRET_OPTION, $secret, false );
|
|
}
|
|
return substr( hash( 'sha256', $secret ), 0, 16 );
|
|
}
|
|
|
|
public static function register() {
|
|
if ( ! self::is_configured() ) {
|
|
return array( 'success' => false, 'message' => __( 'Set an ANIS server address first.', 'argus-wordpress-defence' ) );
|
|
}
|
|
if ( ! self::circuit_allows_request() ) {
|
|
return array( 'success' => false, 'message' => __( 'ANIS is temporarily unreachable -- will retry automatically.', 'argus-wordpress-defence' ) );
|
|
}
|
|
|
|
$license_key = self::license_key();
|
|
$body = array( 'instance_hash' => self::instance_hash(), 'version' => ARGUS_WPD_VERSION );
|
|
if ( '' !== $license_key ) {
|
|
$body['license_key'] = $license_key;
|
|
}
|
|
|
|
$response = wp_remote_post(
|
|
self::base_url() . '/api/v1/instances/register',
|
|
array(
|
|
'timeout' => 15,
|
|
'headers' => array( 'Content-Type' => 'application/json' ),
|
|
'body' => wp_json_encode( $body ),
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
self::circuit_record_failure();
|
|
self::record_error( $response->get_error_message() );
|
|
return array( 'success' => false, 'message' => $response->get_error_message() );
|
|
}
|
|
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
|
self::circuit_record_failure();
|
|
$message = self::extract_error_message( $response );
|
|
self::record_error( $message );
|
|
return array( 'success' => false, 'message' => $message );
|
|
}
|
|
|
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( ! is_array( $data ) || empty( $data['status'] ) ) {
|
|
self::circuit_record_failure();
|
|
self::record_error( __( 'ANIS returned an unexpected response.', 'argus-wordpress-defence' ) );
|
|
return array( 'success' => false, 'message' => __( 'ANIS returned an unexpected response.', 'argus-wordpress-defence' ) );
|
|
}
|
|
|
|
self::circuit_record_success();
|
|
update_option( self::STATUS_OPTION, 'registered', false );
|
|
update_option( self::TIER_OPTION, $data['tier'] ?? 'community', false );
|
|
update_option( self::LAST_REGISTER_OPTION, current_time( 'mysql', true ), false );
|
|
update_option( self::LAST_ERROR_OPTION, '', false );
|
|
if ( isset( $data['features'] ) && is_array( $data['features'] ) ) {
|
|
update_option( self::FEATURES_OPTION, $data['features'], false );
|
|
}
|
|
|
|
Argus_Events::record( 'anis_registered', 'info', sprintf( 'Connected to ANIS (%s tier)', $data['tier'] ?? 'community' ), array( 'tier' => $data['tier'] ?? 'community' ) );
|
|
|
|
return array( 'success' => true, 'message' => sprintf( __( 'Connected -- %s tier.', 'argus-wordpress-defence' ), $data['tier'] ?? 'community' ) );
|
|
}
|
|
|
|
protected static function record_error( $message ) {
|
|
update_option( self::STATUS_OPTION, 'error', false );
|
|
update_option( self::LAST_ERROR_OPTION, $message, false );
|
|
}
|
|
|
|
protected static function extract_error_message( $response ) {
|
|
$code = (int) wp_remote_retrieve_response_code( $response );
|
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( is_array( $body ) && ! empty( $body['message'] ) ) {
|
|
return sprintf( 'ANIS returned %d: %s', $code, $body['message'] );
|
|
}
|
|
return sprintf( 'ANIS returned HTTP %d.', $code );
|
|
}
|
|
|
|
public static function sync() {
|
|
if ( ! self::is_connected() ) {
|
|
return array( 'success' => false, 'message' => __( 'Not connected to ANIS.', 'argus-wordpress-defence' ) );
|
|
}
|
|
|
|
$watermark = get_option( self::SYNC_WATERMARK_OPTION, '' );
|
|
$last_full = get_option( self::LAST_SYNC_OPTION, '' );
|
|
|
|
$needs_snapshot = '' === $watermark
|
|
|| ( '' !== $watermark && strtotime( $watermark . ' UTC' ) < strtotime( '-' . self::SAFETY_MARGIN_DAYS . ' days', current_time( 'timestamp', true ) ) )
|
|
|| ( '' !== $last_full && strtotime( $last_full . ' UTC' ) < strtotime( '-' . self::DRIFT_CORRECTION_HOURS . ' hours', current_time( 'timestamp', true ) ) );
|
|
|
|
if ( $needs_snapshot ) {
|
|
return self::sync_snapshot();
|
|
}
|
|
|
|
$result = self::sync_delta();
|
|
if ( ! $result['success'] ) {
|
|
return self::sync_snapshot();
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
const RETRY_DELAYS_MINUTES = array( 5, 5, 15, 30 );
|
|
|
|
public static function scheduled_sync() {
|
|
$result = self::sync();
|
|
if ( $result['success'] ) {
|
|
self::clear_retry_ladder();
|
|
} else {
|
|
self::start_retry_ladder();
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
protected static function start_retry_ladder() {
|
|
update_option( self::RETRY_STAGE_OPTION, 0, false );
|
|
update_option( self::NEXT_RETRY_AT_OPTION, time() + ( self::RETRY_DELAYS_MINUTES[0] * MINUTE_IN_SECONDS ), false );
|
|
}
|
|
|
|
protected static function clear_retry_ladder() {
|
|
delete_option( self::RETRY_STAGE_OPTION );
|
|
delete_option( self::NEXT_RETRY_AT_OPTION );
|
|
}
|
|
|
|
public static function maybe_retry_sync() {
|
|
$stage = get_option( self::RETRY_STAGE_OPTION, null );
|
|
if ( null === $stage ) {
|
|
return;
|
|
}
|
|
if ( time() < (int) get_option( self::NEXT_RETRY_AT_OPTION, 0 ) ) {
|
|
return;
|
|
}
|
|
|
|
$result = self::sync();
|
|
if ( $result['success'] ) {
|
|
self::clear_retry_ladder();
|
|
return;
|
|
}
|
|
|
|
$next_stage = (int) $stage + 1;
|
|
if ( ! isset( self::RETRY_DELAYS_MINUTES[ $next_stage ] ) ) {
|
|
self::clear_retry_ladder();
|
|
return;
|
|
}
|
|
update_option( self::RETRY_STAGE_OPTION, $next_stage, false );
|
|
update_option( self::NEXT_RETRY_AT_OPTION, time() + ( self::RETRY_DELAYS_MINUTES[ $next_stage ] * MINUTE_IN_SECONDS ), false );
|
|
}
|
|
|
|
public static function sync_snapshot() {
|
|
$response = self::get( '/api/v1/intelligence/decisions' );
|
|
if ( is_wp_error( $response ) ) {
|
|
return array( 'success' => false, 'message' => $response->get_error_message() );
|
|
}
|
|
|
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( ! is_array( $data ) || ! isset( $data['decisions'] ) ) {
|
|
return array( 'success' => false, 'message' => __( 'ANIS returned an unexpected decisions response.', 'argus-wordpress-defence' ) );
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = Argus_DB::table( 'anis_reputation' );
|
|
|
|
$wpdb->query( "TRUNCATE TABLE {$table}" ); // phpcs:ignore
|
|
|
|
foreach ( (array) $data['decisions'] as $decision ) {
|
|
self::upsert_decision( $decision );
|
|
}
|
|
|
|
self::enforce_reputation_cap();
|
|
|
|
$now = current_time( 'mysql', true );
|
|
update_option( self::LAST_SYNC_OPTION, $now, false );
|
|
update_option( self::LAST_SUCCESS_OPTION, $now, false );
|
|
update_option( self::SYNC_WATERMARK_OPTION, gmdate( self::SINCE_FORMAT ), false );
|
|
|
|
return array( 'success' => true, 'message' => sprintf( __( 'Synced %d decision(s) from ANIS (full snapshot).', 'argus-wordpress-defence' ), count( $data['decisions'] ) ) );
|
|
}
|
|
|
|
public static function sync_delta() {
|
|
$since = get_option( self::SYNC_WATERMARK_OPTION, '' );
|
|
if ( '' === $since ) {
|
|
return array( 'success' => false, 'message' => 'no watermark' );
|
|
}
|
|
|
|
$response = self::get( '/api/v1/intelligence/decisions/delta?since=' . rawurlencode( $since ) );
|
|
if ( is_wp_error( $response ) ) {
|
|
return array( 'success' => false, 'message' => $response->get_error_message() );
|
|
}
|
|
|
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( ! is_array( $data ) || ! isset( $data['events'] ) ) {
|
|
return array( 'success' => false, 'message' => __( 'ANIS returned an unexpected delta response.', 'argus-wordpress-defence' ) );
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = Argus_DB::table( 'anis_reputation' );
|
|
|
|
foreach ( (array) $data['events'] as $event ) {
|
|
if ( 'remove' === ( $event['event'] ?? '' ) ) {
|
|
$wpdb->delete( $table, array( 'ip' => $event['ip'] ?? '' ), array( '%s' ) );
|
|
continue;
|
|
}
|
|
self::upsert_decision( $event );
|
|
}
|
|
|
|
update_option( self::SYNC_WATERMARK_OPTION, gmdate( self::SINCE_FORMAT ), false );
|
|
update_option( self::LAST_SUCCESS_OPTION, current_time( 'mysql', true ), false );
|
|
self::enforce_reputation_cap();
|
|
|
|
return array( 'success' => true, 'message' => sprintf( __( 'Synced %d change(s) from ANIS (incremental).', 'argus-wordpress-defence' ), count( $data['events'] ) ) );
|
|
}
|
|
|
|
protected static function enforce_reputation_cap() {
|
|
global $wpdb;
|
|
$table = Argus_DB::table( 'anis_reputation' );
|
|
|
|
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore
|
|
if ( $total <= self::MAX_LOCAL_REPUTATION_ROWS ) {
|
|
return;
|
|
}
|
|
|
|
$overflow = $total - self::MAX_LOCAL_REPUTATION_ROWS;
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$table} ORDER BY confidence ASC, cached_at ASC LIMIT %d", // phpcs:ignore
|
|
$overflow
|
|
)
|
|
);
|
|
}
|
|
|
|
protected static function upsert_decision( array $decision ) {
|
|
if ( empty( $decision['ip'] ) ) {
|
|
return;
|
|
}
|
|
global $wpdb;
|
|
$table = Argus_DB::table( 'anis_reputation' );
|
|
$now = current_time( 'mysql', true );
|
|
|
|
$decided_at = ! empty( $decision['created_at'] ) ? "'" . esc_sql( gmdate( 'Y-m-d H:i:s', strtotime( $decision['created_at'] ) ) ) . "'" : 'NULL';
|
|
$expires_at = ! empty( $decision['expires_at'] ) ? "'" . esc_sql( gmdate( 'Y-m-d H:i:s', strtotime( $decision['expires_at'] ) ) ) . "'" : 'NULL';
|
|
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"INSERT INTO {$table} (ip, action, confidence, reports, source, reason, decided_at, expires_at, cached_at)
|
|
VALUES (%s, %s, %d, %d, %s, %s, {$decided_at}, {$expires_at}, %s)
|
|
ON DUPLICATE KEY UPDATE action = VALUES(action), confidence = VALUES(confidence), reports = VALUES(reports), source = VALUES(source), reason = VALUES(reason), decided_at = VALUES(decided_at), expires_at = VALUES(expires_at), cached_at = VALUES(cached_at)", // phpcs:ignore
|
|
$decision['ip'],
|
|
$decision['action'] ?? 'allow',
|
|
(int) ( $decision['confidence'] ?? 0 ),
|
|
(int) ( $decision['reports'] ?? 0 ),
|
|
$decision['source'] ?? '',
|
|
$decision['reason'] ?? '',
|
|
$now
|
|
)
|
|
);
|
|
}
|
|
|
|
public static function cached_reputation( $ip ) {
|
|
global $wpdb;
|
|
$table = Argus_DB::table( 'anis_reputation' );
|
|
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE ip = %s", $ip ) ); // phpcs:ignore
|
|
}
|
|
|
|
public static function maybe_enforce( $ip ) {
|
|
if ( ! self::is_enabled() ) {
|
|
return false;
|
|
}
|
|
$row = self::cached_reputation( $ip );
|
|
if ( ! $row || 'ban' !== $row->action || (int) $row->confidence < self::AUTO_BAN_CONFIDENCE_THRESHOLD ) {
|
|
return false;
|
|
}
|
|
if ( $row->expires_at && strtotime( $row->expires_at . ' UTC' ) < time() ) {
|
|
return false;
|
|
}
|
|
|
|
Argus_Ban_Engine::ban(
|
|
$ip,
|
|
Argus_Ban_Engine::SOURCE_ARGUS_CLOUD,
|
|
sprintf( 'Flagged by ANIS community threat intelligence (confidence %d, %d report(s))', $row->confidence, $row->reports ),
|
|
array( 'anis_reason' => $row->reason, 'anis_source' => $row->source ),
|
|
Argus_Ban_Engine::LEVEL_EXTENDED
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
public static function maybe_report( $ip, $source, $reason, $level ) {
|
|
if ( ! self::is_enabled() ) {
|
|
return;
|
|
}
|
|
$scenario = self::scenario_for_source( $source );
|
|
if ( ! $scenario ) {
|
|
return;
|
|
}
|
|
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
|
|
return;
|
|
}
|
|
|
|
self::report_async( $ip, $scenario );
|
|
}
|
|
|
|
protected static function scenario_for_source( $source ) {
|
|
$map = array(
|
|
Argus_Ban_Engine::SOURCE_LOCAL_WAF => 'waf_block',
|
|
Argus_Ban_Engine::SOURCE_BRUTE_FORCE => 'brute_force',
|
|
);
|
|
return $map[ $source ] ?? null;
|
|
}
|
|
|
|
public static function report_async( $ip, $scenario, $confidence = null, $country = null ) {
|
|
if ( ! in_array( $scenario, self::REPORT_SCENARIOS, true ) ) {
|
|
return;
|
|
}
|
|
if ( self::circuit_is_open() ) {
|
|
return;
|
|
}
|
|
$body = array(
|
|
'instance_hash' => self::instance_hash(),
|
|
'ip' => $ip,
|
|
'scenario' => $scenario,
|
|
);
|
|
if ( null !== $confidence ) {
|
|
$body['confidence'] = max( 1, min( 100, (int) $confidence ) );
|
|
}
|
|
if ( $country ) {
|
|
$body['country'] = $country;
|
|
}
|
|
|
|
wp_remote_post(
|
|
self::base_url() . '/api/v1/intelligence/report',
|
|
array(
|
|
'timeout' => 5,
|
|
'blocking' => false,
|
|
'headers' => array( 'Content-Type' => 'application/json' ),
|
|
'body' => wp_json_encode( $body ),
|
|
)
|
|
);
|
|
}
|
|
|
|
public static function check_ip_live( $ip ) {
|
|
if ( ! self::is_enabled() || ! self::circuit_allows_request() ) {
|
|
return null;
|
|
}
|
|
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
|
|
return null;
|
|
}
|
|
|
|
$instance = self::instance_hash();
|
|
$args = array( 'timeout' => 2 );
|
|
if ( $instance ) {
|
|
$args['headers'] = array( 'X-ANIS-Instance' => $instance );
|
|
}
|
|
|
|
$response = wp_remote_get( self::base_url() . '/api/v1/intelligence/check/' . rawurlencode( $ip ), $args );
|
|
|
|
if ( is_wp_error( $response ) || (int) wp_remote_retrieve_response_code( $response ) >= 500 ) {
|
|
self::circuit_record_failure();
|
|
return null;
|
|
}
|
|
self::circuit_record_success();
|
|
|
|
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
|
return null;
|
|
}
|
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( ! is_array( $data ) || ! isset( $data['action'] ) ) {
|
|
return null;
|
|
}
|
|
|
|
return array( 'action' => $data['action'], 'score' => (int) ( $data['score'] ?? 0 ) );
|
|
}
|
|
|
|
protected static function get( $path ) {
|
|
if ( ! self::circuit_allows_request() ) {
|
|
return new WP_Error( 'argus_anis_circuit_open', __( 'ANIS is temporarily unreachable.', 'argus-wordpress-defence' ) );
|
|
}
|
|
|
|
$args = array( 'timeout' => 15 );
|
|
$instance = self::instance_hash();
|
|
if ( $instance ) {
|
|
$args['headers'] = array( 'X-ANIS-Instance' => $instance );
|
|
}
|
|
$response = wp_remote_get( self::base_url() . $path, $args );
|
|
|
|
if ( is_wp_error( $response ) || (int) wp_remote_retrieve_response_code( $response ) >= 500 ) {
|
|
|
|
self::circuit_record_failure();
|
|
} else {
|
|
self::circuit_record_success();
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
public static function stats() {
|
|
if ( ! self::is_connected() ) {
|
|
return null;
|
|
}
|
|
$response = self::get( '/api/v1/intelligence/stats' );
|
|
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
|
return null;
|
|
}
|
|
return json_decode( wp_remote_retrieve_body( $response ), true );
|
|
}
|
|
|
|
public static function feeds_status() {
|
|
if ( ! self::is_connected() ) {
|
|
return null;
|
|
}
|
|
$response = self::get( '/api/v1/feeds/status' );
|
|
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
|
return null;
|
|
}
|
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
return is_array( $data ) ? ( $data['feeds'] ?? array() ) : array();
|
|
}
|
|
|
|
public static function local_reputation_count() {
|
|
global $wpdb;
|
|
return (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . Argus_DB::table( 'anis_reputation' ) ); // phpcs:ignore
|
|
}
|
|
|
|
public static function blocked_count() {
|
|
global $wpdb;
|
|
return (int) $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
'SELECT COUNT(*) FROM ' . Argus_DB::table( 'bans' ) . ' WHERE source = %s', // phpcs:ignore
|
|
Argus_Ban_Engine::SOURCE_ARGUS_CLOUD
|
|
)
|
|
);
|
|
}
|
|
|
|
const SYNC_INTERVAL_SECS = HOUR_IN_SECONDS;
|
|
|
|
public static function status() {
|
|
$last_success = get_option( self::LAST_SUCCESS_OPTION, '' );
|
|
|
|
return array(
|
|
'configured' => self::is_configured(),
|
|
'enabled' => (bool) Argus_Settings::get( 'anis_enabled', true ),
|
|
'connected' => self::is_connected(),
|
|
'status' => get_option( self::STATUS_OPTION, 'unconfigured' ),
|
|
'last_error' => get_option( self::LAST_ERROR_OPTION, '' ),
|
|
|
|
'last_sync' => $last_success,
|
|
'next_sync' => $last_success ? gmdate( 'Y-m-d H:i:s', strtotime( $last_success . ' UTC' ) + self::SYNC_INTERVAL_SECS ) : '',
|
|
'local_count' => self::local_reputation_count(),
|
|
'blocked_count' => self::blocked_count(),
|
|
'circuit_open' => self::circuit_is_open(),
|
|
|
|
'protection' => self::protection_state(),
|
|
);
|
|
}
|
|
|
|
protected static function protection_state() {
|
|
if ( ! self::is_enabled() ) {
|
|
return 'not_connected';
|
|
}
|
|
if ( self::circuit_is_open() && self::is_connected() ) {
|
|
return 'limited';
|
|
}
|
|
return self::is_connected() ? 'active' : 'not_connected';
|
|
}
|
|
}
|