wpmudev = $wpmudev; $this->scan = $scan; } /** * Performs the vulnerability check. * * @return bool */ public function vuln_check(): bool { global $wp_version; $info_cached = array(); $data = array( 'plugins' => wp_json_encode( $this->gather_fact( 'plugin', $info_cached ) ), 'themes' => wp_json_encode( $this->gather_fact( 'theme', $info_cached ) ), 'wordpress' => $wp_version, ); $ret = $this->make_check( $data ); if ( is_wp_error( $ret ) ) { $this->handle_error( $ret ); return true; } $ignored_issues = $this->get_ignored_issues(); $this->process_results( $ret, $info_cached, $ignored_issues ); $this->scan->calculate_percent( 100, 5 ); $this->add_ignored_issues( $ignored_issues ); return true; } /** * Gathers information about installed plugins or themes. * * @param string $type The type of information to gather (plugin or theme). * @param array $info_cached The cached information. * * @return array */ private function gather_fact( string $type, &$info_cached ): array { $items = array(); if ( 'plugin' === $type ) { $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED ); if ( Scan_Component::are_actioned_plugins( $actioned_plugins ) ) { foreach ( $actioned_plugins as $slug => $plugin_data ) { $items[ $slug ] = $plugin_data['Version']; $info_cached[ $slug ] = array( $plugin_data['Name'], $plugin_data['Version'], $plugin_data['Slug'] ); } } else { // An emergency option if for some reason the plugin's quick data is not saved in the first step. foreach ( $this->get_plugins() as $slug => $plugin ) { $base_slug = $this->get_plugin_slug_by( $slug ); $items[ $base_slug ] = $plugin['Version']; $info_cached[ $base_slug ] = array( $plugin['Name'], $plugin['Version'], $slug ); } } } elseif ( 'theme' === $type ) { $model = Scan::get_last(); foreach ( $this->get_themes() as $slug => $item ) { // It's actual because we don't have ignored theme slugs. if ( is_object( $model ) && $model->is_issue_ignored( $slug ) ) { continue; } $base_slug = explode( '/', $slug ); $base_slug = array_shift( $base_slug ); $items[ $base_slug ] = $item['Version']; $info_cached[ $base_slug ] = array( $item['Name'], $item['Version'], $slug ); } } else { return $items; } return $items; } /** * Makes a request to the WPMUDEV API to check for known vulnerabilities. * * @param mixed $data The data to send in the request. * * @return array|WP_Error */ protected function make_check( $data ) { return $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_KNOWN_VULN, $data, array( 'method' => 'POST' ) ); } /** * Handles the error by tracking the failure event, logging the error message, and updating the scan status. * * @param WP_Error $error The error object containing the error message. */ private function handle_error( WP_Error $error ) { $error_message = $error->get_error_message(); $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_ERROR, 'Error_Reason' => $error_message, ) ); $this->log( $error_message, \WP_Defender\Controller\Scan::SCAN_LOG ); $this->scan->status = Scan::STATUS_ERROR; $this->scan->save(); } /** * Retrieves the list of ignored issues. * * @return array The list of ignored issues. */ private function get_ignored_issues() { $last = Scan::get_last(); return is_object( $last ) ? $last->get_issues( Scan_Item::TYPE_VULNERABILITY, Scan_Item::STATUS_IGNORE ) : array(); } /** * Processes the results of the vulnerability check and saves them in the Scan model. * * @param array $results The results of the vulnerability check. * @param array $info_cached The cached information. * @param array $ignored_issues The ignored issues. */ private function process_results( $results, $info_cached, $ignored_issues ) { $this->process_result( $results['plugins'], $info_cached, 'plugin', $ignored_issues ); $this->process_result( $results['themes'], $info_cached, 'theme', $ignored_issues ); $this->process_result( $results['wordpress'], array(), 'wp_core', $ignored_issues ); } /** * Processes the result of the vulnerability check and saves them in the Scan model. * * @param mixed $result The result of the vulnerability check. * @param array $info The cached information. * @param string $type The type of the vulnerability check. * @param array $ignored_issues The ignored issues. */ private function process_result( $result, $info, $type, $ignored_issues ) { if ( ! is_array( $result ) || array() === $result ) { return; } $this->log( sprintf( 'Checking %s:', $type ) ); $model = $this->scan; foreach ( $result as $base_slug => $bugs ) { if ( 'wp_core' === $type ) { global $wp_version; $is_exist = false; if ( is_array( $ignored_issues ) && array() !== $ignored_issues ) { foreach ( $ignored_issues as $issue ) { if ( $wp_version === $issue->raw_data['version'] ) { $is_exist = true; } } } if ( ! $is_exist ) { $raw_data = array( 'type' => $type, 'slug' => '', 'base_slug' => '', 'version' => $wp_version, 'name' => 'WordPress Core', 'bugs' => array( array( 'vuln_type' => $bugs['vuln_type'], 'title' => $bugs['title'], 'ref' => $bugs['references'], 'fixed_in' => $bugs['fixed_in'], 'cvss_score' => $bugs['cvss']['score'] ?? '', ), ), 'new_structure' => '3.4.0', ); $model->add_item( Scan_Item::TYPE_VULNERABILITY, $raw_data ); } } elseif ( 'plugin' === $type || 'theme' === $type ) { if ( ! isset( $info[ $base_slug ] ) || ! is_array( $info[ $base_slug ] ) ) { continue; } if ( isset( $bugs['confirmed'] ) && ( is_array( $bugs['confirmed'] ) || $bugs['confirmed'] instanceof Countable ? count( $bugs['confirmed'] ) : 0 ) ) { $wp_org_plugin_details = array( 'is_existed' => false, 'last_version' => '0', ); if ( 'plugin' === $type ) { if ( ! $this->is_likely_wporg_slug( $base_slug ) ) { continue; } // Additional conditions for wp.org plugin. $results = $this->handle_wp_org_response_by( $base_slug ); if ( $results['success'] && isset( $results['body']['version'] ) ) { $wp_org_plugin_details = array( 'is_existed' => true, 'last_version' => $results['body']['version'], ); } // If there is not a plugin version, likely the plugin is closed. We'd not display Update button. } [ $name, $current_version, $slug ] = $info[ $base_slug ]; $raw_data = array( 'type' => $type, 'slug' => $slug, 'base_slug' => $base_slug, 'version' => $current_version, 'name' => $name, 'bugs' => array(), ); $confirmed_bugs = (array) $bugs['confirmed']; // @since 3.4.0 Restructure the view in relation to the new CVSS Score field. $raw_data['new_structure'] = '3.4.0'; $this->log( sprintf( '%s has %d known bugs', $slug, count( $confirmed_bugs ) ) ); foreach ( $confirmed_bugs as $bug ) { if ( 'plugin' === $type && $wp_org_plugin_details['is_existed'] && version_compare( $wp_org_plugin_details['last_version'], $bug['fixed_in'], '<=' ) ) { // This is another plugin but with the same slug. continue; } $raw_data['bugs'][] = array( 'vuln_type' => $bug['vuln_type'], 'title' => $bug['title'], 'ref' => $bug['references'], 'fixed_in' => $bug['fixed_in'], 'cvss_score' => $bug['cvss']['score'] ?? '', ); } $model->add_item( Scan_Item::TYPE_VULNERABILITY, $raw_data ); } } } } /** * Adds ignored vulnerability issues to the scan. * * @param array $ignored_issues The array of ignored vulnerability issues to add. */ private function add_ignored_issues( $ignored_issues ) { if ( ! is_array( $ignored_issues ) || array() === $ignored_issues ) { return; } // wp_core issues have no slug, so carry them forward as-is. $wp_core_issues = array(); $other_issues = array(); foreach ( $ignored_issues as $issue ) { if ( 'wp_core' === ( $issue->raw_data['type'] ?? '' ) ) { $wp_core_issues[] = $issue; } else { $other_issues[] = $issue; } } foreach ( $wp_core_issues as $issue ) { $this->scan->add_item( Scan_Item::TYPE_VULNERABILITY, $issue->raw_data, Scan_Item::STATUS_IGNORE ); } $this->scan->carry_forward_ignored_issues( Scan_Item::TYPE_VULNERABILITY, $other_issues, 'slug' ); } }