382 lines
15 KiB
PHP
382 lines
15 KiB
PHP
<?php
|
|
if (!defined('ABSPATH')) {
|
|
exit;//Exit if accessed directly
|
|
}
|
|
|
|
class AIOWPSecurity_Scan {
|
|
|
|
public function __construct() {
|
|
add_action('aiowps_perform_fcd_scan_tasks', array($this, 'aiowps_scheduled_fcd_scan_handler'));
|
|
}
|
|
|
|
/**
|
|
* This function will recursively scan through all directories starting from the specified location
|
|
* It will store the path/filename, last_modified and filesize values in a multi-dimensional associative array
|
|
*/
|
|
|
|
/**
|
|
* Will recursively scan through all directories starting from ABSPATH.
|
|
* Will return array with the path/filename, last_modified and filesize values
|
|
*
|
|
* @global AIO_WP_Security $aio_wp_security
|
|
* @return boolean|array
|
|
*/
|
|
public function execute_file_change_detection_scan() {
|
|
global $aio_wp_security;
|
|
$scan_result = array();
|
|
$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);
|
|
}
|
|
|
|
$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;
|
|
}
|
|
}
|