settings = new OCVMSettings(); $this->ocvmNotices = new OCVMNotifications(); $this->vm_log_table = $wpdb->prefix . 'one_vm_log'; } /** * Fetches all fixed vulnerabilities from VM log table and assign to $this->history_log_data * Max vul display for self-mWP is 3. mWP limit for SQL query is kept 10000 safe side */ public function collectVulnerabilities(): bool { // DB table global $wpdb; $settings = new OCVMSettings(); $limit = ( $settings->isPremium() ) ? 10000 : 3; // Fetch records if the table exists $table_exists = $wpdb->get_var( "SHOW TABLES LIKE '$this->vm_log_table'" ) === $this->vm_log_table; if ( $table_exists ) { $custom_table_data = $wpdb->get_results( "SELECT * FROM $this->vm_log_table ORDER BY timestamp DESC LIMIT $limit", ARRAY_A ); // If no error and records are not empty, assign log data if ( ! $wpdb->last_error && is_array( $custom_table_data ) && ! empty( $custom_table_data ) ) { $this->history_log_data = $custom_table_data; return true; } } return false; } /** * Check if the item type is 'Theme', 'Plugin' or 'Core' * @param $type string 'plugins', 'themes' or 'wp' expected * @return string 'Theme', 'Plugin' or 'Core' to store in db and display in log */ public function getItemDisplayType( $type ): string { if ( 'themes' === $type ) { return 'Theme'; } elseif ( 'plugins' === $type ) { return 'Plugin'; } elseif ( 'wp' === $type ) { return 'Core'; } else { // Ideally, it should never happen return 'Unknown'; } } /** * Iterate fixed vulnerable items to extract their vulnerabilities * It parses wp, themes, core array to push all their vulnerabilities into log (inserted via a separate function) * WP array hierarchy has one less subarray, so treated differently * @param array $data The complete vulnerabilities item array to push into log * @return void */ public function iterateVulnerabilitiesForLog( $data ): void { foreach ( $data as $item_type => $items ) { // If WP, push vulnerabilities here as no further nested array, else go one level further for plugins/themes if ( $item_type === 'wp' ) { // For wp, few things are fixed $items['log_item_display_type'] = $this->getItemDisplayType( $item_type ); $items['log_item_result'] = $items['log_item_result'] ?? 'Updated'; $items['log_item_name'] = 'WordPress'; // Change fix version if 'Updated', keep same if 'Auto-updated' by VM (Here $item_type will serve for both slug and type as 'wp' $items['log_fixed_version'] = in_array( $items['log_item_result'], array( 'Updated', 'Auto-updated' ) ) ? $this->getItemVersion( $items, $item_type, $item_type ) : ''; $this->insertVulnerabilityToDb( $items ); } else { foreach ( $items as $item_slug => $item_data ) { // log_item_display_type means 'Themes', $single_item_type means 'theme', $item_type is 'themes' (all are needed) // Prepare VM Log items detail in required format $item_data['log_item_display_type'] = $this->getItemDisplayType( $item_type ); $single_item_type = strtolower( $this->getItemDisplayType( $item_type ) ); // Keep if result already there in case of Auto-updated, else prepare using item status $item_data['log_item_result'] = $item_data['log_item_result'] ?? $this->getItemStatus( $item_slug, $single_item_type ); // Fetch item name to display in log $item_data['log_item_name'] = $this->getItemName( $item_slug, $item_data['log_item_result'], $item_type ); // Change fix version if 'Updated', keep same if 'Auto-updated' by VM, else no fix version needed $item_data['log_fixed_version'] = in_array( $item_data['log_item_result'], array( 'Updated', 'Auto-updated' ) ) ? $this->getItemVersion( $item_data, $item_slug, $single_item_type ) : ''; $this->insertVulnerabilityToDb( $item_data ); } } } } /** * Push individual vulnerabilities into separate row in table * WP array has one less subarray, so treated differently * @param array $vulnerabilities The vulnerabilities array for single item at a time * @param string $item_type The type of item: 'plugin' or 'theme' or 'wp' * @param string $item_display_name The type name to display in log: 'Plugin' or 'Theme' or 'WordPress' * @return bool */ public function insertVulnerabilityToDb( $item_data ): void { // Additional check to return if no 'vulnerabilities' found for log insertion if ( ! $item_data || ! array_key_exists( 'vulnerabilities', $item_data ) ) { return; } error_log( '****** inserting fixed vulnerability data into log for item: ****** > ' . $item_data['log_item_name'] ); global $wpdb; // create vm_log_table if it does not exist already $this->create_vm_log_table(); // prepare required data for DB $timestamp = date( 'Y-m-d H:i:s', time() ); $item_type = $item_data['log_item_display_type']; $item_name = $item_data['log_item_name']; $item_result = $item_data['log_item_result']; $fix_version = $item_data['log_fixed_version']; foreach ( $item_data['vulnerabilities'] as $item_vul ) { // store exploited status as '0' or '1' in db for future $is_exploited = $item_vul['is_exploited'] ? 1 : 0; $vul_data = array( 'timestamp' => $timestamp, 'vuln_id' => $item_vul['id'], 'installed_version' => $item_data['installed_version'], 'fix_version' => $fix_version, 'url' => $item_vul['url'], 'vuln_type' => $item_vul['vuln_type'], 'status' => $item_result, 'cvss_score' => $item_vul['cvss_score'], 'is_exploited' => $item_vul['is_exploited'], 'item_type' => $item_type, 'item_name' => $item_name, ); // Insert vulnerability into VM log in database $result = $wpdb->insert( $this->vm_log_table, $vul_data ); // If inserted successfully, push stats for each vulnerability push if ( $result ) { error_log( '****** inserted fixed vulnerability log for -> ' . $item_data['log_item_name'] . ' | type: ' . $item_vul['vuln_type'] . ' ******' ); // $this->sendVersionStats($vul_data); } } } /** * Get the version of a theme or plugin by providing its directory name. * * @param array $item_info Info such as type ('theme' or 'plugin') and status of the item * @param string $item_slug The directory name of the theme or plugin. * @param string $single_item_type theme, plugin or core * @return string The version of the theme or plugin, or an empty string if not found. */ public function getItemVersion( $item_data, $item_slug, $single_item_type ): string { // In case Auto-updated by VM, It is either updated with latest or fixed version if ( isset( $item_data['log_item_result'] ) && 'Auto-updated' === $item_data['log_item_result'] && ! empty( $item_data['log_latest_version'] ) ) { return $item_data['log_latest_version']; } elseif ( isset( $item_data['log_item_result'] ) && 'Auto-updated' === $item_data['log_item_result'] ) { return $item_data['fixed_in']; } // Ideally, this is 'Updated' case, so fetch version from installed item if ( 'theme' === $single_item_type ) { $item_info = wp_get_theme( $item_slug ); return $item_info->get( 'Version' ); } elseif ( 'plugin' === $single_item_type ) { $plugins = get_plugins( '/' . $item_slug ); if ( ! empty( $plugins ) ) { $plugin_info = reset( $plugins ); return $plugin_info['Version']; } } elseif ( 'wp' === $single_item_type ) { $wordpress_version = get_bloginfo( 'version' ); if ( ! empty( $wordpress_version ) ) { return $wordpress_version; } } return ''; } /** * How vulnerability was fixed? * Check the (fixed) result status of a plugin or theme. * @param string $slug The plugin or theme slug. * @param string $item_type The type of item: 'plugin' or 'theme'. * @return string The status: 'Updated', 'Deactivated', or 'Deleted' */ public function getItemStatus( $item_slug, $item_type ): string { if ( $item_type === 'plugin' ) { $this->installed_plugins = array_map( 'dirname', array_keys( get_plugins() ) ); $this->active_plugins = array_map( 'dirname', get_option( 'active_plugins', array() ) ); // If plugin active, consider as updated if ( in_array( $item_slug, $this->active_plugins ) ) { $status = 'Updated'; } elseif ( in_array( $item_slug, $this->installed_plugins ) ) { $status = 'Deactivated'; } else { $status = 'Deleted'; } } elseif ( $item_type === 'theme' ) { $current_theme_info = wp_get_theme(); $given_themes_info = wp_get_theme( $item_slug ); // get_stylesheet() returns current theme if ( $current_theme_info->get_stylesheet() === $item_slug ) { $status = 'Updated'; } elseif ( $given_themes_info->exists() ) { $status = 'Deactivated'; } else { $status = 'Deleted'; } } else { // Ideally, WP can be fixed by update only $status = 'Updated'; } return $status; } /** * Get theme/plugin name name via slug * * @param string $item_slug The plugin or theme slug. * @param string $item_status The status: 'Updated', 'Deactivated', or 'Deleted' * @param string $item_type The type of item: 'plugin' or 'theme' or 'wp' * @return string The plugin or theme name, or Core */ public function getItemName( $item_slug, $item_status, $item_type ) { // Return WordPress for wp if ( $item_slug === 'wp' ) { return 'WordPress'; } // If item (theme or plugin) still exist, fetch info from exiting function. If deleted, fetch from wp.org if ( in_array( $item_status, array( 'Updated', 'Auto-updated', 'Deactivated' ) ) ) { return $this->ocvmNotices->get_name_for_slug( $item_slug, $item_type ); } // WordPress.org API URL $api_url = 'https://api.wordpress.org/' . $item_type . '/info/1.0/'; // Request item information from WordPress.org $response = wp_remote_get( $api_url . $item_slug . '.json' ); if ( ! is_wp_error( $response ) ) { $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( $data && isset( $data['name'] ) ) { return $data['name']; } } // Return the plugin directory itself if there's an error or no information found return $item_slug; } /** * Compare two arrays and return the differences in the 'plugins', 'themes', and 'wp' keys. * * @param array $existing_vuls The array representing existing vulnerabilities. * @param array $latest_vuls The array representing the latest vulnerabilities. * * @return array The differences between the two arrays. */ public function extractFixedVulnerabilities( $existing_vuls, $latest_vuls ): array { // Check if 'plugins' key exists in both arrays $plugins_difference = isset( $existing_vuls['plugins'], $latest_vuls['plugins'] ) ? array_diff_key( $existing_vuls['plugins'], $latest_vuls['plugins'] ) : array(); // Check if 'themes' key exists in both arrays $themes_difference = isset( $existing_vuls['themes'], $latest_vuls['themes'] ) ? array_diff_key( $existing_vuls['themes'], $latest_vuls['themes'] ) : array(); // Check if 'wp' key exists in both arrays $wp_difference = isset( $existing_vuls['wp'], $latest_vuls['wp'] ) ? array_diff_key( $existing_vuls['wp'], $latest_vuls['wp'] ) : array(); // Remove empty keys $result = array_filter( array( 'plugins' => $plugins_difference, 'themes' => $themes_difference, 'wp' => $wp_difference, ) ); return $result; } /** * Send stats for VM Log */ public function sendVersionStats( $vul_data = array() ): void { if ( empty( $vul_data ) ) { return; } class_exists( 'OCPushStats' ) ? OCPushStats::push_vul_monitor_stats( 'log', 'setting', 'vulnerability_monitor', array( 'vulnerability_info' => $vul_data ) ) : ''; } // Create custom table (only when push first vulnerability and table does not exist already) public function create_vm_log_table(): void { global $wpdb; // Check if the table already exists if ( $wpdb->get_var( "SHOW TABLES LIKE '$this->vm_log_table'" ) !== $this->vm_log_table ) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $this->vm_log_table ( ID int(11) UNSIGNED NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, vuln_id int(11) UNSIGNED DEFAULT NULL, vuln_type varchar(100) DEFAULT NULL, item_name varchar(255) DEFAULT NULL, item_type varchar(20) DEFAULT NULL, installed_version varchar(20) DEFAULT NULL, fix_version varchar(20) DEFAULT NULL, url varchar(400) DEFAULT NULL, cvss_score float DEFAULT NULL, status varchar(20) DEFAULT NULL, is_exploited TINYINT(1) NOT NULL DEFAULT 0, meta longtext DEFAULT NULL, PRIMARY KEY (ID) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); } } // Delete vm log table upon plugin uninstall public function vm_log_delete(): void { global $wpdb; // Check if table exists before attempting to delete it if ( $wpdb->get_var( "SHOW TABLES LIKE '$this->vm_log_table'" ) === $this->vm_log_table ) { // Drop the table from the database $wpdb->query( "DROP TABLE IF EXISTS $this->vm_log_table" ); } } }