File: /var/www/html/wptoho/wp-content/plugins/defender-security/src/behavior/scan/class-malware-scan.php
<?php
/**
* Handles malware scan.
*
* @package WP_Defender\Behavior\Scan
*/
namespace WP_Defender\Behavior\Scan;
use ArrayIterator;
use WP_Defender\Traits\IO;
use Calotes\Base\Component;
use WP_Defender\Traits\Plugin;
use WP_Defender\Component\Timer;
use WP_Defender\Model\Scan_Item;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Model\Scan as Model_Scan;
use WP_Defender\Model\Setting\Scan as Scan_Settings;
use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
use WP_Defender\Controller\Scan as Scan_Controller;
/**
* It is responsible for performing suspicious checks on files using a quick scan and a deep scan.
*/
class Malware_Scan extends Component {
use IO;
use Plugin;
public const YARA_RULES = 'defender_yara_rules';
public const MALWARE_LOG = 'malware_scan.log';
/**
* Backup memory.
*
* @var string
*/
private $memory;
/**
* Holds the WPMUDEV object or null if not set.
*
* @var WPMUDEV
*/
private $wpmudev;
/**
* Holds the Model_Scan model or null if not set.
*
* @var Model_Scan
*/
private $scan;
/**
* Constructor for the Malware_Scan class.
*
* @param WPMUDEV $wpmudev The WPMUDEV object.
* @param Model_Scan $scan The Model_Scan model.
*/
public function __construct( WPMUDEV $wpmudev, Model_Scan $scan ) {
$this->wpmudev = $wpmudev;
$this->scan = $scan;
}
/**
* Retrieves additional rules based on the provided scan settings.
*
* @param Scan_Settings $scan_settings The scan settings object.
*
* @return array Returns an array containing plugin-related rules.
*/
protected function get_additional_rules( Scan_Settings $scan_settings ): array {
$plugin_cache = false;
$plugin_slugs_changes = array();
$plugin_pro_slugs = array();
$plugin_all_slugs = array();
// Checked Plugin option.
if ( $scan_settings->integrity_check && $scan_settings->check_plugins ) {
$plugin_cache = true;
$arr = get_site_option( Plugin_Integrity::PLUGIN_SLUGS, false );
if ( is_array( $arr ) && ! empty( $arr ) ) {
$plugin_slugs_changes = $arr;
}
$plugin_slugs = get_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS, false );
if ( is_array( $plugin_slugs ) && ! empty( $plugin_slugs ) ) {
$plugin_pro_slugs = $plugin_slugs;
}
$plugin_all_slugs = $this->get_plugin_slugs();
}
return array(
'plugin_change' => $plugin_cache,
// List of plugins with modifications.
'plugin_slugs_changes' => $plugin_slugs_changes,
// List of pro plugins.
'plugin_pro_slugs' => $plugin_pro_slugs,
'plugin_all_slugs' => $plugin_all_slugs,
);
}
/**
* Check if a file has been modified based on the given rules.
*
* @param string $file_path The path of the file to check.
* @param array $rules The rules to determine if a file has been modified.
*
* @return bool Returns true if the file has been modified, false otherwise.
*/
protected function was_modificated_file( $file_path, $rules ): bool {
// Unchecked 'Plugin change file' option, so green light to display Suspicious checks.
if ( ! $rules['plugin_change'] ) {
return true;
}
$search_on_plugin = WP_PLUGIN_DIR . '/';
if ( false !== stripos( $file_path, $search_on_plugin ) ) {
/**
* Suspicious code in /plugins.
* Empty list of plugin slugs because there are premium plugins,
* not modifications on Free plugins,
* or not plugins on site.
* Should check separate custom files/dirs in the root too.
*/
$rev_file = str_replace( $search_on_plugin, '', $file_path );
$matches = explode( '/', $rev_file );
$base_slug = array_shift( $matches );
// Custom files/dirs.
if ( ! in_array( $base_slug, $rules['plugin_all_slugs'], true ) ) {
return true;
}
if ( empty( $rules['plugin_slugs_changes'] ) && empty( $rules['plugin_pro_slugs'] ) ) {
// No modifications.
return false;
}
// Is it on premium plugins?
if ( in_array( $base_slug, $rules['plugin_pro_slugs'], true ) ) {
return true;
}
if ( in_array( $base_slug, (array) $rules['plugin_slugs_changes'], true ) ) {
// Modifications in this plugin.
return true;
}
// Modifications are not here.
return false;
}
// Other WP places.
return true;
}
/**
* Perform a suspicious check on files using a quick scan and a deep scan.
*
* @param Malware_Quick_Scan $quick_scan The quick scan object.
* @param Malware_Deep_Scan $deep_scan The deep scan object.
*
* @return bool Returns true if the check is successful, false otherwise.
*/
public function suspicious_check( Malware_Quick_Scan $quick_scan, Malware_Deep_Scan $deep_scan ): bool {
$files = get_site_option( Gather_Fact::CACHE_CONTENT, array() );
if ( empty( $files ) ) {
return true;
}
set_time_limit( 0 );
$this->prepare_emergency_shutdown();
$timer = new Timer();
$rules = $this->fetch_yara_rules();
$model = $this->scan;
$pos = (int) $model->task_checkpoint;
$combinations = $this->get_additional_rules( new Scan_Settings() );
$files = new ArrayIterator( $files );
$files->seek( $pos );
while ( $files->valid() ) {
if ( ! $timer->check() ) {
$reason = 'Rage quit';
/**
* Retrieves the Scan_Analytics class.
*
* @var Scan_Analytics $scan_analytics
*/
$scan_analytics = wd_di()->get( Scan_Analytics::class );
$scan_analytics->track_feature(
$scan_analytics::EVENT_SCAN_FAILED,
array(
$scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
'Error_Reason' => $reason,
)
);
$this->log( $reason, self::MALWARE_LOG );
$model->save();
break;
}
if ( $model->is_issue_ignored( $files->current() ) ) {
$this->log( sprintf( 'skip %s because of file is ignored', $files->current() ), self::MALWARE_LOG );
$files->next();
continue;
}
[ $result, $qs_detail ] = $quick_scan->do_quick_scan( $files->current(), $rules );
if ( $result ) {
$this->log( sprintf( 'file %s suspicious', $files->current() ), self::MALWARE_LOG );
$result = $deep_scan->do_deep_scan( $files->current(), $qs_detail );
/**
* Add new item if Suspicious code is found and:
* plugins are premium,
* plugins are on wp.org but the code doesn't match from the WP repo (there are differences in checksums),
* deactivated options of File change detection > Scan plugin file changes.
*/
if ( is_array( $result ) && $this->was_modificated_file( $files->current(), $combinations ) ) {
$result['file'] = $files->current();
$model->add_item( Scan_Item::TYPE_SUSPICIOUS, $result );
}
}
$files->next();
$files_key = $files->key();
$model->task_checkpoint = ! is_null( $files_key ) ? $files_key : '';
$model->calculate_percent( $files_key * 100 / $files->count(), 6 );
if ( 0 === $files_key % 100 ) {
// We should update the model percent each 100 files so we have some progress on the screen.
$model->save();
}
}
if ( ! $files->valid() ) {
$last = Model_Scan::get_last();
if ( is_object( $last ) ) {
$ignored_issues = $last->get_issues( Scan_Item::TYPE_SUSPICIOUS, Scan_Item::STATUS_IGNORE );
foreach ( $ignored_issues as $issue ) {
$this->scan->add_item(
Scan_Item::TYPE_SUSPICIOUS,
$issue->raw_data,
Scan_Item::STATUS_IGNORE
);
}
}
$model->task_checkpoint = '';
}
$model->save();
return ! $files->valid();
}
/**
* We will use this for a safe switch when memory out happen.
*/
public function prepare_emergency_shutdown() {
$this->memory = str_repeat( '*', 1024 * 1024 );
register_shutdown_function(
function () {
if ( Model_Scan::STATUS_FINISH === $this->scan->status ) {
return;
}
$this->memory = null;
$err = error_get_last();
if (
( ! is_null( $err ) )
&& ( ! in_array( $err['type'], array( E_NOTICE, E_WARNING, E_DEPRECATED ), true ) )
) {
$this->log( $err, Scan_Controller::SCAN_LOG );
$this->log( 'Something wrong happen, saving and quit.', Scan_Controller::SCAN_LOG );
$this->scan->status = Model_Scan::STATUS_ERROR;
++$this->scan->task_checkpoint;
$this->scan->save();
}
}
);
}
/**
* Fetch yara rules from API.
*
* @return array
*/
private function fetch_yara_rules(): array {
$rules = get_site_option( self::YARA_RULES, false );
if ( is_array( $rules ) ) {
return $rules;
}
$rules = $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_SIGNATURE );
if ( is_array( $rules ) ) {
update_site_option( self::YARA_RULES, $rules );
return $rules;
}
return array();
}
}