configs->get_value('aiowps_fcd_filename'); if (empty($fcd_filename)) { // means that we haven't done a scan before, or, // the fcd file containing the results doesn't exist $random_suffix = AIOWPSecurity_Utility::generate_alpha_numeric_random_string(10); $fcd_filename = 'aiowps_fcd_data_' . $random_suffix; $aio_wp_security->configs->set_value('aiowps_fcd_filename', $fcd_filename, true); } $fcd_data = self::get_fcd_data(); // get previous scan data if any if (false === $fcd_data) { // an error occurred so return return false; } $scanned_data = $this->do_file_change_scan(); if (empty($fcd_data)) { $this->save_fcd_data($scanned_data); $scan_result['initial_scan'] = '1'; return $scan_result; } else { $scan_result = $this->compare_scan_data($fcd_data['file_scan_data'], $scanned_data); $scan_result['initial_scan'] = ''; $this->save_fcd_data($scanned_data, $scan_result); if (!empty($scan_result['files_added']) || !empty($scan_result['files_removed']) || !empty($scan_result['files_changed'])) { //This means there was a change detected $aio_wp_security->configs->set_value('aiowps_fcds_change_detected', true, true); $aio_wp_security->debug_logger->log_debug(__METHOD__ . " - change to filesystem detected!"); $this->aiowps_send_file_change_alert_email($scan_result); //Send file change scan results via email if applicable } else { //Reset the change flag $aio_wp_security->configs->set_value('aiowps_fcds_change_detected', false, true); } return $scan_result; } } /** * Send email with notification about file changes detected by last scan. * * @global AIO_WP_Security $aio_wp_security * @param array $scan_result Array with scan result returned by compare_scan_data() method. */ public function aiowps_send_file_change_alert_email($scan_result) { global $aio_wp_security; if ('1' == $aio_wp_security->configs->get_value('aiowps_send_fcd_scan_email')) { $subject = __('All In One WP Security - File change detected', 'all-in-one-wp-security-and-firewall') . ' ' . AIOWPSecurity_Utility::convert_timestamp(null, 'l, F jS, Y \a\\t g:i a'); //$attachment = array(); $message = __('A file change was detected on your system for site URL', 'all-in-one-wp-security-and-firewall') . ' ' . network_site_url() . __('. Scan was generated on', 'all-in-one-wp-security-and-firewall') . ' ' . AIOWPSecurity_Utility::convert_timestamp(null, 'l, F jS, Y \a\\t g:i a'); $message .= "\r\n\r\n".__('A summary of the scan results is shown below:', 'all-in-one-wp-security-and-firewall'); $message .= "\r\n\r\n"; $message .= self::get_file_change_summary($scan_result); $message .= "\r\n".__('Login to your site to view the scan details.', 'all-in-one-wp-security-and-firewall'); // Get the email address(es). $addresses = AIOWPSecurity_Utility::get_array_from_textarea_val($aio_wp_security->configs->get_value('aiowps_fcd_scan_email_address')); // If no explicit email address(es) are given, send email to site admin. $to = empty($addresses) ? array(get_site_option('admin_email')) : $addresses; if (!wp_mail($to, $subject, $message)) { $aio_wp_security->debug_logger->log_debug(__METHOD__ . " - File change notification email failed to send.", 4); } } } /** * This function is called via the following filter 'aiowps_perform_fcd_scan_tasks' and will start the file scan * * @return void */ public function aiowps_scheduled_fcd_scan_handler() { global $aio_wp_security; if ('1' != $aio_wp_security->configs->get_value('aiowps_enable_automated_fcd_scan')) return; $aio_wp_security->debug_logger->log_debug_cron(__METHOD__ . " - Scheduled fcd_scan is enabled. Checking now to see if scan needs to be done..."); $current_time = time(); $next_fcd_scan_time = self::get_next_scheduled_scan(); if ($next_fcd_scan_time <= $current_time) { // It's time to do a filescan $result = $this->execute_file_change_detection_scan(); if (false === $result) { $aio_wp_security->debug_logger->log_debug(__METHOD__ . " - Scheduled filescan operation failed.", 4); } else { $aio_wp_security->configs->set_value('aiowps_last_fcd_scan_time', $current_time, true); } } } /** * This function will get the next scheduled scan timestamp and return it * * @return int|bool - the next scheduled scan timestamp, or false if the scheduled scan is not setup */ public static function get_next_scheduled_scan() { global $aio_wp_security; if ('1' != $aio_wp_security->configs->get_value('aiowps_enable_automated_fcd_scan')) return false; $fcd_scan_frequency = $aio_wp_security->configs->get_value('aiowps_fcd_scan_frequency'); // Number of hours or days or months interval $interval_setting = $aio_wp_security->configs->get_value('aiowps_fcd_scan_interval'); // Hours/Days/Months switch ($interval_setting) { case '0': $interval = 'hours'; break; case '1': $interval = 'days'; break; case '2': $interval = 'weeks'; break; } $last_fcd_scan_time = $aio_wp_security->configs->get_value('aiowps_last_fcd_scan_time'); if (null == $last_fcd_scan_time) { // Set the last scan time to now so it can trigger for the next scheduled period $last_fcd_scan_time = time(); $aio_wp_security->configs->set_value('aiowps_last_fcd_scan_time', $last_fcd_scan_time, true); } elseif (is_string($last_fcd_scan_time)) { $last_fcd_scan_time = strtotime($last_fcd_scan_time); $aio_wp_security->configs->set_value('aiowps_last_fcd_scan_time', $last_fcd_scan_time, true); } $next_fcd_scan_time = strtotime("+".abs($fcd_scan_frequency).$interval, $last_fcd_scan_time); return $next_fcd_scan_time; } /** * Get the last filechange detection data which is stored in the special file. * * @global AIO_WP_Security $aio_wp_security * @return bool|array - false on failure, array on success */ public static function get_fcd_data() { // phpcs:disable WordPress.WP.AlternativeFunctions -- Silence wp_filesystem method suggestion. We cannot use that method. global $aio_wp_security; $aiowps_backup_dir = WP_CONTENT_DIR.'/'.AIO_WP_SECURITY_BACKUPS_DIR_NAME; $fcd_filename = $aio_wp_security->configs->get_value('aiowps_fcd_filename'); if (empty($fcd_filename)) { // means that we haven't done a scan before, or, // the fcd file containing the results doesn't exist $random_suffix = AIOWPSecurity_Utility::generate_alpha_numeric_random_string(10); $fcd_filename = 'aiowps_fcd_data_' . $random_suffix; $aio_wp_security->configs->set_value('aiowps_fcd_filename', $fcd_filename, true); } $results_file = $aiowps_backup_dir. '/'. $fcd_filename; if (!is_file($results_file)) { if (is_dir($results_file)) { $new_dir_name = $results_file . '_backup'; rename($results_file, $new_dir_name); //Rename the folder to create backup of the folder. This condition should not really happen, but if it does (user sets some non-sensible value), then it's better to not nuke the existing folder } $fp = @fopen($results_file, 'w'); //open for write - will create file if doesn't exist return array(); } if (empty(filesize($results_file))) { return array(); // if newly created file return empty array } $fp = @fopen($results_file, 'r'); //open for read and write - will create file if doesn't exist if (false === $fp) { // Error $aio_wp_security->debug_logger->log_debug(__METHOD__ . " - fopen returned false when opening fcd data file"); return false; } $contents = fread($fp, filesize($results_file)); fclose($fp); if (false === $contents) { // Error $aio_wp_security->debug_logger->log_debug(__METHOD__ . " - fread returned false when reading fcd data file"); return false; } else { $fcd_file_contents = json_decode($contents, true); if (isset($fcd_file_contents['file_scan_data'])) { return $fcd_file_contents; } else { return array(); } } // phpcs:enable WordPress.WP.AlternativeFunctions -- Silence wp_filesystem method suggestion. We cannot use that method. } /** * Recursively scan the entire $start_dir directory and return file size * and last modified date of every regular file. Ignore files and file * types specified in file scanner settings. * * @global AIO_WP_Security $aio_wp_security * @param string $start_dir * @return array */ public function do_file_change_scan($start_dir = ABSPATH) { global $aio_wp_security; $filescan_data = array(); // Iterator key is absolute file path, iterator value is SplFileInfo object, // iteration skips '..' and '.' records, because we're not interested in directories. $dit = new RecursiveDirectoryIterator($start_dir, FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS); $rit = new RecursiveIteratorIterator($dit, RecursiveIteratorIterator::SELF_FIRST, RecursiveIteratorIterator::CATCH_GET_CHILD); // Grab files/directories to skip $files_to_skip = AIOWPSecurity_Utility::explode_trim_filter_empty($aio_wp_security->configs->get_value('aiowps_fcd_exclude_files')); // Grab (lowercased) file types to skip $file_types_to_skip = AIOWPSecurity_Utility::explode_trim_filter_empty(strtolower($aio_wp_security->configs->get_value('aiowps_fcd_exclude_filetypes'))); $start_dir_length = strlen($start_dir); foreach ($rit as $filename => $fileinfo) { if (!file_exists($filename) || is_dir($filename)) { continue; // if file doesn't exist or is a directory move on to next iteration } if ($fileinfo->getFilename() == 'wp-security-log-cron-job.txt' || $fileinfo->getFilename() == 'wp-security-log.txt') { continue; // Skip AIOS log files } // Let's omit any file types from the scan which were specified in the settings if necessary if (!empty($file_types_to_skip)) { //$current_file_ext = strtolower($fileinfo->getExtension()); //getExtension() only available on PHP 5.3.6 or higher $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if (in_array($ext, $file_types_to_skip)) { continue; } } // Let's omit specific files or directories from the scan which were specified in the settings if (!empty($files_to_skip)) { $skip_this = false; foreach ($files_to_skip as $f_or_dir) { // Expect files/dirs to be specified relatively to $start_dir, // so start searching at $start_dir_length offset. if (strpos($filename, $f_or_dir, $start_dir_length) !== false) { $skip_this = true; break; // ! } } if ($skip_this) { continue; } } $filescan_data[$filename] = array( 'last_modified' => $fileinfo->getMTime(), 'filesize' => $fileinfo->getSize(), ); } return $filescan_data; } public function compare_scan_data($last_scan_data, $new_scanned_data) { // Identify new files added: get all files which are in the new scan but not present in the old scan $files_added = @array_diff_key($new_scanned_data, $last_scan_data); // Identify files deleted: get all files which are in the old scan but not present in the new scan $files_removed = @array_diff_key($last_scan_data, $new_scanned_data); // Identify existing files: get all files which are in new scan, but were not added $files_kept = @array_diff_key($new_scanned_data, $files_added); $files_changed = array(); // Loop through existing files and determine, if they have been changed foreach ($files_kept as $filename => $new_scan_meta) { $last_scan_meta = $last_scan_data[$filename]; // Check filesize and last_modified values if (($new_scan_meta['last_modified'] !== $last_scan_meta['last_modified']) || ($new_scan_meta['filesize'] !== $last_scan_meta['filesize'])) { $files_changed[$filename] = $new_scan_meta; } } // Create single array of all changes return array( 'files_added' => $files_added, 'files_removed' => $files_removed, 'files_changed' => $files_changed, ); } public static function get_file_change_summary($scan_result) { $scan_summary = ""; if (!empty($scan_result['files_added'])) { //Output of files added $scan_summary .= "\r\n".__('The following files were added to your host', 'all-in-one-wp-security-and-firewall').":\r\n"; foreach ($scan_result['files_added'] as $key => $value) { $scan_summary .= "\r\n".$key.' ('.__('modified on:', 'all-in-one-wp-security-and-firewall'). ' ' . AIOWPSecurity_Utility::convert_timestamp($value['last_modified']).')'; } $scan_summary .= "\r\n======================================\r\n"; } if (!empty($scan_result['files_removed'])) { //Output of files removed $scan_summary .= "\r\n".__('The following files were removed from your host', 'all-in-one-wp-security-and-firewall').":\r\n"; foreach ($scan_result['files_removed'] as $key => $value) { $scan_summary .= "\r\n".$key.' ('.__('modified on:', 'all-in-one-wp-security-and-firewall'). ' ' . AIOWPSecurity_Utility::convert_timestamp($value['last_modified']).')'; } $scan_summary .= "\r\n======================================\r\n"; } if (!empty($scan_result['files_changed'])) { //Output of files changed $scan_summary .= "\r\n".__('The following files were changed on your host', 'all-in-one-wp-security-and-firewall').":\r\n"; foreach ($scan_result['files_changed'] as $key => $value) { $scan_summary .= "\r\n".$key.' ('.__('modified on:', 'all-in-one-wp-security-and-firewall'). ' ' . AIOWPSecurity_Utility::convert_timestamp($value['last_modified']).')'; } $scan_summary .= "\r\n======================================\r\n"; } return $scan_summary; } /** * Saves file change detection data into a special file * * @global AIO_WP_Security $aio_wp_security * @param type $scanned_data * @param type $scan_result * @return boolean */ public function save_fcd_data($scanned_data, $scan_result = array()) { global $aio_wp_security; $date_time = current_time('mysql'); $data = array('date_time' => $date_time, 'file_scan_data' => $scanned_data, 'last_scan_result' => $scan_result); $fcd_filename = $aio_wp_security->configs->get_value('aiowps_fcd_filename'); $aiowps_backup_dir = WP_CONTENT_DIR.'/'.AIO_WP_SECURITY_BACKUPS_DIR_NAME; if (!AIOWPSecurity_Utility_File::create_dir($aiowps_backup_dir)) { $aio_wp_security->debug_logger->log_debug(__METHOD__ . " - Creation of DB backup directory failed!", 4); return false; } // phpcs:disable WordPress.WP.AlternativeFunctions -- Silence wp_filesystem method suggestion. We cannot use that method. $results_file = $aiowps_backup_dir. '/'. $fcd_filename; $fp = fopen($results_file, 'w'); fwrite($fp, json_encode($data)); fclose($fp); // phpcs:enable WordPress.WP.AlternativeFunctions -- Silence wp_filesystem method suggestion. We cannot use that method. return true; } }