'Ensure standard plugin activated when Must-Use', 'test' => [$instance, 'get_test_mu_ensure_activated'] ]; $tests['direct']['wp_fail2ban_log_comments_extra_deprecaed'] = [ 'label' => 'WP_FAIL2BAN_LOG_COMMENTS_EXTRA deprecated', 'test' => [$instance, 'get_test_log_comments_extra_deprecated'] ]; $tests['direct']['wp_fail2ban_comments_extra_log_deprecaed'] = [ 'label' => 'WP_FAIL2BAN_COMMENTS_EXTRA_LOG deprecated', 'test' => [$instance, 'get_test_comments_extra_log_deprecated'] ]; $tests['direct']['wp_fail2ban_running'] = [ 'label' => 'fail2ban running', 'test' => [$instance, 'get_test_fail2ban_running'] ]; if (!self::should_skip_filters()) { $tests['direct']['wp_fail2ban_filter_obsolete'] = [ 'label' => 'WP fail2ban obsolete filters', 'test' => [$instance, 'get_test_filter_obsolete'] ]; $tests['direct']['wp_fail2ban_filter_modified'] = [ 'label' => 'WP fail2ban modified filters', 'test' => [$instance, 'get_test_filter_modified'] ]; $tests['direct']['wp_fail2ban_filter_missing'] = [ 'label' => 'WP fail2ban missing filters', 'test' => [$instance, 'get_test_filter_missing'] ]; } if (!defined('WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND') || ( is_array(WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND) && true !== WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND['addon']['wpf2b-addon-blocklist'] ?? false)) { $tests['direct']['wp_fail2ban_blocklist_installed'] = [ 'label' => 'WP fail2ban Blocklist installed', 'test' => [$instance, 'get_test_blocklist_installed'] ]; } if (!defined('WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND') || ( is_array(WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND) && true !== WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND['addon']['wp-fail2ban-addon-contact-form-7'] ?? false)) { $tests['direct']['wp_fail2ban_cf7_installed'] = [ 'label' => 'Add-on for Contact Form 7 installed', 'test' => [$instance, 'get_test_cf7_installed'] ]; } if (!defined('WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND') || ( is_array(WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND) && true !== WP_FAIL2BAN_SITE_HEALTH_SKIP_RECOMMEND['addon']['wp-fail2ban-addon-gravity-forms'] ?? false)) { $tests['direct']['wp_fail2ban_gf_installed'] = [ 'label' => 'Add-on for Gravity Forms installed', 'test' => [$instance, 'get_test_gf_installed'] ]; } return $tests; } /** * Is the "normal" plugin activated if we're running as Must-Use? * * @since 4.4.0.8 * * @return array Empty */ public function get_test_mu_ensure_activated() { foreach (get_mu_plugins() as $plugin => $data) { if (0 === strpos($data['Name'], 'WP fail2ban')) { // MU plugin // // Make sure the "normal" plugin is activated, if installed that way $plugin = plugin_basename(WP_FAIL2BAN_FILE); if (array_key_exists($plugin, get_plugins()) && !is_plugin_active($plugin)) { activate_plugin( $plugin, '', // don't redirect anywhere false, true // don't call activation hooks ); } break; } } return false; } /** * Is WP_FAIL2BAN_LOG_COMMENTS_EXTRA defined? * * Constant has been deprecated. * * @since 5.0.0 * * @return array The test result. */ public function get_test_log_comments_extra_deprecated() { if (Config::ndef('WP_FAIL2BAN_LOG_COMMENTS_EXTRA')) { return false; } return [ /* translators: %s: 'WP_FAIL2BAN_LOG_COMMENTS_EXTRA' (simplifies custom dictionary) */ 'label' => self::PREFIX.sprintf(__('%s is deprecated', 'wp-fail2ban'), 'WP_FAIL2BAN_LOG_COMMENTS_EXTRA'), 'status' => 'critical', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => sprintf( '

%s

%s

', sprintf( /* translators: %s: 'WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS' (simplifies custom dictionary) */ __('It has been replaced by %s - please update your configuration.', 'wp-fail2ban'), sprintf( 'WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS', WP_FAIL2BAN_VER2 ), ), sprintf( /* translators: %s: 'WP_FAIL2BAN_LOG_COMMENTS_EXTRA' (simplifies custom dictionary) */ __('%s will be removed in version 6.0.', 'wp-fail2ban'), 'WP_FAIL2BAN_LOG_COMMENTS_EXTRA' ) ), 'actions' => '', 'test' => 'wp_fail2ban_log_comments_extra_deprecaed' ]; } /** * Is WP_FAIL2BAN_COMMENTS_EXTRA_LOG defined? * * Constant has been deprecated. * * @since 5.0.0 * * @return array The test result. */ public function get_test_comments_extra_log_deprecated() { if (Config::ndef('WP_FAIL2BAN_COMMENTS_EXTRA_LOG')) { return false; } return [ /* translators: %s: 'WP_FAIL2BAN_COMMENTS_EXTRA_LOG' (simplifies custom dictionary) */ 'label' => self::PREFIX.sprintf(__('%s is deprecated', 'wp-fail2ban'), 'WP_FAIL2BAN_COMMENTS_EXTRA_LOG'), 'status' => 'critical', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => sprintf( '

%s

%s

', sprintf( /* translators: %s: 'WP_FAIL2BAN_COMMENT_ATTEMPT_LOG' (simplifies custom dictionary) */ __('It has been replaced by %s - please update your configuration.', 'wp-fail2ban'), sprintf( 'WP_FAIL2BAN_COMMENT_ATTEMPT_LOG', WP_FAIL2BAN_VER2 ), ), sprintf( /* translators: %s: 'WP_FAIL2BAN_COMMENTS_EXTRA_LOG' (simplifies custom dictionary) */ __('%s will be removed in version 6.0.', 'wp-fail2ban'), 'WP_FAIL2BAN_COMMENTS_EXTRA_LOG' ) ), 'actions' => '', 'test' => 'wp_fail2ban_comments_extra_log_deprecaed' ]; } /** * Attempt to find the fail2ban install path * * @since 5.0.0 * * @param string $suffix Subdirectory to test for * * @return string|null Existing path to fail2ban dir, or null if none found */ public static function get_fail2ban_path(string $suffix = ''): ?string { $fail2ban_path = null; if (defined('WP_FAIL2BAN_INSTALL_PATH')) { $path = trailingslashit(WP_FAIL2BAN_INSTALL_PATH).$suffix; if (is_dir($path)) { $fail2ban_path = $path; } } else { $paths = array_map(function ($e) use ($suffix) { return trailingslashit($e).$suffix; }, self::FAIL2BAN_PATHS); foreach ($paths as $path) { if (is_dir($path)) { $fail2ban_path = $path; break; } } } return $fail2ban_path; } /** * Do the filters actually need to be updated? * * 5.0.x => 5.1.y: NO * * @since 5.1.0 * * @param string $ver Version of existing filter * * @return bool */ protected function check_filter_needs_update(string $ver, string $filter, array &$reasons): ?bool { list($major, $minor, $patch) = explode('.', $ver); /* Specific version update logic */ switch ($major) { case 4: switch ($minor) { case 4: $rv = null; switch ($filter) { case 'hard': // [hard] Untrusted X-Forwarded-For header if (count(Config::get('WP_FAIL2BAN_PROXIES'))) { $reasons[] = __('Untrusted proxies will not be blocked.', 'wp-fail2ban'); $rv = true; } break; case 'soft': // [soft] Comment attempt on .* post \d+ if (Config::get('WP_FAIL2BAN_LOG_COMMENT_ATTEMPTS') || Config::get('WP_FAIL2BAN_LOG_COMMENTS_EXTRA') > 0) { $reasons[] = __('Attempted comments will not be blocked.', 'wp-fail2ban'); $rv = true; } break; } return $rv; } break; } /* Always update for major version changes that aren't handled above */ if ($major != WP_FAIL2BAN_VER_MAJOR) { return true; } return apply_filters(__METHOD__, false, $major, $minor, $patch); } /** * Check all the standard filters for obsolete version or modification * * @since 5.0.0 * * @param ?array &$flags Summary of findings * * @return array|null Results of the checks */ protected function check_filters(?array &$flags): ?array { static $status = [ 'obsolete' => false, 'custom' => false, 'unknown' => false, 'partial' => false ]; static $failures = false; if (false === $failures) { if (null === ($filter_d = self::get_fail2ban_path('filter.d'))) { $failures = null; } else { $failures = []; $filter_files = [ 'hard', 'soft', 'extra' ]; foreach ($filter_files as $filter) { $filter_file = "{$filter_d}/wordpress-{$filter}.conf"; // Exists and we can get the contents if (is_readable($filter_file)) { $installed_file = sha1_file($filter_file); $local_file = sha1_file(WP_FAIL2BAN_DIR."/filters.d/wordpress-{$filter}.conf"); if ($installed_file == $local_file) { // OK - identical } elseif (array_key_exists($installed_file, WP_FAIL2BAN_HASHES) && array_key_exists($filter, WP_FAIL2BAN_HASHES[$installed_file])) { $ver = WP_FAIL2BAN_HASHES[$installed_file][$filter]; $reasons = []; switch ($this->check_filter_needs_update($ver, $filter, $reasons)) { case true: $failures[$filter] = [ 'status' => 'obsolete', 'version' => $ver, 'reasons' => $reasons ]; $status['obsolete'] = true; break; case null: $failures[$filter] = [ 'status' => 'old', 'version' => $ver ]; $status['old'] = true; break; case false: // OK - compatible break; } } else { $failures[$filter] = [ 'status' => 'custom', 'version' => null ]; $status['custom'] = true; } // Exists, but can't get contents } elseif (is_file($filter_file)) { $failures[$filter] = [ 'status' => 'unknown', 'version' => null ]; $status['unknown'] = true; // Does not exist } else { $failures[$filter] = [ 'status' => 'missing', 'version' => null ]; $status['partial'] = true; } } } } $flags = $status; return $failures; } /** * Is fail2ban running? * * For now, just try systemctl. * * @since 5.2.2 Check for exec * @since 5.1.0 * * @return array The test result. */ public function get_test_fail2ban_running() { if (!function_exists('exec')) { return false; } $results = [ 'label' => __('fail2ban is running', 'wp-fail2ban'), 'status' => 'good', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => sprintf('

%s

', __('fail2ban is running.', 'wp-fail2ban')), 'actions' => '', 'test' => 'wp_fail2ban_running' ]; if (file_exists('/usr/bin/systemctl')) { $output = []; // get the active status; there is no output if (false === exec('/usr/bin/systemctl is-active --quiet fail2ban', $output, $rv)) { return false; } // get the status if (false === exec('/usr/bin/systemctl status --quiet fail2ban', $output)) { return false; } if ($rv) { // 0 is active $results['label'] = __('fail2ban is not running', 'wp-fail2ban'); $results['status'] = 'critical'; $results['description'] = sprintf( /* translators: %s: fail2ban */ __('%s is not running - your server is unprotected.', 'wp-fail2ban'), 'fail2ban' ); $results['actions'] = sprintf( '

%s

', sprintf( /* translators: %s: fail2ban */ __('Enable %s', 'wp-fail2ban'), 'fail2ban' ) ); } $results['description'] .= '
'.join("\n", $output).'
'; } else { // for now don't try anything else return false; } $results['label'] = self::PREFIX.$results['label']; return $results; } /** * Common messages about updating filters * * @since 5.2.0 * * @param bool $asap * * @return string */ protected function update_filters_asap(bool $asap = true): string { $output = ($asap) ? sprintf( '

%s

', sprintf( /* translators: %s: fail2ban */ __('You should update your %s filters as soon as possible. This is usually done by your server administrator.', 'wp-fail2ban'), 'fail2ban' ) ) : sprintf( '

%s

', sprintf( /* translators: %s: fail2ban */ __('You should update your %s filters. This is usually done by your server administrator.', 'wp-fail2ban'), 'fail2ban' ) ); if (file_exists('/opt/digitalocean/bin/droplet-agent')) { // Probably running DO droplet $output .= sprintf( /* translators: 1: "Life With WP fail2ban", 2: "DigitalOcean WordPress Droplet" */ __('It looks like you’re using a %1$s; step-by-step instructions for updating the filters can be found on the %2$s site.', 'wp-fail2ban'), 'DigitalOcean WordPress Droplet', '“Life With WP fail2ban' ); } $output .= sprintf( '

%s

', sprintf( 'https://docs.wp-fail2ban.com/en/%s/maintenance.html', WP_FAIL2BAN_VER2 ), __('Learn more about updating filters.', 'wp-fail2ban') ); return $output; } /** * Are the fail2ban filters current? * * This test will not work if we do not have access to fail2ban/filter.d; * e.g. if we're running chroot'd * * @since 5.0.1 Drop cron nag * @since 5.0.0 * * @return array The test result. */ public function get_test_filter_obsolete() { $results = [ 'label' => __('The filters are up to date', 'wp-fail2ban'), 'status' => 'good', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => sprintf('

%s

', __('You are using the latest WP fail2ban filters.', 'wp-fail2ban')), 'actions' => '', 'test' => 'wp_fail2ban_filter_obsolete' ]; $failures = $this->check_filters($status); if (is_null($failures)) { $results['label'] = __('The filters could not be checked', 'wp-fail2ban'); $results['status'] = 'recommended'; $results['description'] = sprintf( '

%s

%s

', sprintf( /* translators: %s: fail2ban */ __('Your %s install could not be found.', 'wp-fail2ban'), 'fail2ban' ), __('This may be expected behaviour, depending on your server configuration. You should ask your server administrator to review the documentation linked below and take any appropriate action.', 'wp-fail2ban') ); $results['actions'] = sprintf( '

%s

', sprintf( 'https://docs.wp-fail2ban.com/en/%s/configuration/site-health-tool.html', WP_FAIL2BAN_VER2 ), __('Configuring the Site Health tool', 'wp-fail2ban') ); } elseif (empty($failures)) { // Good - nothing to do } elseif ($status['obsolete']) { $results['label'] = __('One or more of your fail2ban filters are obsolete', 'wp-fail2ban'); $results['status'] = 'critical'; $output = sprintf( '

%s

', sprintf( /* translators: %s: fail2ban. */ __('Using the latest version of the %s filters is critical for correct behaviour. Obsolete filters may cause users to be blocked incorrectly, or attackers not to be detected.', 'wp-fail2ban'), 'fail2ban' ) ); $output .= ''; $output .= $this->update_filters_asap(); $results['description'] = $output; } elseif ($status['old']) { $results['label'] = __('One or more of your fail2ban filters are out of date, but compatible', 'wp-fail2ban'); $results['description'] = __('Your filters are compatible with your current configuration. There is no need to update them at this time.', 'wp-fail2ban'); $results['old'] = 'old'; } elseif ($status['custom']) { $results['status'] = 'custom'; } elseif ($status['partial']) { $results['status'] = 'partial'; } $results['label'] = self::PREFIX.$results['label']; return $results; } /** * Are the fail2ban filters modifed? * * Custom filter files should have a different name. * * This test will not work if we do not have access to fail2ban/filter.d; * e.g. if we're running chroot'd * * @since 5.0.0 * * @return array The test result. */ public function get_test_filter_modified() { // The filter_obsolete has already failed to run if (is_null($failures = $this->check_filters($status))) { return false; } $results = [ 'label' => __('The filters have not been modified', 'wp-fail2ban'), 'status' => 'good', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => sprintf('

%s

', __('The standard WP fail2ban filters are installed.', 'wp-fail2ban')), 'actions' => '', 'test' => 'wp_fail2ban_filter_modified' ]; if (empty($failures)) { // Good - nothing to do } elseif ($status['custom']) { $results['label'] = 'One or more of your filters have been modified'; $results['status'] = 'recommended'; $output = sprintf( '

%s

', sprintf( /* translators: %s: the documentation */ __('You should not modify the standard configuration files. Please refer to %s on how to create custom filters.', 'wp-fail2ban'), sprintf( '%s', WP_FAIL2BAN_VER2, __('the documentation', 'wp-fail2ban') ) ) ); $output .= ''; $results['description'] = $output; } $results['label'] = self::PREFIX.$results['label']; return $results; } /** * Are any of the fail2ban filters missing? * * This test will not work if we do not have access to fail2ban/filter.d; * e.g. if we're running chroot'd * * @since 5.0.0 * * @return array The test result. */ public function get_test_filter_missing() { // The filter_obsolete has already failed to run if (is_null($failures = $this->check_filters($status))) { return false; } $results = [ 'label' => __('The filters are all present', 'wp-fail2ban'), 'status' => 'good', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => sprintf('

%s

', __('All the WP fail2ban filters are installed.', 'wp-fail2ban')), 'actions' => '', 'test' => 'wp_fail2ban_filter_missing' ]; if (empty($failures)) { // Good - nothing to do } elseif ($status['partial']) { $results['label'] = 'One or more of your filters are missing'; $results['status'] = 'recommended'; $output = sprintf( '

%s

', sprintf( /* translators: %s: the documentation */ __('You should include all the standard configuration files. Please refer to %s.', 'wp-fail2ban'), sprintf( '%s', WP_FAIL2BAN_VER2, __('the documentation', 'wp-fail2ban') ) ) ); $output .= ''; $results['description'] = $output; } $results['label'] = self::PREFIX.$results['label']; return $results; } /** * Is WP fail2ban Blocklist installed and activated? * * @since 5.0.0 * * @return array The test result. */ public function get_test_blocklist_installed(): array { $results = [ 'label' => '', 'status' => '', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => '', 'actions' => '', 'test' => 'wp_fail2ban_blocklist_installed' ]; if (defined('WP_FAIL2BAN_ADDON_BLOCKLIST_VER')) { // Installed and activated? $results['label'] = __('Blocklist is installed and activated', 'wp-fail2ban'); $results['status'] = 'good'; $results['description'] = ''; } else { $results['status'] = 'recommended'; $installed = false; // Check if it's installed if (file_exists(WP_PLUGIN_DIR.'/wpf2b-addon-blocklist')) { $installed = 'wpf2b-addon-blocklist'; } elseif (file_exists(WP_PLUGIN_DIR.'/wp-fail2ban-addon-blocklist')) { $installed = 'wp-fail2ban-addon-blocklist'; } if ($installed) { $plugin = $installed.'/addon.php'; $results['label'] = __('The Blocklist add-on is not activated', 'wp-fail2ban'); $results['description'] = ''; $results['actions'] = sprintf( '

%s

%s

', esc_url(wp_nonce_url(network_admin_url("plugins.php?action=activate&plugin={$plugin}"), 'activate-plugin_'.$plugin)), __('Activate WP fail2ban Blocklist', 'wp-fail2ban'), __('Get support', 'wp-fail2ban') ); } else { $results['label'] = __('The Blocklist add-on is not installed', 'wp-fail2ban'); $results['description'] = sprintf( '

%s

', sprintf( __('%s is a collaborative preemptive blocklist - it protects your site before it’s attacked.', 'wp-fail2ban'), 'WP fail2ban Blocklist' ) ); $url = network_admin_url('update.php?action=install-plugin&plugin=wpf2b-addon-blocklist'); $url = wp_nonce_url($url, 'install-plugin_wpf2b-addon-blocklist'); $url = esc_url($url); $results['actions'] = sprintf( '

%s

%s

', $url, __('Install WP fail2ban Blocklist', 'wp-fail2ban'), __('Learn more about WP fail2ban Blocklist', 'wp-fail2ban') ); } } $results['label'] = self::PREFIX.$results['label']; return $results; } /** * Is a WP fail2ban Add-on installed and activated? * * @since 5.0.0 * * @return array|false The test result. */ protected function get_test_addon_installed(array $params) { $results = [ 'label' => '', 'status' => '', 'badge' => [ 'label' => __('Security'), 'color' => 'blue' ], 'description' => '', 'actions' => '', 'test' => $params['test'] ]; if (defined($params['active']['define'])) { // Installed and activated? $results['label'] = sprintf(__('%s is installed and activated', 'wp-fail2ban'), $params['name']); $results['status'] = 'good'; $results['description'] = ''; } else { $results['status'] = 'recommended'; $installed = false; // Check if it's installed if (file_exists(WP_PLUGIN_DIR.'/'.$params['slug']['free'])) { $installed = $params['slug']['free']; } elseif (file_exists(WP_PLUGIN_DIR.'/'.$params['slug']['premium'])) { $installed = $params['slug']['premium']; } if ($installed) { $plugin = $installed.'/addon.php'; $results['label'] = sprintf(__('The %s add-on is not activated', 'wp-fail2ban'), $params['name']); $results['description'] = $params['inactive']['description']; $results['actions'] = sprintf( '

%s

', esc_url(wp_nonce_url(network_admin_url("plugins.php?action=activate&plugin={$plugin}"), 'activate-plugin_'.$plugin)), sprintf(__('Activate the %s add-on', 'wp-fail2ban'), $params['name']) ); } else { $results['label'] = sprintf(__('The %s is not installed', 'wp-fail2ban'), $params['name']); $results['description'] = $params['missing']['description']; $url = network_admin_url('update.php?action=install-plugin&plugin='.$params['slug']['free']); $url = wp_nonce_url($url, 'install-plugin_'.$params['slug']['free']); $url = esc_url($url); $results['actions'] = sprintf( '

%s

', $url, sprintf(__('Install %s', 'wp-fail2ban'), $params['name']) ); } $results['label'] = self::PREFIX.$results['label']; } return $results; } public function get_test_cf7_installed() { $results = false; if (defined('WPCF7_VERSION')) { $results = $this->get_test_addon_installed([ 'test' => 'wp_fail2ban_cf7_installed', 'name' => __('Contact Form 7', 'wp-fail2ban'), 'slug' => [ 'free' => 'wp-fail2ban-addon-contact-form-7', 'premium' => 'wp-fail2ban-addon-contact-form-7-premium' ], 'active' => [ 'define' => 'WP_FAIL2BAN_ADDON_CF7_VER' ], 'inactive' => [ 'description' => '', ], 'missing' => [ 'description' => '' ] ]); } return $results; } public function get_test_gf_installed() { $results = false; if (class_exists('\GFCommon')) { $results = $this->get_test_addon_installed([ 'test' => 'wp_fail2ban_gf_installed', 'name' => __('Gravity Forms', 'wp-fail2ban'), 'slug' => [ 'free' => 'wp-fail2ban-addon-gravity-forms', 'premium' => 'wp-fail2ban-addon-gravity-forms-premium' ], 'active' => [ 'define' => 'WP_FAIL2BAN_ADDON_GRAVITY_FORMS_VER' ], 'inactive' => [ 'description' => '', ], 'missing' => [ 'description' => '' ] ]); } return $results; } }