attach_behavior( WPMUDEV::class, WPMUDEV::class ); $this->attach_behavior( Core_Integrity::class, Core_Integrity::class ); $this->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class ); $this->is_apikey = wd_di()->get( WPMUDEV::class )->is_apikey_available(); $this->settings = wd_di()->get( Scan_Settings::class ); } /** * Performs additional actions after an advanced scan. * * @param Scan_Model $model The scan model. */ public function advanced_scan_actions( $model ) { $this->reindex_ignored_issues( $model ); $this->clean_up(); if ( defender_is_wp_org_version() ) { Rate::run_counter_of_completed_scans(); } } /** * Process current scan. * * @return bool|int */ public function process() { $scan = Scan_Model::get_active(); if ( ! is_object( $scan ) ) { // This case can be a scan get cancel. return - 1; } $this->scan = $scan; $tasks = $this->get_tasks(); $runner = new ArrayIterator( $tasks ); $task = $this->scan->status; if ( Scan_Model::STATUS_INIT === $scan->status ) { // Get the first. $this->log( 'Prepare facts for a scan', Scan_Controller::SCAN_LOG ); $task = Scan_Model::STEP_GATHER_INFO; $this->scan->status = $task; $this->scan->percent = 0; $this->scan->total_tasks = $runner->count(); $this->scan->save(); $this->scan->enqueue_status_message( $this->scan->get_status_text() ); } if ( in_array( $this->scan->status, array( Scan_Model::STATUS_ERROR, Scan_Model::STATUS_IDLE, ), true ) ) { // Stop and return true to abort the process. return true; } // Find the current task. $offset = array_search( $task, array_values( $tasks ), true ); if ( false === $offset ) { $this->log( sprintf( 'offset is not found, search %s', $task ), Scan_Controller::SCAN_LOG ); return false; } // Reset the tasks to current. $runner->seek( $offset ); $this->log( sprintf( 'Current task %s', $runner->current() ), Scan_Controller::SCAN_LOG ); if ( $this->has_method( $task ) ) { $this->log( sprintf( 'processing %s', $runner->key() ), Scan_Controller::SCAN_LOG ); $result = $this->task_handler( $task ); if ( true === $result ) { $this->log( sprintf( 'task %s processed', $runner->key() ), Scan_Controller::SCAN_LOG ); // Task is done, move to next. $runner->next(); if ( $runner->valid() ) { $this->log( sprintf( 'queue %s for next', $runner->key() ), Scan_Controller::SCAN_LOG ); $this->scan->status = $runner->key(); $this->scan->task_checkpoint = ''; $this->scan->date_end = gmdate( 'Y-m-d H:i:s' ); $this->scan->save(); $this->scan->enqueue_status_message( $this->scan->get_status_text() ); // Queue for next run. return false; } $this->log( 'All done!', Scan_Controller::SCAN_LOG ); // No more task in the queue, we are done. $this->scan->status = Scan_Model::STATUS_FINISH; $this->scan->save(); $this->advanced_scan_actions( $this->scan ); do_action( 'defender_notify', 'malware-notification', $this->scan ); return true; } $this->scan->status = $task; $this->scan->save(); } return false; } /** * Reindex ignored issues to update their status in the scan model. * * @param Scan_Model $model The scan model. */ private function reindex_ignored_issues( $model ) { $issues = $model->get_issues( null, Scan_Item::STATUS_IGNORE ); $ignore_lists = array(); foreach ( $issues as $issue ) { $data = $issue->raw_data; if ( isset( $data['file'] ) ) { $ignore_lists[] = $data['file']; } elseif ( isset( $data['slug'] ) ) { $ignore_lists[] = $data['slug']; } } $model->update_ignore_list( $ignore_lists ); } /** * Get a list of tasks will run in a scan. * * @return array */ public function get_tasks(): array { $tasks = array( Scan_Model::STEP_GATHER_INFO => 'gather_info' ); if ( $this->settings->integrity_check ) { // Nested options. if ( $this->settings->check_core ) { $tasks[ Scan_Model::STEP_CHECK_CORE ] = 'core_integrity_check'; } if ( $this->settings->check_plugins ) { $tasks[ Scan_Model::STEP_CHECK_PLUGIN ] = 'plugin_integrity_check'; } } if ( $this->settings->check_abandoned_plugin ) { $tasks[ Scan_Model::STEP_ABANDONED_PLUGIN_CHECK ] = 'abandoned_plugin_check'; } // Check if this is HC feature. if ( $this->is_apikey ) { if ( $this->settings->check_known_vuln && $this->has_method( Scan_Model::STEP_VULN_CHECK ) ) { $tasks[ Scan_Model::STEP_VULN_CHECK ] = 'vuln_check'; } // The division between PHP and JS files occurs when collecting files. if ( $this->settings->scan_malware && $this->has_method( Scan_Model::STEP_SUSPICIOUS_CHECK ) ) { $tasks[ Scan_Model::STEP_SUSPICIOUS_CHECK ] = 'suspicious_check'; } } return $tasks; } /** * Handles individual scan tasks based on the task identifier. * * @param string $task The task identifier. * * @return bool Returns true if the task was handled successfully. */ private function task_handler( $task ) { switch ( $task ) { case 'gather_info': if ( ! ( $this->gather_fact instanceof Gather_Fact ) && class_exists( Gather_Fact::class ) ) { $this->set_gather_fact( wd_di()->make( Gather_Fact::class, array( 'scan' => $this->scan ) ) ); } return $this->gather_info( $this->gather_fact ); case 'vuln_check': if ( ! ( $this->known_vulnerability instanceof Known_Vulnerability ) && class_exists( Known_Vulnerability::class ) ) { $this->set_known_vulnerability( wd_di()->make( Known_Vulnerability::class, array( 'scan' => $this->scan ) ) ); } return $this->vuln_check( $this->known_vulnerability ); case 'suspicious_check': if ( class_exists( Malware_Scan::class ) ) { $this->set_malware_scan( wd_di()->make( Malware_Scan::class, array( 'scan' => $this->scan ) ) ); } return $this->suspicious_check( $this->malware_scan ); case 'abandoned_plugin_check': if ( ! ( $this->abandoned_plugin instanceof Abandoned_Plugin ) && class_exists( Abandoned_Plugin::class ) ) { $this->set_abandoned_plugin( wd_di()->make( Abandoned_Plugin::class, array( 'scan' => $this->scan ) ) ); } return $this->abandoned_plugin_check( $this->abandoned_plugin ); case 'core_integrity_check': return $this->core_integrity_check(); case 'plugin_integrity_check': return $this->plugin_integrity_check(); default: return false; } } /** * A wrapper method for Known_Vulnerability class method vuln_check. * * @param Known_Vulnerability $known_vulnerability An instance of Known_Vulnerability. * * @return bool True always as in wrapped method Known_Vulnerability::vuln_check. */ private function vuln_check( Known_Vulnerability $known_vulnerability ): bool { if ( method_exists( $known_vulnerability, 'vuln_check' ) ) { return $known_vulnerability->vuln_check(); } return true; // Followed Known_Vulnerability::vuln_check return pattern i.e. always true for skipped vuln check. } /** * Setter injection method for Known_Vulnerability instance. * * @param Known_Vulnerability $known_vulnerability The Known_Vulnerability instance to set. */ public function set_known_vulnerability( Known_Vulnerability $known_vulnerability ) { if ( class_exists( Known_Vulnerability::class ) ) { $this->known_vulnerability = $known_vulnerability; } } /** * A wrapper method for Malware_Scan class method suspicious_check. * * @param Malware_Scan $malware_scan An instance of Malware_Scan. * * @return bool True if method Malware_Scan::suspicious_check not exists else bool value returned by that method. */ private function suspicious_check( Malware_Scan $malware_scan ): bool { if ( method_exists( $malware_scan, 'suspicious_check' ) ) { $quick_scan = wd_di()->get( \WP_Defender\Behavior\Scan\Malware_Quick_Scan::class ); $deep_scan = wd_di()->get( \WP_Defender\Behavior\Scan\Malware_Deep_Scan::class ); return $malware_scan->suspicious_check( $quick_scan, $deep_scan ); } return true; } /** * Setter injection method for Malware_Scan instance. * * @param Malware_Scan $malware_scan The Malware_Scan instance to set. */ public function set_malware_scan( Malware_Scan $malware_scan ) { if ( class_exists( Malware_Scan::class ) ) { $this->malware_scan = $malware_scan; } } /** * Set the Abandoned_Plugin object. * * @param Abandoned_Plugin $abandoned_plugin The Abandoned_Plugin object to set. */ public function set_abandoned_plugin( Abandoned_Plugin $abandoned_plugin ) { if ( class_exists( Abandoned_Plugin::class ) ) { $this->abandoned_plugin = $abandoned_plugin; } } /** * A wrapper method for Abandoned_Plugin class method abandoned_plugin_check. * * @param Abandoned_Plugin $abandoned_plugin An instance of Abandoned_Plugin. * * @return bool */ private function abandoned_plugin_check( Abandoned_Plugin $abandoned_plugin ): bool { if ( method_exists( $abandoned_plugin, 'abandoned_plugin_check' ) ) { return $abandoned_plugin->abandoned_plugin_check(); } return true; } /** * Cancels an active scan and cleans up related data. */ public function cancel_a_scan() { $scan = Scan_Model::get_active(); if ( is_object( $scan ) ) { $scan->delete(); } $this->clean_up(); $this->remove_lock( $this->lock_filename ); $scan_analytics = wd_di()->get( Scan_Analytics::class ); $scan_analytics->track_feature( $scan_analytics::EVENT_SCAN_FAILED, array( $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_CANCEL, ) ); } /** * Track the event if we have a failed checksum. */ public function maybe_track_failed_checksum() { // If there is a Checksum issue, then check DB's value from time(). $checksum_issue = (int) get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 0 ); if ( $checksum_issue > 0 ) { $scan_analytics = wd_di()->get( Scan_Analytics::class ); $reason = 'Failed to fetch checksums from wp.org'; $scan_analytics->track_feature( $scan_analytics::EVENT_SCAN_FAILED, array( $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR, 'Error_Reason' => $reason, ) ); $this->log( $reason, Scan_Controller::SCAN_LOG ); } } /** * Clean up data generate by current scan. */ public function clean_up() { $this->delete_interim_data(); $models = Scan_Model::get_last_all(); if ( is_array( $models ) && array() !== $models ) { // Remove the latest. Don't remove code to find the first value. $current = array_shift( $models ); foreach ( $models as $model ) { $model->delete(); } } } /** * Get the total scanning active issues. * * @return int $count */ public function indicator_issue_count(): int { $count = 0; $scan = Scan_Model::get_last(); if ( is_object( $scan ) && ! is_wp_error( $scan ) ) { // Only Scan issues. $count = (int) $scan->count( null, Scan_Item::STATUS_ACTIVE ); } return $count; } /** * Checks if any scan type is active. * * @return bool True if any scan type is active, false otherwise. */ public function is_any_scan_type_active(): bool { $settings = $this->settings; $file_change_check = $settings->is_checked_any_file_change_types(); // Check#1 the 'File change detection' type because only it's available with nested types. // Check#2 the Abandoned plugin type. $check_free_settings = $file_change_check || $settings->check_abandoned_plugin; // Indicates if the API key is available for HC features. if ( $this->is_apikey ) { // HC version. Check all parent types. return $check_free_settings || $settings->check_known_vuln || $settings->scan_malware; } else { // Without HC access. return $check_free_settings; } } /** * Checks if any scan type is active based on the scan settings and the user's membership status. * * @param array $scan_settings The scan settings. * * @return bool Returns true if any scan type is active, false otherwise. */ public function check_scan_active_by( $scan_settings ): bool { if ( ! isset( $scan_settings['integrity_check'] ) || ! $scan_settings['integrity_check'] ) { // Check the parent type. $file_change_check = false; } elseif ( $scan_settings['integrity_check'] && ( ! isset( $scan_settings['check_core'] ) || ! $scan_settings['check_core'] ) && ( ! isset( $scan_settings['check_plugins'] ) || ! $scan_settings['check_plugins'] ) ) { // Check the parent and child types. $file_change_check = false; } else { $file_change_check = true; } $check_free_settings = $file_change_check || ( isset( $scan_settings['check_abandoned_plugin'] ) && $scan_settings['check_abandoned_plugin'] ); // For HC features. if ( $this->is_apikey ) { // Similar to is_any_scan_type_active() method.Check all parent types including HC features. return $check_free_settings || ( isset( $scan_settings['check_known_vuln'] ) && $scan_settings['check_known_vuln'] ) || ( isset( $scan_settings['scan_malware'] ) && $scan_settings['scan_malware'] ); } return $check_free_settings; } /** * Update the idle scan status due the time limit. * * @since 2.6.1 */ public function update_idle_scan_status() { $idle_scan = wd_di()->get( Scan_Model::class )->get_idle(); if ( is_object( $idle_scan ) ) { $ready_to_send = false; if ( Scan_Model::STATUS_IDLE === $idle_scan->status ) { $ready_to_send = true; } $this->delete_interim_data(); if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( 'defender/async_scan' ); } $idle_scan->status = Scan_Model::STATUS_IDLE; $idle_scan->task_checkpoint = 'time_limit'; $idle_scan->save(); $this->remove_lock( $this->lock_filename ); if ( $ready_to_send ) { do_action( 'defender_notify', 'malware-notification', $idle_scan ); } } } /** * Update the idle scan status due the checksum issue. * * @param object $scan The current scan. * * @since 4.9.0 */ public function update_idle_scan_status_by_checksum_issue( $scan ) { $ready_to_send = false; if ( Scan_Model::STATUS_IDLE === $scan->status ) { $ready_to_send = true; } $this->delete_interim_data(); if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( 'defender/async_scan' ); } $scan->status = Scan_Model::STATUS_IDLE; $scan->task_checkpoint = 'checksum_issue'; $scan->save(); $this->remove_lock( $this->lock_filename ); if ( $ready_to_send ) { do_action( 'defender_notify', 'malware-notification', $scan ); } } /** * Clear all temporary scan data. * * @since 2.6.1 */ private function delete_interim_data() { delete_site_option( Gather_Fact::CACHE_CORE ); delete_site_option( Gather_Fact::CACHE_CONTENT ); delete_site_option( Malware_Scan::YARA_RULES ); delete_site_option( Core_Integrity::CACHE_CHECKSUMS ); delete_site_option( Plugin_Integrity::PLUGIN_SLUGS ); delete_site_option( Plugin_Integrity::PLUGIN_SLUGS_CHECKPOINT ); delete_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS ); delete_site_option( self::PLUGINS_ACTIONED ); $this->maybe_track_failed_checksum(); } /** * Display styles on the Plugins page. */ public function show_plugin_admin_styles() { $custom_css = '.vulnerability-indent{ padding-left: 26px; } .plugins .plugin-update-tr .plugin-update.plugin-vulnerability{box-shadow: inset 0 0px 0 rgb(0 0 0 / 10%); border-bottom: rgb(0 0 0 / 10%) solid 1px;}'; wp_add_inline_style( 'defender-menu', $custom_css ); } /** * Display update information for a plugin. * * @param string $file Plugin basename. * @param array $plugin_data Plugin information. * * @return void */ public function attach_plugin_vulnerability_warning( $file, $plugin_data ) { /** * WP Plugin list table instance. * * @var WP_Plugins_List_Table $wp_list_table */ $wp_list_table = _get_list_table( 'WP_Plugins_List_Table', array( 'screen' => get_current_screen() ) ); $bugs = $this->vulnerability_details[ $file ]['bugs']; if ( ! is_array( $bugs ) || array() === $bugs ) { return; } $last_fixed_in = '0'; // Check if there have been updates since the last scan. $exist_update = true; if ( isset( $plugin_data['Version'] ) && '' !== $plugin_data['Version'] ) { // The current plugin version. $current_version = $plugin_data['Version']; foreach ( $bugs as $bug_details ) { // If the fixed version is existed then get the latest one. if ( isset( $bug_details['fixed_in'] ) && '' !== $bug_details['fixed_in'] && version_compare( $bug_details['fixed_in'], $last_fixed_in, '>' ) ) { $last_fixed_in = $bug_details['fixed_in']; } } if ( version_compare( $last_fixed_in, $current_version, '>' ) ) { $exist_update = false; } } // If there were updates, do not display notice. if ( $exist_update ) { return; } if ( ( ! isset( $plugin_data['slug'] ) || '' === $plugin_data['slug'] ) && isset( $this->vulnerability_details[ $file ]['base_slug'] ) ) { $plugin_data['slug'] = $this->vulnerability_details[ $file ]['base_slug']; } if ( is_network_admin() || ! is_multisite() ) { if ( is_network_admin() ) { $active_class = is_plugin_active_for_network( $file ) ? ' active' : ''; } else { $active_class = is_plugin_active( $file ) ? ' active' : ''; } printf( '