diff --git a/inc/spbc-firewall.php b/inc/spbc-firewall.php index 4047acf2b..1b69ea8e8 100644 --- a/inc/spbc-firewall.php +++ b/inc/spbc-firewall.php @@ -171,6 +171,7 @@ function spbc_firewall_skip_check() || spbc_wp_doing_cron() // Pass WP cron tasks || \CleantalkSP\Variables\Server::inUri('/favicon.ico') // Exclude favicon.ico requests from the check || spbc_mailpoet_doing_cron() + || spbc_is_cli() ) { return true; } diff --git a/inc/spbc-pluggable.php b/inc/spbc-pluggable.php index ee42c65bc..eb977732a 100644 --- a/inc/spbc-pluggable.php +++ b/inc/spbc-pluggable.php @@ -80,3 +80,13 @@ function spbc_mailpoet_doing_cron() strpos(Server::get('REQUEST_URI', null, 'url'), 'mailpoet_router') !== false ); } + +/** + * Checks if the request is the command line access + * + * @return boolean + */ +function spbc_is_cli() +{ + return PHP_SAPI === "cli"; +} diff --git a/inc/spbc-wpcli.php b/inc/spbc-wpcli.php new file mode 100644 index 000000000..f38df099b --- /dev/null +++ b/inc/spbc-wpcli.php @@ -0,0 +1,494 @@ + Exit..', 'security_malware_firewall')); + return; + } + $data['user_token'] = $params['token']; + + if (!isset($params['email'])) { + $admin_email = spbc_get_admin_email(); + /** + * Filters the email to get Access key + * + * @param string $admin_email email to get Access key + */ + $data['email'] = apply_filters('apbct_get_api_key_email', $admin_email); + self::prompt(__('The email is not specified, the administrator\'s email will be used: ', 'security_malware_firewall') . $admin_email); + } else { + $data['email'] = $params['email']; + } + + if (!isset($params['domain'])) { + $data['website'] = parse_url(get_option('home'), PHP_URL_HOST) . parse_url(get_option('home'), PHP_URL_PATH); + self::prompt(__('The domain is not specified, the current domain will be used: ', 'security_malware_firewall') . $data['website']); + } else { + $data['website'] = $params['domain']; + } + + $data['platform'] = 'wordpress'; + $data['product_name'] = 'security'; + $data['method_name'] = 'get_api_key'; + $data['timezone'] = (string)get_option('gmt_offset'); + + self::prompt(__('Trying get api key via WP_CLI utils..', 'security_malware_firewall')); + + $result = WP_CLI\Utils\http_request($this->method, $this->url, $data, [], ['insecure' => true]); + if (!isset($result->body)) { + self::prompt(__("HTTP error occurred, exit..", 'security_malware_firewall')); + return; + } + + $result = json_decode($result->body, true); + if (!empty($result['error']) || !empty($result['error_message'])) { + self::prompt(__("API error:", 'security_malware_firewall')); + $error = isset($result['error_message']) ? esc_html($result['error_message']) : esc_html($result['error']); + self::prompt($error, true); + return; + } elseif (!isset($result['data'])) { + self::prompt(__("Error. Probably, automatic key getting is disabled in the CleanTalk dashboard settings. Please, get the Access Key from CleanTalk Control Panel. Exit..", 'security_malware_firewall')); + return; + } + + if ( isset($result['data']['user_token']) ) { + $spbc->data['user_token'] = $result['data']['user_token']; + self::prompt(__('User token installed.', 'security_malware_firewall')); + } + + if ( !empty($result['data']['auth_key']) && spbc_api_key__is_correct($result['data']['auth_key'])) { + $new_key = trim($result['data']['auth_key']); + $spbc->data['key_changed'] = $new_key !== $spbc->settings['spbc_key']; + $spbc->settings['spbc_key'] = $new_key; + $spbc->api_key = $new_key; + self::prompt(__('Api key installed: ', 'security_malware_firewall') . $spbc->settings['spbc_key']); + } + + $spbc->save('settings'); + $spbc->save('data'); + + self::prompt(__('Running synchronization process and SFW update init..', 'security_malware_firewall')); + + spbc_sync(true); + if ( $spbc->isHaveErrors() ) { + self::prompt(__("Error occurred while syncing: ", 'security_malware_firewall')); + self::prompt($spbc->errors, true); + } else { + self::prompt(__("Synchronization success.\n", 'security_malware_firewall')); + } + self::prompt(__('Service created successful.', 'security_malware_firewall')); + } + + /** + * Manage malware scanner. Provide an arg [start|clear_results|update_signatures] to proceed. + * + * @param array $args + * @param array $_params + * + * @return void + * @throws WP_CLI\ExitException + */ + public function malware_scanner($args, $_params) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + { + $commands_map = array( + 'start' => function () { + $this->scannerRunBackgroundScan(); + }, + 'clear_results' => function () { + $this->scannerClearResults(); + }, + 'update_signatures' => function () { + $this->scannerUpdateSignatures(); + }, + ); + + $allowed_commands = array_keys($commands_map); + + self::prompt(__('Scanner background service start..', 'security_malware_firewall')); + + if (count($args) !== 1) { + self::prompt(__('Error. Commands count invalid. Use exactly one command.', 'security_malware_firewall')); + self::prompt($allowed_commands, 1); + return; + } + + $command = isset($args[0]) ? $args[0] : null; + if (!in_array($command, $allowed_commands)) { + self::prompt(__('Error. Unknown command. Use one of the following:', 'security_malware_firewall')); + self::prompt($allowed_commands, 1); + return; + } + + //run + call_user_func($commands_map[$command]); + } + + /** + * Set existing settings template. Provide an arg [list|set|reset] to proceed. + * + * @param array $args [list|set|reset] + * @param array $params + * + * @psalm-suppress PossiblyUnusedMethod + * + * @throws WP_CLI\ExitException + */ + public function template($args, $params) + { + global $spbc; + + self::prompt(__('Template service start..', 'security_malware_firewall')); + self::prompt(__('Trying to get templates list..', 'security_malware_firewall')); + + $data = []; + $key = $spbc->settings['spbc_key']; + + if (!$key) { + self::prompt(__('Error. No api key found. Set up api_key first. Exit..', 'security_malware_firewall')); + return; + } + + self::prompt(__('API Key found..', 'security_malware_firewall')); + + $data['auth_key'] = $key; + $data['method_name'] = 'services_templates_get'; + $data['search[product_id]'] = 4; + + $result = WP_CLI\Utils\http_request($this->method, $this->url, $data, [], ['insecure' => true]); + if (!isset($result->body)) { + self::prompt($result, true); + self::prompt(__('HTTP error occurred. Exit..', 'security_malware_firewall')); + return; + } + + self::prompt(__('Templates request success..', 'security_malware_firewall')); + + $result = json_decode($result->body, true); + if (!isset($result['data'])) { + self::prompt(json_last_error(), true); + self::prompt(json_last_error_msg(), true); + self::prompt(__('JSON parse error occurred. Exit..', 'security_malware_firewall')); + return; + } + + self::prompt(__('Results decoded..', 'security_malware_firewall')); + + if (isset($result['error'])) { + self::prompt(__('API error:', 'security_malware_firewall')); + $error = isset($result['error_message']) ? esc_html($result['error_message']) : esc_html($result['error']); + self::prompt($error, true); + return; + } + + self::prompt(__('Response is correct..', 'security_malware_firewall')); + + if (in_array('list', $args)) { + self::prompt(__('Listing mode..', 'security_malware_firewall')); + if (empty($result['data'])) { + self::prompt(__('Error. No templates found. Exit..', 'security_malware_firewall')); + return; + } + self::prompt(__('Success! Available templates, format is ID -> NAME:', 'security_malware_firewall')); + foreach ($result['data'] as $template) { + $id = isset($template['template_id']) ? $template['template_id'] : 'N/A'; + $name = isset($template['name']) ? $template['name'] : 'N/A'; + self::prompt($id . ' -> ' . $name); + } + return; + } + + if (in_array('set', $args)) { + if (in_array('reset', $args)) { + self::prompt(__('Reset mode..', 'security_malware_firewall')); + $settings_template_service = new CleantalkSettingsTemplates($key); + $res = $settings_template_service->resetOptions(); + if (!$res) { + self::prompt(__('Can\'t reset settings to default. Exit..', 'security_malware_firewall')); + } + self::prompt(__('Success! Template was reset to default.', 'security_malware_firewall')); + return; + } + self::prompt(__('Set up mode..', 'security_malware_firewall')); + + if (!isset($params['id'])) { + self::prompt(__('Error. Please add \ param to choose template. Exit..', 'security_malware_firewall')); + return; + } + + $id = null; + $name = ''; + $set = []; + foreach ($result['data'] as $key => $template) { + if ( + isset($template['template_id'], $template['name'], $template['options_site']) && + $template['template_id'] == $params['id'] + ) { + $id = $template['template_id']; + $name = $template['name']; + $set = json_decode($template['options_site'], true); + } + } + if (is_null($id)) { + self::prompt(__('Error. Selected ID does not exist. Exit..', 'security_malware_firewall')); + return; + } + + require_once('spbc-settings.php'); + $settings_template_service = new CleantalkSettingsTemplates($key); + $res = $settings_template_service->setOptions($id, $name, $set); + if (!$res) { + self::prompt(__('Error occurred during setting a template. Exit..', 'security_malware_firewall')); + } + self::prompt(__('Success! Template installed: ', 'security_malware_firewall') . $name); + return; + } + + self::prompt(__('No available params found. Use or ', 'security_malware_firewall')); + } + + /** + * Set a new settings state. Provide a setting name on state On|Off. + * + * @param mixed $args legacy support + * @param array $input_params CLI params + * + * @psalm-suppress PossiblyUnusedMethod + */ + public function settings($args, $input_params) + { + global $spbc; + + self::prompt(__('Settings update start..', 'security_malware_firewall')); + + if ( empty($input_params)) { + self::prompt(__('No available params found - nothing to do. Exit..', 'security_malware_firewall')); + } + + $available_params = [ + // 2FA + '2fa__enabled' => '2fa__enable', + // Login Page + 'login_page__rename_enabled' => 'login_page_rename__enabled', + // Scanner + 'scanner__auto_start_enabled' => 'scanner__auto_start', + 'scanner__heuristic_analysis_enabled' => 'scanner__heuristic_analysis', + 'scanner__signature_analysis_enabled' => 'scanner__signature_analysis', + 'scanner__automatic_cure_enabled' => 'scanner__auto_cure', + 'scanner__os_cron_analysis_enabled' => 'scanner__os_cron_analysis', + 'scanner__unknown_files_listing_enabled' => 'scanner__list_unknown', + // Filesystem watcher + 'scanner__fs_watcher' => 'scanner__fs_watcher', + //firewall + 'firewall__traffic_control_enabled' => 'traffic_control__enabled', + 'firewall__firewall_enabled' => 'secfw__enabled', + 'firewall__waf_enabled' => 'waf__enabled', + 'firewall__waf_blocker_enabled' => 'waf_blocker__enabled', + // + 'complete_deactivation_enabled' => 'misc__complete_deactivation', + // + 'vulnerability_check__cron_enabled' => 'vulnerability_check__enable_cron', + 'vulnerability_check__reports_enabled' => 'vulnerability_check__show_reports', + 'vulnerability_check__testing_before_install_enabled' => 'vulnerability_check__test_before_install', + 'vulnerability_check__warn_on_modules_pages_enabled' => 'vulnerability_check__warn_on_modules_pages', + // + 'wpsec__xmlrpc_disabled' => 'wp__disable_xmlrpc', + 'wpsec__rest_api_for_non_authenticated_disabled' => 'wp__disable_rest_api_for_non_authenticated', + // + 'admin_bar__enabled' => 'admin_bar__show', + 'dashboard_widget__enabled' => 'wp__dashboard_widget__show', + ]; + + if (isset($input_params['list'])) { + self::prompt(__('Current settings:', 'security_malware_firewall')); + self::prompt(static::listSettings($spbc, $available_params), true); + return; + } + + foreach ( $input_params as $key => $value) { + if ('2fa__enabled' === $key && 'on' === $value) { + self::prompt(__('Error. Two factor auth can only be disabled via WP CLI. <--2fa__enabled=off>', 'security_malware_firewall')); + unset($input_params[$key]); + continue; + } + if ('login_page__rename_enabled' === $key && 'on' === $value) { + self::prompt(__('Error. Renaming login page can only be disabled via WP CLI. <--login_page__rename_enabled=off>', 'security_malware_firewall')); + unset($input_params[$key]); + continue; + } + if (!in_array($key, array_keys($available_params))) { + self::prompt(__('Error. Unknown param: ', 'security_malware_firewall') . $key); + unset($input_params[$key]); + continue; + } + $input_params[$key] = trim($value, ' \'\"'); + } + + if (!empty($input_params)) { + self::prompt(__('Found valid params:', 'security_malware_firewall')); + self::prompt($input_params, true); + } else { + self::prompt(__('No valid params found. Nothing to do. Exit..', 'security_malware_firewall')); + return; + } + + foreach ($available_params as $avail_param => $setting_key) { + if ( isset($input_params[$avail_param]) ) { + if ( $input_params[$avail_param] == 'on' ) { + self::prompt(__('Set ', 'security_malware_firewall') . $avail_param . __(' to ON', 'security_malware_firewall')); + $spbc->settings[$setting_key] = 1; + } else if ($input_params[$avail_param] == 'off') { + self::prompt(__('Set ', 'security_malware_firewall') . $avail_param . __(' to OFF', 'security_malware_firewall')); + $spbc->settings[$setting_key] = 0; + } else { + self::prompt(__('Error. Unknown value for setting: ', 'security_malware_firewall') . $avail_param . '->' . $input_params[$avail_param]); + } + } + } + + $spbc->save('settings'); + self::prompt(__('Updated settings state:', 'security_malware_firewall')); + self::prompt(static::listSettings($spbc, $available_params), true); + } + + /** + * @param $spbc + * @param $available_params + * + * @return array + */ + private static function listSettings($spbc, $available_params) + { + $out = []; + $available_params_flip = array_flip($available_params); + foreach ($spbc->settings as $key => $value) { + if (in_array($key, array_keys($available_params_flip))) { + $value = $value ? 'on' : 'off'; + $out[$available_params_flip[$key]] = $value; + } + } + return $out; + } + + /** + * Echo a message. If the message is not string or $pretty flag is set to true, it will be printed as a print_r. + * @param mixed $msg Value to print + * @param bool $pretty Flag to force print as a print_r + * + * @return void + */ + private static function prompt($msg, $pretty = false) + { + if ($pretty || !is_string($msg)) { + WP_CLI::print_value($msg); + return; + } + WP_CLI::line($msg); + } + + /** + * Clear all the scan results. + * @return void + */ + private function scannerClearResults() + { + require_once SPBC_PLUGIN_DIR . 'inc/spbc-scanner.php'; + if (!isset($params['force'])) { + WP_CLI::confirm( + __( + 'This action will delete last scan results, the action is not reversible. Are you sure to implement?', + 'security_malware_firewall' + ) + ); + } + self::prompt(__('Clearing scan results..', 'security_malware_firewall')); + try { + spbc_scanner_clear(true); + } catch (\Exception $e) { + self::prompt(__('Error. ', 'security_malware_firewall') . $e->getMessage(), true); + } + WP_CLI::success(__('Scan results cleared.', 'security_malware_firewall')); + } + + /** + * Update signatures for Signature Analysis module. + * @return void + */ + private function scannerUpdateSignatures() + { + global $spbc; + self::prompt('Updating signatures..'); + $result = spbc_scanner__signatures_update(true); + if (isset($result['error'])) { + $error = is_string($result['error']) ? $result['error'] : __('unknown error', 'security_malware_firewall'); + self::prompt(__('Error. Update failed. Reason: ', 'security_malware_firewall') . $error); + return; + } + $final_count = $spbc->data['scanner']['signature_count']; + if (empty($final_count)) { + self::prompt(__('Error. Update failed. Zero count of signatures.', 'security_malware_firewall')); + return; + } + WP_CLI::success(__('Signatures updated. Final count: ', 'security_malware_firewall') . $final_count); + } + + /** + * Run background scan within 30 seconds. + * @return void + */ + private function scannerRunBackgroundScan() + { + self::prompt('Starting schedule background scan..'); + + Transaction::get('background_scanner')->clearTransactionTimer(); + Cron::removeTask('background_scan'); + \CleantalkSP\SpbctWP\Scanner\ScannerQueue::launchBackground(); + + WP_CLI::success(__('Background scan will start within 30 seconds. Make note, this could take up to 1 hour to complete the scan process in the background.', 'security_malware_firewall')); + } +} diff --git a/lib/CleantalkSP/Common/RemoteCalls.php b/lib/CleantalkSP/Common/RemoteCalls.php index e0724c5bb..72e7117a6 100644 --- a/lib/CleantalkSP/Common/RemoteCalls.php +++ b/lib/CleantalkSP/Common/RemoteCalls.php @@ -232,7 +232,7 @@ public static function performTest($host, $params, $patterns = array()) . $params['spbc_remote_call_action'] . ' RESPONSE: ' . '"' - . htmlspecialchars(substr($result, 0, 400)) + . json_encode(substr($result, 0, 400)) . '"' ); } diff --git a/lib/CleantalkSP/SpbctWP/CleantalkSettingsTemplates.php b/lib/CleantalkSP/SpbctWP/CleantalkSettingsTemplates.php index 344b3c2cc..c734162db 100644 --- a/lib/CleantalkSP/SpbctWP/CleantalkSettingsTemplates.php +++ b/lib/CleantalkSP/SpbctWP/CleantalkSettingsTemplates.php @@ -312,7 +312,7 @@ private function getOptions() * * @return bool */ - private function setOptions($template_id, $template_name, $settings) + public function setOptions($template_id, $template_name, $settings) { global $spbc; $settings = array_replace((array)$spbc->settings, $settings); @@ -330,7 +330,7 @@ private function setOptions($template_id, $template_name, $settings) * * @return bool */ - private function resetOptions() + public function resetOptions() { global $spbc; $def_settings = $spbc->default_settings; diff --git a/security-malware-firewall.php b/security-malware-firewall.php index 108e831af..d8e91f04d 100644 --- a/security-malware-firewall.php +++ b/security-malware-firewall.php @@ -151,6 +151,7 @@ require_once SPBC_PLUGIN_DIR . 'inc/spbc-tools.php'; // Different helper functions require_once SPBC_PLUGIN_DIR . 'inc/spbc-pluggable.php'; // WordPress functions require_once SPBC_PLUGIN_DIR . 'inc/spbc-scanner.php'; +require_once(SPBC_PLUGIN_DIR . 'inc/spbc-wpcli.php'); // ArrayObject with settings and other global variables global $spbc; @@ -638,12 +639,12 @@ function spbc_get_countries_by_ips($ips_data = '') /** * Gets and write new signatures in local database - * + * @param bool $force_update if true, ignores last update and overwrite current signatures * @return bool|array * @global State $spbc * @global WPDB $wpdb */ -function spbc_scanner__signatures_update() +function spbc_scanner__signatures_update($force_update = false) { global $spbc; @@ -652,12 +653,12 @@ function spbc_scanner__signatures_update() */ $spbc->error_delete('scanner_update_signatures_bad_signatures', 'save'); - $latest_signature_submitted_time = SignatureAnalysisFacade::getLatestSignatureSubmittedTime(); + $latest_signature_submitted_time = $force_update ? 0 : SignatureAnalysisFacade::getLatestSignatureSubmittedTime(); $signatures_from_cloud = SignatureAnalysisFacade::getSignaturesFromCloud($latest_signature_submitted_time); // Signatures updated - if (isset($signatures_from_cloud['error']) && $signatures_from_cloud['error'] === 'UP_TO_DATE') { + if (!$force_update && isset($signatures_from_cloud['error']) && $signatures_from_cloud['error'] === 'UP_TO_DATE') { return array('success' => 'UP_TO_DATE'); } diff --git a/tests/Common/Helpers/HelperHTTPTest.php b/tests/Common/Helpers/HelperHTTPTest.php index 765ddbb64..b46ed2752 100644 --- a/tests/Common/Helpers/HelperHTTPTest.php +++ b/tests/Common/Helpers/HelperHTTPTest.php @@ -42,7 +42,7 @@ public function testServerSelectByPing() public function testPingMethods() { - $host = 'apix1.cleantalk.org'; + $host = 'api.cleantalk.org'; $ping = HTTP::pingSpbc($host, 'pingFSockOpen'); $this->assertNotEmpty($ping); $this->assertIsFloat($ping);