= $threshold ) { return self::decision( self::ACTION_BLOCK, 'brute-force-threshold', sprintf( '%d failed login attempts in the tracked window', $attempts ), $mode, 'login_failure' ); } if ( $attempts >= max( 1, (int) ceil( $threshold / 2 ) ) ) { return self::decision( self::ACTION_CHALLENGE, 'login-elevated', sprintf( '%d failed login attempts -- challenging before further tries', $attempts ), $mode, 'login_failure' ); } return self::decision( self::ACTION_ALLOW, 'login-under-threshold', 'Failure count below challenge threshold', $mode, 'login_failure' ); } protected static function decision( $action, $rule_name, $reason, $mode, $trigger ) { return array( 'action' => $action, 'rule_name' => $rule_name, 'reason' => $reason, 'observation_only' => Argus_Settings::MODE_MONITOR === $mode, 'mode' => $mode, 'trigger' => $trigger, ); } public static function evaluate_and_enforce( $ip, $trigger, array $context = array() ) { $decision = self::evaluate( $ip, $trigger, $context ); self::enforce_decision( $ip, $trigger, $decision, $context ); return $decision; } public static function enforce_decision( $ip, $trigger, array $decision, array $context = array() ) { global $wpdb; $wpdb->insert( Argus_DB::table( 'policy_decisions' ), array( 'ip' => $ip, 'trigger_type' => $trigger, 'action' => $decision['action'], 'rule_name' => $decision['rule_name'], 'reason' => $decision['reason'], 'observation_only' => $decision['observation_only'] ? 1 : 0, 'mode' => $decision['mode'], 'created_at' => current_time( 'mysql', true ), ), array( '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s' ) ); if ( self::ACTION_ALLOW === $decision['action'] || $decision['observation_only'] ) { if ( self::ACTION_ALLOW !== $decision['action'] ) { Argus_Events::record( 'policy_observed', 'low', 'Would have ' . $decision['action'] . ': ' . $decision['reason'], $decision, $ip ); } return; } self::enforce( $decision, $ip, $context ); } public static function deny_already_banned( $ip ) { self::deny_and_exit( $ip, 'IP is currently banned' ); } protected static function enforce( array $decision, $ip, array $context ) { switch ( $decision['action'] ) { case self::ACTION_BLOCK: Argus_Ban_Engine::ban( $ip, 'waf_match' === $decision['trigger'] ? Argus_Ban_Engine::SOURCE_LOCAL_WAF : Argus_Ban_Engine::SOURCE_POLICY_ENGINE, $decision['reason'], array( 'rule_name' => $decision['rule_name'], 'context' => $context ) ); self::deny_and_exit( $ip, $decision['reason'] ); break; case self::ACTION_CHALLENGE: if ( ! Argus_Challenge::has_valid_pass( $ip ) ) { Argus_Events::record( 'challenge_issued', 'medium', 'Challenge issued: ' . $decision['reason'], $decision, $ip ); Argus_Challenge::render_and_exit( $ip ); } break; case self::ACTION_RATE_LIMIT: self::deny_and_exit( $ip, $decision['reason'], 429 ); break; } } protected static function deny_and_exit( $ip, $reason, $status = 403 ) { nocache_headers(); status_header( $status ); header( 'Content-Type: text/plain; charset=utf-8' ); echo 'Forbidden.'; exit; } const RETENTION_DAYS = 90; public static function prune() { global $wpdb; $table = Argus_DB::table( 'policy_decisions' ); $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( self::RETENTION_DAYS * DAY_IN_SECONDS ) ); return (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) ); // phpcs:ignore } }