HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux WebLive 5.15.0-79-generic #86-Ubuntu SMP Mon Jul 10 16:07:21 UTC 2023 x86_64
User: ubuntu (1000)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/html/wpwatermates_err/wp-content/plugins/defender-security/src/model/scan.php
<?php

namespace WP_Defender\Model;

use WP_Defender\Behavior\Scan_Item\Core_Integrity;
use WP_Defender\Behavior\Scan_Item\Plugin_Integrity;
use WP_Defender\Behavior\Scan_Item\Malware_Result;
use WP_Defender\Behavior\Scan_Item\Vuln_Result;
use WP_Defender\Component\Error_Code;
use WP_Defender\DB;
use WP_Defender\Traits\Formats;
use WP_Defender\Traits\IO;

class Scan extends DB {
	use IO, Formats;

	public const STATUS_INIT = 'init', STATUS_ERROR = 'error', STATUS_FINISH = 'finish';
	// Default state.
	public const STEP_GATHER_INFO = 'gather_info';
	// For 'File change detection'.
	public const STEP_CHECK_CORE = 'core_integrity_check', STEP_CHECK_PLUGIN = 'plugin_integrity_check';
	// For 'Known vulnerabilities' and 'Suspicious code'.
	public const STEP_VULN_CHECK = 'vuln_check', STEP_SUSPICIOUS_CHECK = 'suspicious_check';
	public const IGNORE_INDEXER = 'defender_scan_ignore_index';

	protected $table = 'defender_scan';

	/**
	 * Any valid relative Date and Time formats.
	 *
	 * @link https://www.php.net/manual/en/datetime.formats.relative.php
	 *
	 * @since 2.6.1
	 *
	 * @var string
	 */
	public const THRESHOLD_PERIOD = '3 hours ago';

	/**
	 * Constant to notate the scan is idle or crossed the threshold limit.
	 *
	 * @since 2.6.1
	 *
	 * @var string
	 */
	public const STATUS_IDLE = 'idle';

	/**
	 * @var int
	 * @defender_property
	 */
	public $id;
	/**
	 * Scan status, the native status is init, error, and finish, we can have other status base on the
	 * task the scan is running, like gather_fact, core_integrity etc.
	 *
	 * @var string
	 * @defender_property
	 */
	public $status;
	/**
	 * Mysql time.
	 * @var string
	 * @defender_property
	 */
	public $date_start;

	/**
	 * Store the current percent.
	 * @var int
	 * @defender_property
	 */
	public $percent = 0;

	/**
	 * Store how many tasks we process.
	 * @var int
	 * @defender_property
	 */
	public $total_tasks = 0;

	/**
	 * We will use this so internal task can store the current checkpoint.
	 *
	 * @var string
	 * @defender_property
	 */
	public $task_checkpoint = '';

	/**
	 * Mysql time.
	 * @var string
	 * @defender_property
	 */
	public $date_end;

	/**
	 * This only true when a scan trigger by report schedule.
	 * @var bool
	 * @defender_property
	 */
	public $is_automation = false;

	/**
	 * Return an array with various params, mostly this will be used.
	 *
	 * @param $per_page
	 * @param $paged
	 * @param $type
	 *
	 * @return array
	 */
	public function prepare_issues( $per_page = null, $paged = null, $type = null ): array {
		$ignored_models = $this->get_issues( $type, Scan_Item::STATUS_IGNORE, $per_page, $paged );
		$active_models = $this->get_issues( $type, Scan_Item::STATUS_ACTIVE, $per_page, $paged );

		$issues = [];
		$ignored = [];
		$count_total = count( $active_models );
		$count_issues_filtered = 0;

		$scan_item_group_total = wd_di()->get( Scan_Item::class )
			->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );

