ONECOM_WP_CORE_VERSION, 'plugins' => array(), 'themes' => array(), ); private $http_error = false; private $http_response; private $http_args = array( 'timeout' => 10, 'httpversion' => '1.0', 'compress' => false, 'decompress' => true, 'sslverify' => true, 'stream' => false, ); public $isNewVulnerabilityExist = false; public $isFixVersionMissing = false; public function __construct() { $this->settings = new OCVMSettings(); $this->setCron(); } public function collectVersions(): void { // get all active plugins $activePlugins = get_site_option( 'active_plugins' ); // active plugins' slug and version foreach ( $activePlugins as $activePlugin ) { $this->versions['plugins'][] = array( 'slug' => explode( DIRECTORY_SEPARATOR, $activePlugin )[0], 'installed_version' => get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $activePlugin, false, false )['Version'], ); } // in case current theme is a child theme then fetch parent theme's details if ( get_template_directory() !== get_stylesheet_directory() && ! empty( wp_get_theme()->parent() ) ) { $theme = wp_get_theme()->parent(); } else { // get active theme $theme = wp_get_theme(); } // active theme's slug and version $this->versions['themes'][] = array( 'slug' => $theme->template, 'installed_version' => $theme->version, ); } /** * Send versions of all plugins/themes/core */ public function sendVersions() { // stats $this->sendVersionStats( $this->versions ); // http call $this->http_args['user-agent'] = 'WordPress/' . ONECOM_WP_CORE_VERSION . '; ' . home_url(); $this->http_args['body'] = json_encode( $this->versions ); $this->http_response = wp_remote_post( $this->settings::ocvm_endpoint, $this->http_args ); } /** * Process http_response */ public function processResponse() { if ( is_wp_error( $this->http_response ) ) { if ( isset( $this->http_response->errors['http_request_failed'] ) ) { $this->http_error = __( 'Connection timed out', OC_VALIDATOR_DOMAIN ); } else { $this->http_error = $this->http_response->get_error_message(); } } else { if ( wp_remote_retrieve_response_code( $this->http_response ) !== 200 ) { $this->http_error = '(' . wp_remote_retrieve_response_code( $this->http_response ) . ') ' . wp_remote_retrieve_response_message( $this->http_response ); } else { $body = wp_remote_retrieve_body( $this->http_response ); $body_arr = json_decode( $body, 1 ); if ( null !== $body_arr['error'] ) { error_log( 'Error reported by API endpoint --> ' . $body ); } else { // To handle the case of plugins that are having same slug for free and pro version but different names $plugins_array = $body_arr['data']['plugins'] ?? array(); if ( is_array( $plugins_array ) ) { foreach ( $plugins_array as $plugin => $vulnerabilities ) { $name = $this->get_name_for_slug( $plugin, 'plugins' ); foreach ( $vulnerabilities['vulnerabilities'] as $key => $value ) { if ( isset( $value['product_name_premium'] ) && '' !== $value['product_name_premium'] && $name !== $value['product_name_premium'] ) { error_log( 'vulnerability for the premium version of plugin found removing it..' ); unset( $plugins_array[ $plugin ]['vulnerabilities'][ $key ] ); // Check if vulnerabilities array is empty after unset if ( empty( $plugins_array[ $plugin ]['vulnerabilities'] ) ) { unset( $plugins_array[ $plugin ] ); break; } } } } } $this->scan_data = array_merge( array( 'plugins' => (array) $plugins_array, 'themes' => (array) $body_arr['data']['themes'], 'wp' => (array) $body_arr['data']['wp'], ) ); } } } } /** * Check if Vulnerability reported by Endpoint * If no vulnerabilities, clear existing vulnerabilities in db */ public function vulnerabilityExists(): bool { if ( empty( $this->scan_data['wp'] ) && empty( $this->scan_data['plugins'] ) && empty( $this->scan_data['themes'] ) ) { // Get existing VM data $existing_vm_data = $this->settings->get(); // if no vulnerability exits now, push existing vulnerability (if any) to VM log as they are fixed (via update, delete, deactivation) $fixed_vuls = $existing_vm_data['vulnerabilities']; if ( count( $fixed_vuls ) > 0 ) { $history_log_obj = new OCVMHistoryLog(); $history_log_obj->iterateVulnerabilitiesForLog( $fixed_vuls ); } // As no vulnerability found in this scan, clear existing vulnerabilities (however retain settings) $existing_vm_data['vulnerabilities'] = array(); $this->settings->update( $existing_vm_data ); return false; } return true; } /** * Get FQN of plugin by slug * @param $slug string Theme's stylesheet or Plugin's PHP dir name * @return string Name of the plugin/theme */ protected function get_name_for_slug( $slug, $type ): string { if ( 'themes' === $type ) { // theme headers $themeData = wp_get_theme( $slug ); return $themeData->get( 'Name' ); } require_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin_files = get_plugins( '/' . $slug ); if ( ! $plugin_files ) { return ''; } $plugin_files = array_keys( $plugin_files ); $plugin_dir = $slug . '/' . reset( $plugin_files ); $pluginData = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . $plugin_dir ); return $pluginData['Name']; } /** * Iterator */ public function arrayIterator( $items, $type = 'plugins' ) { $arr_items = array(); foreach ( $items as $slug => $vuls ) { // find the highest fix version across all the vulnerabilities found for this item $arr_items[ $slug ]['fixed_in'] = max( array_column( $vuls['vulnerabilities'], 'fixed_in' ) ); $arr_items[ $slug ]['name'] = $this->get_name_for_slug( $slug, $type ); $arr_items[ $slug ] = array_merge( $arr_items[ $slug ], $vuls ); // cleanup duplicate vulnerability codes $vuls_arr = array(); foreach ( $arr_items[ $slug ]['vulnerabilities'] as $k => $v ) { //check is fixed version is missing if ( $arr_items[ $slug ]['vulnerabilities'][ $k ]['fixed_in'] === '' ) { // Store items with missing fixed-in, to prevent repeat mail after update attempt if ( ! array_key_exists( $type, $this->items_without_fixed_in ) || ! in_array( $slug, $this->items_without_fixed_in[ $type ] ) ) { $this->items_without_fixed_in[ $type ][] = $slug; } error_log( 'Fixed in missing for ' . $type ); $this->isFixVersionMissing = true; } //round cvss score at two decimal points $arr_items[ $slug ]['vulnerabilities'][ $k ]['cvss_score'] = sprintf( '%.2f', $arr_items[ $slug ]['vulnerabilities'][ $k ]['cvss_score'] ); $v['cvss_score'] = sprintf( '%.2f', $v['cvss_score'] ); $vuln_type = $v['vuln_type']; $fixedIn = $v['fixed_in']; if ( isset( $vuls_arr[ $vuln_type ] ) ) { // Compare fixed_in values to keep the one with the higher fixed_in value $existingFixedIn = $vuls_arr[ $vuln_type ]['fixed_in']; if ( version_compare( $fixedIn, $existingFixedIn, '>' ) ) { // Replace the existing vulnerability with the new one $vuls_arr[ $vuln_type ] = $v; } elseif ( version_compare( $fixedIn, $existingFixedIn, '<=' ) ) { unset( $arr_items[ $slug ]['vulnerabilities'][ $k ] ); } } else { $vuls_arr[ $vuln_type ] = $v; } } // Convert the associative array back to a sequential array $arr_items[ $slug ]['vulnerabilities'] = array_values( $vuls_arr ); } return $arr_items; } /** * Save vulnerabilities in DB */ public function saveVulnerabilities() { $existing = $this->settings->get(); $db_data = array(); $history_log_obj = new OCVMHistoryLog(); //TODO: delete the entries of plugins/themes/core from $existing data if any of them dont exist now. // This is required to clean up stale data and to avoid sending/showing irrelevant mails/notifications. if ( ! empty( $this->scan_data['plugins'] ) ) { $db_data['plugins'] = $this->arrayIterator( $this->scan_data['plugins'] ); } if ( ! empty( $this->scan_data['themes'] ) ) { $db_data['themes'] = $this->arrayIterator( $this->scan_data['themes'], 'themes' ); } if ( ! empty( $this->scan_data['wp'] ) && ! empty( $this->scan_data['wp']['vulnerabilities'] ) ) { $temp = array_unique( array_column( $this->scan_data['wp']['vulnerabilities'], 'vuln_type' ) ); $unique_vul = array_intersect_key( $this->scan_data['wp']['vulnerabilities'], $temp ); shuffle( $unique_vul ); foreach ( $unique_vul as $k => $v ) { //check is fixed version is missing if ( $v['fixed_in'] === '' ) { error_log( 'Fixed in missing for WP' ); $this->items_without_fixed_in['wp'] = ''; $this->isFixVersionMissing = true; } //round cvss score at two decimal points $unique_vul[ $k ]['cvss_score'] = sprintf( '%.2f', $unique_vul[ $k ]['cvss_score'] ); } $wp_vuls = $unique_vul; $db_data['wp']['installed_version'] = $this->scan_data['wp']['installed_version']; $db_data['wp']['fixed_in'] = max( array_column( $wp_vuls, 'fixed_in' ) ); $db_data['wp']['vulnerabilities'] = $wp_vuls; $db_data['wp']['name'] = 'WordPress Core'; } //check is new vulnerability exist $this->isNewVulnerabilityFound( $db_data, $existing ); if ( empty( $existing['vulnerabilities'] ) ) { $existing['vulnerabilities'] = $db_data; } else { $this->last_scan_data = $this->extractVulnerableItems( $existing['vulnerabilities'] ); // if existing vulnerabilities are not in latest scan (updated, deleted, or deactivated), push to VM log $fixed_vuls = $history_log_obj->extractFixedVulnerabilities( $existing['vulnerabilities'], $db_data ); if ( count( $fixed_vuls ) > 0 ) { error_log( 'Fixed vulnerabilities found to push in log' ); $history_log_obj->iterateVulnerabilitiesForLog( $fixed_vuls ); } else { error_log( 'No fixed vulnerabilities found so far' ); } $existing['vulnerabilities'] = $db_data; } // save all vulnerabilities $existing['vulnerabilities'] = $db_data; $this->settings->update( $existing ); } /** * Check is found any new vulnerability * @param $new_vul * @param $existing_vul * @return void */ public function isNewVulnerabilityFound( $new_vul, $existing_vul ) { global $wp_version; if ( ! is_array( $new_vul ) && ! is_array( $existing_vul ) ) { return; } error_log( '~~~ Checking for new vulnerability ~~~' ); //Prepare default structure for vulnerability check $new_vul = $new_vul + array( 'plugins' => array(), 'themes' => array(), 'wp' => array( 'vulnerabilities' => array() ), ); $existing_vul['vulnerabilities'] = $existing_vul['vulnerabilities'] + array( 'plugins' => array(), 'themes' => array(), 'wp' => array( 'vulnerabilities' => array() ), ); $pluginCheck = ( isset( $new_vul['plugins'] ) ) ? array_diff_key( $new_vul['plugins'], $existing_vul['vulnerabilities']['plugins'] ) : array(); //check new vulnerability for themes $themeCheck = ( isset( $new_vul['themes'] ) ) ? array_diff_key( $new_vul['themes'], $existing_vul['vulnerabilities']['themes'] ) : array(); //check new vulnerability for WP $existingWP = isset( $existing_vul['vulnerabilities']['wp']['vulnerabilities'] ) ? count( $existing_vul['vulnerabilities']['wp']['vulnerabilities'] ) : 0; $newWP = ( isset( $new_vul['wp']['vulnerabilities'] ) ) ? count( $new_vul['wp']['vulnerabilities'] ) : 0; //get new vulnerability regarding WP core $wpCheck = $newWP - $existingWP; //get the max fixed_in version $ver = ( $newWP > 0 ) ? max( array_column( $new_vul['wp']['vulnerabilities'], 'fixed_in' ) ) : $wp_version; $user_settings = $this->settings->get(); //check is installed WP and coming WP vulnerability fixed_in are same or empty //then, override the WP vulnerability if found the same version during auto-update on if ( version_compare( $wp_version, $ver ) >= 0 && 1 == $user_settings['settings']['auto_update'] ) { $wpCheck = 0; } if ( count( $pluginCheck ) > 0 || count( $themeCheck ) > 0 || $wpCheck > 0 ) { error_log( '~~~ New vulnerability found ~~~' ); $this->isNewVulnerabilityExist = true; return; } error_log( '~~~ No new vulnerability found ~~~' ); } /** * Check and Display Vulnerabilities if any */ public function manageVulnerabilities(): void { // return if wp core update is in progress/scheduled shortly if (!$this->wp_core_updates_not_scheduled()) { error_log('wp core update in-progress/scheduled shortly terminating VM scan.'); return; } error_log( 'Scan started!' ); self::collectVersions(); self::sendVersions(); self::processResponse(); if ( false !== $this->http_error ) { error_log( 'Error found -- ' . $this->http_error ); return; // stop execution and log error if required } if ( null === $this->scan_data ) { error_log( 'No data found from API response -- ' . json_encode( $this->http_response ) ); return; // stop execution and log error if required } if ( true !== self::vulnerabilityExists() ) { error_log( 'No vulnerabilities found! All good! Exiting...' ); return; } error_log( 'Vulnerabilities found!' ); self::saveVulnerabilities(); // get user's preferences $user_settings = $this->settings->get(); $notify_admins = new OCVMSendEmails(); //get daily email send status $daily_email_sent = get_site_transient( 'ocvm_daily_email_sent' ); //Allow auto-update only for mWP if ( 1 == $user_settings['settings']['auto_update'] && $this->settings->isPremium() ) { // Auto-update items $auto_update = new OCVMAutoUpdates(); $auto_update->updateItems(); // check user's preferences for email type, send vuls-fixed and vuls-found email if ( 1 === $user_settings['settings']['notify_all'] && 1 === $user_settings['settings']['email_type']['email_fixed'] && current_action() === 'ocvm_scan' ) { error_log( 'Sending vuls-fixed / vuls-found on ocvm_scan run as per requirement' ); $this->fixedAndNoAutoFixedEmail( $notify_admins, $auto_update ); } } else { // check user's preferences for email type if ( 1 === $user_settings['settings']['notify_all'] && 1 === $user_settings['settings']['email_type']['email_detect'] ) { // send vuls-found email // on new vulnerability found we will send always email // ocvm_scan should run once a day if email is not sent for a day // if email is already sent, then will skip sending email via ocvm_scan // otherwise, we will skip sending email if ( $this->isNewVulnerabilityExist ) { error_log( 'Sending email on new vulnerability found.' ); $notify_admins->sendEmail(); ( ! $daily_email_sent ) ? set_site_transient( 'ocvm_daily_email_sent', 'yes', 7 * DAY_IN_SECONDS ) : ''; } elseif ( current_action() === 'ocvm_scan' && ! $daily_email_sent ) { error_log( 'Sending email on ocvm_scan run' ); $notify_admins->sendEmail(); //set site transient for 24hr set_site_transient( 'ocvm_daily_email_sent', 'yes', 7 * DAY_IN_SECONDS ); } else { error_log( 'Skip sending email because no new vulnerability found.' ); } } } } public function fixedAndNoAutoFixedEmail( $notify_admins, $auto_update ): void { error_log( 'Sending email for vulnerability on the basis of condition' ); //send auto-update email for vulnerability fixed if no case of missing fixed version if ( ! $this->isFixVersionMissing ) { error_log( 'Sending email only for fixed vulnerability' ); $notify_admins->sendEmail( 'vulsFixed', $auto_update->updateAttempt ); return; } // Send vulsNotAutoFixed only if any fixed version is missing and weekly mail is not sent or new vul found $daily_email_sent = get_site_transient( 'ocvm_daily_email_sent' ); if ( current_action() === 'ocvm_scan' && ( ! $daily_email_sent || ! $this->vulExistsInLastScan( $this->items_without_fixed_in, $this->last_scan_data ) ) ) { $notify_admins->sendEmail( 'vulsNotAutoFixed', $auto_update->updateAttempt ); set_site_transient( 'ocvm_daily_email_sent', 'yes', 7 * DAY_IN_SECONDS ); } error_log( 'Sending email for vulnerability fix always' ); $notify_admins->sendEmail( 'vulsFixed', $auto_update->updateAttempt ); } /** * Set cron if not exists */ public function setCron() { if ( ! wp_next_scheduled( $this->settings::wpcron_hook ) ) { wp_schedule_event( time(), 'daily', // TODO: enable 24 hours duration before real deployment // $this->settings->get()['scan_duration'], $this->settings::wpcron_hook ); } } /** * Prepare vulerable (last scan) items names, rmove `vulnerablities` and extra data * * @return array */ public function extractVulnerableItems( $vuls ): array { $vul_items = array(); foreach ( $vuls as $item => $value ) { // Get vulnerable plugins and themes slugs if ( $item === 'plugins' || $item === 'themes' ) { $vul_items[ $item ] = array_keys( $value ); } elseif ( $item === 'wp' ) { $vul_items[ $item ] = ''; } } return $vul_items; } /** * Check if all current i.e new vulnerable (missing fixed-in) items were already exists in last scan * If exists (and weekly mail sent), we will prevent sending 'vulsNotAutoFixed' mail after auto update * * @return bool */ public function vulExistsInLastScan( $new_vuls, $old_vuls ): bool { foreach ( $new_vuls as $type => $items ) { // Return false if the 'plugins', 'themes' or 'wp' key found in new_vuls does not exists in old_vuls if ( ! isset( $old_vuls[ $type ] ) ) { return false; } // Return false any plugin/theme from new_vuls does not exists in old_vuls, else return true as all vuls found in last scan foreach ( $items as $item ) { if ( ! in_array( $item, $old_vuls[ $type ] ) ) { return false; } } } return true; } /** * Delete cron upon deactivation */ public function deleteCron() { $timestamp = wp_next_scheduled( 'bl_cron_hook' ); wp_unschedule_event( $timestamp, 'bl_cron_hook' ); } /** * Schedule WPCron scan scheduling after 30sec for multiple events (activation, deactivation, updation, deletion) * @param $plugin * @param $network_wide * @return void */ public function scheduleOCVMScan() { wp_schedule_single_event( time() + 30, 'ocvm_scan' ); } /** * Schedule event after 30sec of auto update enabled * * @return void */ public function scheduleCronAfterAutoupdate() { wp_schedule_single_event( time() + 30, 'ocvm_scan' ); } /** * Send version stats */ public function sendVersionStats( $ver = array() ): void { if ( empty( $ver ) ) { return; } // Add one.com features related data $additional_info = $this->ocFeaturesInfo( $ver ); class_exists( 'OCPushStats' ) ? OCPushStats::push_vul_monitor_stats( 'scan', 'setting', 'vulnerability_monitor', array( 'active_versions' => $additional_info ) ) : ''; } /** * Add one.com features related essential info in VM Scan */ public function ocFeaturesInfo($ver) { // Add ALP status $alp_status = (string) get_site_option( 'onecom_login_masking', '' ); $ver['alp_status'] = $alp_status; // add error page status $error_class_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'fatal-error-handler.php'; $error_page = new Onecom_Error_Page(); $error_page_status = ( file_exists( $error_class_path ) && $error_page->is_onecom_plugin() ) ? '1' : '0'; $ver['error_page_status'] = $error_page_status; // add cookie banner status $settings = get_site_option( 'oc_cb_configuration' ); $cb_status = ( empty( $settings ) || empty( $settings['config'] ) ) ? '0' : $settings['config']['show']; $ver['cookie_banner_status'] = $cb_status; // add old cu consent $old_consent = get_site_option( 'oc_cu_consent' ); $old_consent_status = false !== $old_consent ? $old_consent : ''; $ver['old_consent_status'] = $old_consent_status; // add new cu consent $new_consent = get_site_option( 'onecom_data_consent_status' ); $new_consent_status = false !== $new_consent ? $new_consent : ''; $ver['data_consent_status'] = $new_consent_status; return $ver; } /** * @return bool * check if wp core update is running */ public function wp_core_updates_not_scheduled() { if (file_exists(ABSPATH . '.maintenance')) { return false; // Update is in progress } // Check if update lock is active (via core_updater.lock) $update_locked = get_option('core_updater.lock'); if ($update_locked) { return false; } // Check if an automatic update is scheduled in the near future $scheduled_event = wp_get_scheduled_event('wp_maybe_auto_update'); if ($scheduled_event) { $time_until_next_update = $scheduled_event->timestamp - time(); if ($time_until_next_update > 0 && $time_until_next_update < 1800) { // Next 30 minutes return false; // Update is scheduled soon } } // No update in progress or scheduled soon return true; } }