384 lines
15 KiB
PHP
384 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Vulnerability Monitor History log - A place to display all fixed vulnerabilities
|
|
*
|
|
* When VM Scan happens?
|
|
* Vulnerability monitor scan is initiated whenever an item is updated, activated, deleted or deactivated.
|
|
* Also, when VM setting auto-update enabled, a scan happens. A daily scan is also scheduled.
|
|
*
|
|
* When a fixed vulnerability push to VM Log?
|
|
* There are 3 places where logic for VM Push log (iterateVulnerabilitiesForLog()) is added:
|
|
* 1) vulnerabilityExists() - If no vulnerability exits with latest scan, all existing (if any) will be treated as fixed and push to VM Log
|
|
* 2) saveVulnerabilities() - When saving latest vulnerability after scan, if any existing vulnerability is no longer in latest scan, it will be treated as fixed. So push to VM log (as Deactivated, Deleted or Updated based on current item status)
|
|
* 3) updateItemRecords() - A common function invoked by wp, themes, plugins whenever something is fixed (updated) by VM, so whenever fixed item removed from VM settings db, we push to VM Log.
|
|
*
|
|
* VM History Log's Item Result:
|
|
* - Update meaning that user has updated item manually or clicking on update button or WP's Auto Update
|
|
* - Auto-update meaning that item is updated automatically by Vulnerability Monitor (Setting auto-update is ON)
|
|
*
|
|
* - Auto-update case: If item is updated with latest version instead of fixed version, push latest as fixed version in log
|
|
*/
|
|
class OCVMHistoryLog {
|
|
|
|
|
|
private $settings;
|
|
public $ocvmNotices;
|
|
public $history_log_data = null;
|
|
public $vm_log_table = null;
|
|
private $installed_plugins = array();
|
|
private $active_plugins = array();
|
|
|
|
// Initiate commons
|
|
public function __construct() {
|
|
global $wpdb;
|
|
$this->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" );
|
|
}
|
|
}
|
|
}
|