		$count_issues = ! empty( $scan_item_group_total['all'] ) ?
			$scan_item_group_total['all'] : 0;
		$count_core = ! empty( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
			$scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
		$count_plugin = ! empty( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
			$scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
		$count_malware = ! empty( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
			$scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
		$count_vuln = ! empty( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
			$scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;

		$scan_item_ignore_total = wd_di()->get( Scan_Item::class )
			->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );

		$count_ignored = ! empty( $scan_item_ignore_total['all'] ) ?
			$scan_item_ignore_total['all'] : 0;

		foreach ( $ignored_models as $model ) {
			$ignored[] = $model->to_array();
		}
		foreach ( $active_models as $active_model ) {
			$issues[] = $active_model->to_array();

			// We will now count all issues again by type filter for pagination usage.
			if ( null !== $type && 'all' !== $type ) {
				if ( $type === $active_model->type ) {
					$count_issues_filtered++;
				}
			} else {
				$count_issues_filtered++;
			}
		}

		return [
			'ignored' => $ignored,
			'issues' => $issues,
			'count_total' => $count_total,
			'count_issues' => $count_issues,
			'count_issues_filtered' => $count_issues_filtered,
			'count_ignored' => $count_ignored,
			'count_core' => $count_core,
			'count_plugin' => $count_plugin,
			'count_malware' => $count_malware,
			'count_vuln' => $count_vuln,
		];
	}

	/**
	 * @param null $type
	 * @param null $status
	 * @param null $per_page
	 * @param null $paged
	 *
	 * @return Scan_Item[]
	 */
	public function get_issues( $type = null, $status = null, $per_page = null, $paged = null ) {
		$orm = self::get_orm();
		$builder = $orm->get_repository( Scan_Item::class )
					->where( 'parent_id', $this->id );

		if (
			! is_null( $type )
			&& in_array(
				$type,
				[
					Scan_Item::TYPE_VULNERABILITY,
					Scan_Item::TYPE_INTEGRITY,
					Scan_Item::TYPE_PLUGIN_CHECK,
					Scan_Item::TYPE_SUSPICIOUS,
				],
				true
			)
		) {
			$builder->where( 'type', $type );
		}
		if (
			! is_null( $status )
			&& in_array( $status, [ Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ], true )
		) {
			$builder->where( 'status', $status );
		}
		if ( ! is_null( $per_page ) && ! is_null( $paged ) ) {
			$limit = ( ( $paged - 1 ) * $per_page ) . ',' . $per_page;
			$builder->limit( $limit );
		}
		$models = $builder->get();
		foreach ( $models as $key => $model ) {
			switch ( $model->type ) {
				case Scan_Item::TYPE_INTEGRITY:
					$model->attach_behavior( Core_Integrity::class, Core_Integrity::class );
					break;
				case Scan_Item::TYPE_PLUGIN_CHECK:
					$model->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
					break;
				case Scan_Item::TYPE_SUSPICIOUS:
					$model->attach_behavior( Malware_Result::class, Malware_Result::class );
					break;
				case Scan_Item::TYPE_VULNERABILITY:
				default:
					$model->attach_behavior( Vuln_Result::class, Vuln_Result::class );
					break;
			}
			$models[ $key ] = $model;
		}

		return $models;
	}

	public function count( $type = null, $status = null ) {
		$orm = self::get_orm();
		$builder = $orm->get_repository( Scan_Item::class )->where( 'parent_id', $this->id );

		if (
			! is_null( $type )
			&& in_array(
				$type,
				[
					Scan_Item::TYPE_VULNERABILITY,
					Scan_Item::TYPE_INTEGRITY,
					Scan_Item::TYPE_PLUGIN_CHECK,
					Scan_Item::TYPE_SUSPICIOUS,
				],
				true
			)
		) {
			$builder->where( 'type', $type );
		}
		if (
			! is_null( $status )
			&& in_array( $status, [ Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ], true )
		) {
			$builder->where( 'status', $status );
		}

		return $builder->count();
	}

	/**
	 * @param int $id
	 *
	 * @return bool|void
	 */
	public function unignore_issue( $id ) {
		$issue = $this->get_issue( $id );
		if ( ! is_object( $issue ) ) {
			return false;
		}
		$issue->status = Scan_Item::STATUS_ACTIVE;
		$issue->save();

		$ignore_lists = get_site_option( self::IGNORE_INDEXER, [] );
		$data = $issue->raw_data;
		if ( isset( $data['file'] ) ) {
			unset( $ignore_lists[ array_search( $data['file'], $ignore_lists, true ) ] );
		} elseif ( isset( $data['slug'] ) ) {
			unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists, true ) ] );
		}
		$this->update_ignore_list( $ignore_lists );
	}

	/**
	 * Check if a slug is ignored, we use a global indexer, so we can check while
	 * the active scan is running.
	 *
	 * @param string $slug
	 *
	 * @return bool
	 */
	public function is_issue_ignored( $slug ) {
		$ignore_lists = get_site_option( self::IGNORE_INDEXER, [] );

		return in_array( $slug, $ignore_lists, true );
	}

	/**
	 * @param int $id
	 *
	 * @return bool|void
	 */
	public function ignore_issue( $id ) {
		$issue = $this->get_issue( $id );
		if ( ! is_object( $issue ) ) {
			return false;
		}

		$issue->status = Scan_Item::STATUS_IGNORE;
		$issue->save();

		// Add this into global ignore index.
		$ignore_lists = get_site_option( self::IGNORE_INDEXER, [] );
		$data = $issue->raw_data;
		if ( isset( $data['file'] ) ) {
			$ignore_lists[] = $data['file'];
		} elseif ( isset( $data['slug'] ) ) {
			$ignore_lists[] = $data['slug'];
		}
		$this->update_ignore_list( $ignore_lists );
	}

	/**
	 * @param int $id
	 *
	 * @return Scan_Item|null
	 */
	public function get_issue( $id ) {
		$orm   = self::get_orm();
		$model = $orm->get_repository( Scan_Item::class )
			->where( 'id', $id )
			->first();

		if ( is_object( $model ) ) {
			switch ( $model->type ) {
				case Scan_Item::TYPE_INTEGRITY:
					$model->attach_behavior( Core_Integrity::class, Core_Integrity::class );
					break;
				case Scan_Item::TYPE_PLUGIN_CHECK:
					$model->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
					break;
				case Scan_Item::TYPE_SUSPICIOUS:
					$model->attach_behavior( Malware_Result::class, Malware_Result::class );
					break;
				case Scan_Item::TYPE_VULNERABILITY:
				default:
					$model->attach_behavior( Vuln_Result::class, Vuln_Result::class );
					break;
			}
		}

		return $model;
	}

	/**
	 * Remove an issue, this will happen when that issue is resolve, or the file link to this issue get deleted.
	 *
	 * @param int $id
	 */
	public function remove_issue( $id ) {
		$orm = self::get_orm();
		$orm->get_repository( Scan_Item::class )->delete( [ 'id' => $id ] );
	}

	/**
	 * This will build the data we use to output to frontend, base on the current scenario.
	 * @param null|int $per_page
	 * @param null|int $paged
	 * @param null|string $type
	 *
	 * @return array
	 */
	public function to_array( $per_page = null, $paged = null, $type = null ) {
		if ( ! in_array( $this->status, [ self::STATUS_ERROR, self::STATUS_FINISH, self::STATUS_IDLE ], true ) ) {

			return [
				'status' => $this->status,
				'status_text' => $this->get_status_text(),
				'percent' => $this->percent,
				// This only for hub, when a scan running.
				'count' => [ 'total' => 0 ],
			];
		} elseif ( in_array( $this->status, [ self::STATUS_FINISH, self::STATUS_IDLE ], true ) ) {
			$total_filtered = (int) $this->count( $type );
			$count_issues_filtered = (int) $this->count( $type, Scan_Item::STATUS_ACTIVE );
			$total_count = (int) $this->count( null, Scan_Item::STATUS_ACTIVE );

			$scan_item_ignore_total = wd_di()->get( Scan_Item::class )
				->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );

			$count_ignored = ! empty( $scan_item_ignore_total['all'] ) ?
				$scan_item_ignore_total['all'] : 0;

			$total_issue_pages = 1;
			$total_ignored_pages = 1;
			if( ! is_null( $per_page ) && ( $total_count > $per_page ) ) {
				$data = $this->prepare_issues( $per_page, $paged, $type );
				if (  ! is_null( $paged ) ) {
					$total_issue_pages = ceil( $count_issues_filtered / $per_page );
					$total_ignored_pages = ceil( $count_ignored / $per_page );
				}
			} else {
				$data = $this->prepare_issues( null, null, $type );
			}

			$scan_item_group_total = wd_di()->get( Scan_Item::class )
				->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );

			$count_issues = ! empty( $scan_item_group_total['all'] ) ?
				$scan_item_group_total['all'] : 0;
			$count_core = ! empty( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
				$scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
			$count_plugin = ! empty( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
				$scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
			$count_malware = ! empty( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
				$scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
			$count_vuln = ! empty( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
				$scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;

			return [
				'status' => $this->status,
				'issues_items' => $data['issues'],
				'ignored_items' => $data['ignored'],
				'last_scan' => $this->format_date_time( $this->date_start ),
				'count' => [
					'total' => is_array($data['issues']) || $data['issues'] instanceof \Countable ? count( $data['issues'] ) : 0,
					'total_filtered' => $total_filtered,
					'issues_total' => $count_issues,
					'issues_total_filtered' => $count_issues_filtered,
					'ignored_total' => $count_ignored,
					'core' => $count_core + $count_plugin,
					'content' => $count_malware,
					'vuln' => $count_vuln,
				],
				'paging' => [
					'issue' => [
						'paged' => $paged,
						'total_pages' => $total_issue_pages,
					],
					'ignored'  => [
						'paged' => $paged,
						'total_pages' => $total_ignored_pages,
					],
					'per_page' => $per_page,
				],
			];
		}
	}

	/**
	 * @param bool $from_report
	 *
	 * @return Scan|\WP_Error
	 */
	public static function create( $from_report = false ) {
		$orm = self::get_orm();
		$active = self::get_active();
		if ( is_object( $active ) ) {
			return new \WP_Error( Error_Code::INVALID, __( 'A scan is already in progress.', 'defender-security' ) );
		}
		$model = new Scan();
		$model->status = self::STATUS_INIT;
		$model->date_start = gmdate( 'Y-m-d H:i:s' );
		$model->date_end = gmdate( 'Y-m-d H:i:s' );
		$model->is_automation = $from_report;

		$orm->save( $model );

		return $model;
	}

	/**
	 * Delete current scan.
	 *
	 * @param int|null $id Table primary key id.
	 */
	public function delete( $id = null ) {
		if ( ! $this->is_positive_int( $id ) ) {
			$id = $this->id;
		}

		// Delete all the related result items.
		$orm = self::get_orm();

		$orm->get_repository( Scan_Item::class )->delete(
			[ 'parent_id' => $id ]
		);

		$orm->get_repository( self::class )->delete(
			[ 'id' => $id ]
		);
	}

	/**
	 * Get the current active scan if any.
	 *
	 * @return self|null
	 */
	public static function get_active() {
		$orm = self::get_orm();

		return $orm->get_repository( self::class )
			->where( 'status', 'NOT IN', [ self::STATUS_FINISH, self::STATUS_ERROR, self::STATUS_IDLE ] )
			->first();
	}

	/**
	 * Get last result.
	 *
	 * @return self|null
	 */
	public static function get_last() {
		$orm = self::get_orm();

		return $orm->get_repository( self::class )
			->where( 'status', 'IN', [ self::STATUS_FINISH, self::STATUS_IDLE ] )
			->order_by( 'id', 'desc' )
			->first();
	}

	/**
	 * @return array
	 */
	public static function get_last_all() {
		$orm = self::get_orm();

		return $orm->get_repository( self::class )
			->where( 'status', 'IN', [ self::STATUS_FINISH, self::STATUS_IDLE ] )
			->order_by( 'id', 'desc' )
			->get();
	}

	/**
	 * If the scan find any, we will use this to add the issue.
	 *
	 * @param $type
	 * @param $data
	 * @param $status
	 */
	public function add_item( $type, $data, $status = Scan_Item::STATUS_ACTIVE ) {
		$model = new Scan_Item();
		$model->type = $type;
		$model->parent_id = $this->id;
		$model->raw_data = $data;
		$model->status = $status;
		$ret = $model->save();

		return $ret;
	}

	/**
	 * Return current status as readable string.
	 *
	 * @return string
	 */
	public function get_status_text() {
		switch ( $this->status ) {
			case self::STATUS_INIT:
				return __( 'Initializing...', 'defender-security' );
			case self::STEP_GATHER_INFO:
				return __( 'Gathering information...', 'defender-security' );
			case self::STEP_CHECK_CORE:
				return __( 'Analyzing WordPress Core...', 'defender-security' );
			case self::STEP_CHECK_PLUGIN:
				return __( 'Analyzing WordPress Plugins...', 'defender-security' );
			case self::STEP_VULN_CHECK:
				return __( 'Checking for any published vulnerabilities in your plugins & themes...', 'defender-security' );
			case self::STEP_SUSPICIOUS_CHECK:
				return __( 'Analyzing WordPress Content...', 'defender-security' );
			default:
				return __( 'The scan is running', 'defender-security' );
		}
	}

	/**
	 * Calculation scan percentage base on the tasks percent.
	 *
	 * @param $task_percent
	 * @param $pos
	 *
	 * @return float
	 */
	public function calculate_percent( $task_percent, $pos = 1 ) {
		$task_max = 100 / $this->total_tasks;
		$task_base = $task_max * ( $pos - 1 );
		$micro = $task_percent * $task_max / 100;
		$this->percent = round( $task_base + $micro, 2 );
		if ( $this->percent > 100 ) {
			$this->percent = 100;
		}

		return $this->percent;
	}

	/**
	 * Get list of whitelisted files.
	 *
	 * @return array
	 */
	private function whitelisted_files() {
		return [
			// Configuration files.
			'user.ini',
			'php.ini',
			'robots.txt',
			'.htaccess',
			'nginx.conf',
			// Hidden system files and directories.
			'.well_known',
			'.idea',
			'.DS_Store',
			'.svn',
			'.git',
			'.quarantine',
			'.tmb',
			'.vscode',
		];
	}

	/**
	 * Check if a slug is whitelisted.
	 *
	 * @param string $slug path to file
	 *
	 * @return bool
	 */
	public function is_issue_whitelisted( $slug ) {
		$whitelisted_files = $this->whitelisted_files();
		foreach ( $whitelisted_files as $file ) {
			if ( false !== stristr( $slug, $file ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array $ignore_lists
	 */
	public function update_ignore_list( $ignore_lists ) {
		$ignore_lists = array_unique( $ignore_lists );
		$ignore_lists = array_filter( $ignore_lists );
		update_site_option( self::IGNORE_INDEXER, $ignore_lists );
	}

	/**
	 * Get the threshold time limit as DateTime object.
	 *
	 * @return \DateTime Threshold time limit as DateTime object.
	 */
	public function threshold_date_time_object() {
		$timezone = new \DateTimeZone( 'UTC' );

		/**
		 * Filter to override scan threshold period.
		 *
		 * @since 2.6.1
		 *
		 * @link https://www.php.net/manual/en/datetime.formats.relative.php
		 *
		 * @param string $threshold Any valid relative Date and Time formats.
		 */
		$threshold = apply_filters( 'wd_scan_threshold', self::THRESHOLD_PERIOD );

		return new \DateTime( $threshold, $timezone );
	}

	/**
	 * Threshold time limit in mysql string format.
	 *
	 * @return string Threshold time limit as mysql string format.
	 */
	public function threshold_date_time_mysql() {
		$type = 'Y-m-d H:i:s';
		$threshold_date_time_object = $this->threshold_date_time_object();
		$mysql_format = $threshold_date_time_object->format( $type );

		return $mysql_format;
	}

	/**
	 * Get the idle scan if any.
	 *
	 * @return self|null
	 */
	public function get_idle() {
		$orm = self::get_orm();

		$mysql_date = $this->threshold_date_time_mysql();

		return $orm->get_repository( self::class )
			->where( 'status', 'NOT IN', [ self::STATUS_FINISH, self::STATUS_ERROR ] )
			->where( 'date_start', '<', $mysql_date )
			->first();
	}

	/**
	 * Delete all idle scan and scan items
	 *
	 * @since 2.6.1
	 */
	public function delete_idle() {
		$idle_scans = self::get_orm()
			->get_repository( self::class )
			->where( 'status', self::STATUS_IDLE )
			->get();

		foreach ( $idle_scans as $idle_scan ) {
			$this->delete( $idle_scan->id );
		}
	}

	/**
	 * Verify positive integer or not.
	 *
	 * @param mixed $id Argument to check for a positive number.
	 *
	 * @return bool Return true on positive integer else false.
	 * @since 2.6.1
	 */
	private function is_positive_int( $id ) {
		return is_int( $id ) && $id > 0;
	}
}