15 ) ); if ( is_wp_error( $ping ) || (int) wp_remote_retrieve_response_code( $ping ) >= 500 ) { return false; } wp_version_check(); $updates = get_core_updates(); global $wp_version; self::upsert_status( 'core', 'core', 'WordPress Core', $wp_version, self::STATUS_CLEAN ); update_option( self::LAST_SUCCESS_OPTION, current_time( 'mysql', true ), false ); if ( empty( $updates ) || ! is_array( $updates ) || 'upgrade' !== ( $updates[0]->response ?? '' ) ) { return true; } $update = $updates[0]; Argus_Findings::record( 'vulnerability', 'medium', array( 'what_happened' => sprintf( 'WordPress core is out of date: running %s, %s is available', $wp_version, $update->version ), 'why_it_matters' => 'Older WordPress core releases can be missing security fixes, even when no specific vulnerability is separately confirmed for this exact version.', 'what_argus_found' => sprintf( 'Checked against WordPress.org\'s own update API. Current: %s. Available: %s.', $wp_version, $update->version ), 'when_it_happened' => current_time( 'mysql' ), 'why_suspicious' => 'This is a currency check, not a confirmed exploit -- ARGUS flags any available core update for visibility, not only ones known to fix a specific CVE.', 'what_could_be_affected' => 'The entire site, since WordPress core underlies every plugin and theme.', 'what_should_you_do' => 'Review the WordPress release notes for ' . $update->version . ' and update when convenient. If you have a reason to defer (compatibility testing, staging validation), that is a reasonable choice -- just don\'t defer indefinitely.', ), array( 'current_version' => $wp_version, 'available_version' => $update->version, 'action' => array( 'type' => 'update_core', 'label' => sprintf( __( 'Update to %s', 'argus-wordpress-defence' ), $update->version ), ), ) ); return true; } public static function inventory() { global $wp_version; $items = array( array( 'type' => 'core', 'slug' => 'core', 'name' => 'WordPress Core', 'version' => $wp_version ) ); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( get_plugins() as $plugin_file => $plugin_data ) { $items[] = array( 'type' => 'plugin', 'slug' => self::slug_from_plugin_file( $plugin_file ), 'name' => $plugin_data['Name'] ?? $plugin_file, 'version' => $plugin_data['Version'] ?? '', ); } foreach ( wp_get_themes() as $stylesheet => $theme ) { $items[] = array( 'type' => 'theme', 'slug' => $stylesheet, 'name' => $theme->get( 'Name' ) ?: $stylesheet, 'version' => $theme->get( 'Version' ) ?: '', ); } return $items; } public static function resolve_inventory() { global $wpdb; $vuln_cache_total = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . Argus_DB::table( 'vuln_cache' ) ); // phpcs:ignore foreach ( self::inventory() as $item ) { if ( 'core' === $item['type'] ) { continue; } self::resolve_component( $item['type'], $item['slug'], $item['name'], $item['version'], $vuln_cache_total ); } self::prune_removed_components(); } protected static function resolve_component( $type, $slug, $name, $version, $vuln_cache_total ) { global $wpdb; if ( 0 === $vuln_cache_total ) { self::upsert_status( $type, $slug, $name, $version, self::STATUS_PENDING ); return; } $table = Argus_DB::table( 'vuln_cache' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE component_type = %s AND slug = %s", $type, $slug ) // phpcs:ignore ); $flagged = null; foreach ( $rows as $row ) { if ( '' === $version || ! $row->vulnerable_below ) { continue; } if ( version_compare( $version, $row->vulnerable_below, '<' ) ) { $flagged = $row; break; } } if ( $flagged ) { self::upsert_status( $type, $slug, $name, $version, self::STATUS_VULNERABLE, $flagged ); self::record_vulnerability_finding( $type, $slug, $name, $version, $flagged ); return; } self::upsert_status( $type, $slug, $name, $version, self::STATUS_CLEAN ); } protected static function upsert_status( $type, $slug, $name, $version, $status, $vuln_row = null ) { global $wpdb; $table = Argus_DB::table( 'vuln_status' ); $now = current_time( 'mysql', true ); $data = array( 'component_type' => $type, 'slug' => $slug, 'name' => $name, 'installed_version' => $version, 'checked_version' => $version, 'status' => $status, 'severity' => $vuln_row->severity ?? null, 'cve' => $vuln_row->cve ?? null, 'fixed_in' => $vuln_row->fixed_in ?? null, 'description' => $vuln_row->description ?? null, 'last_checked_at' => $now, 'updated_at' => $now, ); $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE component_type = %s AND slug = %s", $type, $slug ) // phpcs:ignore ); if ( $existing_id ) { $wpdb->update( $table, $data, array( 'id' => $existing_id ) ); } else { $wpdb->insert( $table, $data ); } } protected static function prune_removed_components() { global $wpdb; $table = Argus_DB::table( 'vuln_status' ); $current = array(); foreach ( self::inventory() as $item ) { $current[] = $item['type'] . '|' . $item['slug']; } $existing = $wpdb->get_results( "SELECT id, component_type, slug FROM {$table}" ); // phpcs:ignore foreach ( $existing as $row ) { if ( ! in_array( $row->component_type . '|' . $row->slug, $current, true ) ) { $wpdb->delete( $table, array( 'id' => $row->id ), array( '%d' ) ); } } } protected static function record_vulnerability_finding( $type, $slug, $name, $version, $vuln_row ) { Argus_Findings::record( 'vulnerability', $vuln_row->severity, array( 'what_happened' => sprintf( '%s "%s" (v%s) has a known vulnerability%s', ucfirst( $type ), $name, $version, $vuln_row->cve ? ' (' . $vuln_row->cve . ')' : '' ), 'why_it_matters' => $vuln_row->description ?: 'This version is affected by a publicly documented vulnerability.', 'what_argus_found' => sprintf( 'Installed: %s. Vulnerable below: %s. Fixed in: %s.', $version, $vuln_row->vulnerable_below, $vuln_row->fixed_in ?: 'unknown' ), 'when_it_happened' => current_time( 'mysql' ), 'why_suspicious' => 'This is a version-matching result against ARGUS\'s vulnerability intelligence, not a detected exploitation attempt.', 'what_could_be_affected' => 'Depends on the specific vulnerability -- see the description above.', 'what_should_you_do' => $vuln_row->fixed_in ? sprintf( 'Update to version %s or later.', $vuln_row->fixed_in ) : 'No fixed version is currently known -- consider deactivating this component until one is available, or accept the risk deliberately if it\'s not exposed.', ), array( 'component_type' => $type, 'slug' => $slug, 'installed_version' => $version, 'cve' => $vuln_row->cve, 'action' => $vuln_row->fixed_in ? array( 'type' => 'plugin' === $type ? 'update_plugin' : 'update_theme', 'label' => sprintf( __( 'Update to %s', 'argus-wordpress-defence' ), $vuln_row->fixed_in ), 'plugin' => 'plugin' === $type ? self::plugin_file_for_slug( $slug ) : null, 'theme' => 'theme' === $type ? $slug : null, ) : null, ) ); } protected static function plugin_file_for_slug( $slug ) { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( array_keys( get_plugins() ) as $plugin_file ) { if ( self::slug_from_plugin_file( $plugin_file ) === $slug ) { return $plugin_file; } } return null; } public static function import_feed( array $records ) { global $wpdb; $table = Argus_DB::table( 'vuln_cache' ); $now = current_time( 'mysql', true ); $count = 0; foreach ( $records as $r ) { if ( empty( $r['component_type'] ) || empty( $r['slug'] ) ) { continue; } $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE component_type = %s AND slug = %s AND cve <=> %s", // phpcs:ignore $r['component_type'], $r['slug'], $r['cve'] ?? null ) ); $data = array( 'component_type' => $r['component_type'], 'slug' => $r['slug'], 'vulnerable_below' => $r['vulnerable_below'] ?? null, 'fixed_in' => $r['fixed_in'] ?? null, 'severity' => $r['severity'] ?? 'medium', 'cve' => $r['cve'] ?? null, 'description' => $r['description'] ?? '', 'updated_at' => $now, ); if ( $existing_id ) { $wpdb->update( $table, $data, array( 'id' => $existing_id ) ); } else { $wpdb->insert( $table, $data ); } $count++; } if ( $count > 0 ) { self::resolve_inventory(); } return $count; } protected static function slug_from_plugin_file( $plugin_file ) { $parts = explode( '/', $plugin_file ); return $parts[0] ?? $plugin_file; } public static function status() { $last_success = get_option( self::LAST_SUCCESS_OPTION, '' ); global $wpdb; $rows = $wpdb->get_results( 'SELECT * FROM ' . Argus_DB::table( 'vuln_status' ) . ' ORDER BY component_type = "core" DESC, name ASC' ); // phpcs:ignore $components = array(); foreach ( $rows as $row ) { $status = $row->status; if ( 'core' === $row->component_type && $last_success && strtotime( $last_success . ' UTC' ) < ( time() - self::UNAVAILABLE_AFTER_SECS ) ) { $status = self::STATUS_UNAVAILABLE; } $components[] = array( 'type' => $row->component_type, 'slug' => $row->slug, 'name' => $row->name, 'installed_version' => $row->installed_version, 'status' => $status, 'severity' => $row->severity, 'cve' => $row->cve, 'fixed_in' => $row->fixed_in, ); } return array( 'protection' => $last_success ? 'active' : 'starting', 'last_check' => $last_success, 'next_check' => $last_success ? gmdate( 'Y-m-d H:i:s', strtotime( $last_success . ' UTC' ) + self::CHECK_INTERVAL_SECS ) : '', 'components' => $components, ); } }