upload_software(); } // Register the actions. add_action( 'patchstack_send_software_data', [ $this, 'upload_software' ] ); add_action( 'patchstack_send_hacker_logs', [ $this, 'upload_firewall_logs' ] ); add_action( 'patchstack_send_event_logs', [ $this, 'upload_activity_logs' ] ); add_action( 'patchstack_import_ap_logs', [ $this, 'import_ap_logs' ] ); // In case a plugin or upgrade has been performed, re-synchronize with the app. add_action( 'activated_plugin', [ $this, 'upload_software' ] ); add_action( 'deactivated_plugin', [ $this, 'upload_software' ] ); add_action( 'deleted_plugin', [ $this, 'upload_software' ] ); add_action( 'upgrader_process_complete', [ $this, 'upload_software' ] ); add_action( '_core_updated_successfully', [ &$this, 'upload_software' ] ); } /** * Synchronize the software data with our API. * This includes plugins, themes, WordPress and PHP version. * * @return void|array */ public function upload_software() { // Get the software data and hash. $data = $this->get_software_data(); $hash = sha1( json_encode( $data ) ); // Do not sync for no reason. if ( ! defined( 'DOING_CRON' ) && ! isset( $_POST['patchstack_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash && ! is_admin() && ! defined( 'WP_CLI' ) ) { return; } // Make sure to not keep calling this function. update_option( 'patchstack_software_upload_attempted', true ); // Synchronize the software list with the API. $results = $this->plugin->api->upload_software( [ 'software' => json_encode( $data ) ] ); if ( isset( $results['success'] ) ) { update_option( 'patchstack_software_data_hash', $hash ); // The result will also contain a list of all vulnerable plugins on the site that is returned by the API. // If the auto update setting is enabled for vulnerable plugins, perform the update once the 15 minute // scheduled task "patchstack_update_plugins" is executed. $update = get_site_option( 'patchstack_auto_update', [] ); if ( isset( $results['vulnerable'] ) && is_array( $update ) && in_array( 'vulnerable', $update ) ) { update_site_option( 'patchstack_vulnerable_plugins', $results['vulnerable'] ); } // If we have vulnerable plugins, determine if we had them before and if not, pull latest firewall rules. if ( isset( $results['vulnerable'] ) && count( $results['vulnerable'] ) > 0 ) { $prev = get_site_option( 'patchstack_latest_vulnerable', [] ); foreach ( $results['vulnerable'] as $vuln ) { if ( is_array( $prev ) && ! in_array ( $vuln, $prev ) ) { do_action( 'patchstack_post_dynamic_firewall_rules' ); break; } } update_site_option( 'patchstack_latest_vulnerable', $results['vulnerable'] ); } else { update_site_option( 'patchstack_latest_vulnerable', [] ); } // If we received the number of vulnerable count. if ( isset ( $results['vulnerability_count'], $results['vulnerability_fix_count'] ) ) { update_option( 'patchstack_vulns_present', $results['vulnerability_count'] ); update_option( 'patchstack_fixes_present', $results['vulnerability_fix_count'] ); } return $results; } return; } /** * Synchronize the firewall logs with our API. * * @return void */ public function upload_firewall_logs() { global $wpdb; // Do not execute upload action on free sites. if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) { return; } // Do not process if we are already processing a previous batch. if ( get_option( 'patchstack_firewall_log_processing', false ) ) { return; } update_option( 'patchstack_firewall_log_processing', true ); // Attempt to fetch data, if any. $lastId = get_option( 'patchstack_firewall_log_lastid', 0 ); $successId = $lastId; // Do a maximum of 100 log entries per cronjob. for ($i = 0; $i <= 1; $i++) { // Pull the data from the database, in batches of 100. $items = $wpdb->get_results( $wpdb->prepare( 'SELECT id, ip, log_date, request_uri, user_agent, fid, method, post_data FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id > %d ORDER BY id LIMIT 0,100', $lastId ) ); // No need to continue if we have no data. if ( $wpdb->num_rows == 0 ) { update_option( 'patchstack_firewall_log_lastid', 0 ); break; } // Construct the array to be uploaded to our API. $logs = []; foreach ( $items as $item ) { // Entries that we don't want to store on the API side. if ( stripos( $item->request_uri, 'wp-comments-post' ) !== false ) { continue; } // Push to entries to be uploaded. $logs[] = [ 'ip' => $item->ip, 'fid' => $item->fid, 'request_uri' => $item->request_uri, 'user_agent' => $item->user_agent, 'method' => $item->method, 'log_date' => $item->log_date, 'post_data' => $item->post_data, ]; $lastId = $item->id; } // JSON encode the logs and upload. $logs = json_encode( $logs ); $results = $this->plugin->api->upload_firewall_logs( [ 'logs' => $logs, 'type' => 'firewall', ] ); if ( isset( $results['errors'] ) ) { update_option( 'patchstack_firewall_log_lastid', $successId ); break; } $successId = $lastId; update_option( 'patchstack_firewall_log_lastid', $successId ); } // Delete the logs. $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId ); // No longer processing. update_option( 'patchstack_firewall_log_processing', false ); } /** * Synchronize the activity logs with our API. * * @return void */ public function upload_activity_logs() { global $wpdb; // Do not execute upload action on free sites. if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) { return; } // Do not process if we are already processing a previous batch. if ( get_option( 'patchstack_eventlog_processing', false ) ) { return; } update_option( 'patchstack_eventlog_processing', true ); // Determine if we should upload failed logins to the app. $where = " AND action != 'failed login' "; if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) { $where = ' '; } // Attempt to fetch data, if any. $lastId = get_option( 'patchstack_eventlog_lastid', 0 ); $successId = $lastId; // Do a maximum of several hundred log entries per cronjob. for ($i = 0; $i <= 1; $i++) { // Pull the data from the database, in batches of 100. $items = $wpdb->get_results( $wpdb->prepare( 'SELECT id, author, ip, object, object_id, object_name, action, date FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id > %d' . $where . 'ORDER BY id LIMIT 0,100', $lastId ), ARRAY_A ); // No need to continue if we have no data. if ( $wpdb->num_rows == 0 ) { update_option( 'patchstack_eventlog_lastid', 0 ); break; } // Get the last ID in the result set. $lastId = $items[count($items) - 1]['id']; // Send to the API. $logs = json_encode( $items ); $results = $this->plugin->api->upload_activity_logs( [ 'logs' => $logs ] ); if ( isset( $results['errors'] ) ) { update_option( 'patchstack_eventlog_lastid', $successId ); break; } $successId = $lastId; update_option( 'patchstack_eventlog_lastid', $successId ); } // Delete the logs. $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId ); // No longer processing. update_option( 'patchstack_eventlog_processing', false ); } /** * Obtain information about the software that the user has installed. * This includes plugins, themes, WordPress and PHP version. * * @return array */ public function get_software_data() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( ! function_exists( 'get_plugin_updates' ) ) { require_once ABSPATH . 'wp-admin/includes/update.php'; } // Refetch updates data if we are performing a plugin listener related action. if ( isset( $_POST['patchstack_secret'] ) ) { @require_once ABSPATH . 'wp-includes/update.php'; @wp_update_themes(); @wp_update_plugins(); } // Fetch list of plugins. $all_plugin = get_plugins(); $installed_plugins = array_keys( $all_plugin ); $updatable_plugins = get_plugin_updates(); $software_list = []; foreach ( $installed_plugins as $plugin ) { if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) { continue; } $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); $new_version = empty( $updatable_plugins[ $plugin ]->update->new_version ) ? '' : $updatable_plugins[ $plugin ]->update->new_version; $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name']; $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version']; if ( ! empty( $plugin_name ) && ! empty( $plugin_version ) ) { // Determine the active state. if ( isset( $_GET['action'], $_GET['plugin'] ) && $_GET['action'] == 'deactivate' && $_GET['plugin'] == $plugin) { $active = 0; } else { $active = (int) is_plugin_active( $plugin ); } $software_list[] = [ 'sw_type' => 'plugin', 'sw_name' => $plugin_name, 'sw_cur_ver' => $plugin_version, 'sw_new_ver' => $new_version, 'sw_key' => $plugin, 'sw_active' => $active ]; } } // Fetch list of themes. $themes = wp_get_themes(); $themes_keys = array_keys( $themes ); $updatable_themes = get_theme_updates(); foreach ( $themes_keys as $theme_key ) { $themes_data = $themes[ $theme_key ]; $theme_temporary = empty( $updatable_themes[ $theme_key ] ) ? '' : $updatable_themes[ $theme_key ]; $theme_new_version = empty( $updatable_themes[ $theme_key ] ) || ! isset( $theme_temporary->update, $theme_temporary->update['new_version'] ) ? '' : $theme_temporary->update['new_version']; $theme_name = $themes_data->get( 'Name' ); $theme_version = $themes_data->get( 'Version' ); if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) { $software_list[] = [ 'sw_type' => 'theme', 'sw_name' => $theme_name, 'sw_cur_ver' => $theme_version, 'sw_new_ver' => $theme_new_version, 'sw_key' => $theme_key, ]; } } // Fetch WordPress version. global $wp_version; $core_updates = get_core_updates(); $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : ''; $software_list[] = [ 'sw_type' => 'wordpress', 'sw_name' => 'WordPress', 'sw_cur_ver' => $wp_version, 'sw_new_ver' => $new_wp_version, ]; // Fetch PHP version. $software_list[] = [ 'sw_type' => 'php', 'sw_name' => 'PHP', 'sw_cur_ver' => phpversion(), 'sw_new_ver' => '', ]; // Fetch database server version. global $wpdb; if ( ! is_null( $wpdb ) ) { $software_list[] = [ 'sw_type' => 'database', 'sw_name' => 'Database', 'sw_cur_ver' => $wpdb->get_var( 'SELECT VERSION()' ), 'sw_new_ver' => '' ]; } return $software_list; } /** * Import the logs generated by the auto prepend firewall rules. * * @return void */ public function import_ap_logs() { if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) { return; } // Do not process if we are already processing a previous batch. if ( get_option( 'patchstack_firewall_log_ap_processing', false ) ) { return; } update_option( 'patchstack_firewall_log_ap_processing', true ); // Attempt to load config file. $logs = __DIR__ . '/../../../pslogs/logs.php'; if ( ! file_exists( $logs ) ) { return; } // Load the extension. if ( ! file_exists( __DIR__ . '/../lib/patchstack/vendor/autoload.php' ) ) { return; } global $wpdb; require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php'; $extension = new Patchstack\Extensions\WordPress\Extension( [], $this ); // Read the logs file. $file = new SplFileObject( $logs ); // Iterate through each line. while ( ! $file->eof() ) { $line = $file->fgets(); // Skip first line. if ( trim($line) == '' || strpos( $line, 'insert( $wpdb->get_blog_prefix( $data['site_id'] ) . 'patchstack_firewall_log', [ 'ip' => $data['ip'], 'request_uri' => $data['request_uri'], 'user_agent' => $data['user_agent'], 'method' => $data['method'], 'fid' => $data['fid'], 'flag' => '', 'post_data' => $data['post_data'], 'block_type' => 'BLOCK' ] ); // Update counters. $hits = (int) $this->get_blog_option( $data['site_id'], 'patchstack_hits_all_time', 0 ); $this->update_blog_option( $data['site_id'], 'patchstack_hits_all_time', $hits + 1 ); $counters = $this->get_blog_option( $data['site_id'], 'patchstack_hits_last_30', [] ); $counters = $extension->merge_counters( [ date('Y-m-d') => 1 ], $counters ); $this->update_blog_option( $data['site_id'], 'patchstack_hits_last_30', $counters ); } $file = null; file_put_contents( $logs, '' . PHP_EOL ); // Update processing state. update_option( 'patchstack_firewall_log_ap_processing', false ); } }