PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.0
Defender Security – Malware Scanner, Login Security & Firewall v5.6.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / model / class-scan.php

class-scan.php in Defender Security – Malware Scanner, Login Security & Firewall 5.6.0, at src/model/class-scan.php

892 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles interaction with the database for scans.
4 *
5 * @package WP_Defender\Model
6 */
7
8 namespace WP_Defender\Model;
9
10 use WP_Error;
11 use DateTime;
12 use Countable;
13 use DateTimeZone;
14 use WP_Defender\DB;
15 use WP_Defender\Traits\IO;
16 use WP_Defender\Traits\Formats;
17 use WP_Defender\Component\Error_Code;
18 use WP_Defender\Behavior\Scan_Item\Core_Integrity;
19 use WP_Defender\Behavior\Scan_Item\Plugin_Integrity;
20 use WP_Defender\Behavior\Scan_Item\Abandoned_Result;
21
22 /**
23 * Model for scan table.
24 */
25 class Scan extends DB {
26
27 use IO;
28 use Formats;
29
30 public const STATUS_INIT = 'init', STATUS_ERROR = 'error', STATUS_FINISH = 'finish';
31 // Default state.
32 public const STEP_GATHER_INFO = 'gather_info', STEP_ABANDONED_PLUGIN_CHECK = 'abandoned_plugin_check';
33 public const STEP_CHECK_CORE = 'core_integrity_check', STEP_CHECK_PLUGIN = 'plugin_integrity_check';
34 public const STEP_VULN_CHECK = 'vuln_check', STEP_SUSPICIOUS_CHECK = 'suspicious_check';
35 public const IGNORE_INDEXER = 'defender_scan_ignore_index';
36
37 /**
38 * Table name.
39 *
40 * @var string
41 */
42 protected $table = 'defender_scan';
43
44 /**
45 * Any valid relative Date and Time formats.
46 *
47 * @link https://www.php.net/manual/en/datetime.formats.relative.php
48 * @since 2.6.1
49 * @var string
50 */
51 public const THRESHOLD_PERIOD = '3 hours ago';
52
53 /**
54 * Constant to notate the scan is idle or crossed the threshold limit.
55 *
56 * @since 2.6.1
57 * @var string
58 */
59 public const STATUS_IDLE = 'idle';
60
61 /**
62 * Primary key column.
63 *
64 * @var int
65 * @defender_property
66 */
67 public $id;
68 /**
69 * Table column for the status.
70 * Possible values are,
71 * - init
72 * - error
73 * - finish
74 * - gather_fact
75 * - core_integrity_check
76 * - plugin_integrity_check
77 * - vuln_check
78 * - suspicious_check
79 * - idle
80 * - abandoned_plugin_check
81 *
82 * @var string
83 * @defender_property
84 */
85 public $status;
86 /**
87 * Table column for the start time.
88 *
89 * @var string
90 * @defender_property
91 */
92 public $date_start;
93
94 /**
95 * Table column for the current percentage.
96 *
97 * @var int
98 * @defender_property
99 */
100 public $percent = 0;
101
102 /**
103 * Table column for the total tasks.
104 * Store how many tasks we process.
105 *
106 * @var int
107 * @defender_property
108 */
109 public $total_tasks = 0;
110
111 /**
112 * Table column for the current task checkpoint.
113 *
114 * @var string
115 * @defender_property
116 */
117 public $task_checkpoint = '';
118
119 /**
120 * Table column for the end time.
121 *
122 * @var string
123 * @defender_property
124 */
125 public $date_end;
126
127 /**
128 * Table column for the scan trigger by report schedule.
129 *
130 * @var bool
131 * @defender_property
132 */
133 public $is_automation = false;
134
135 /**
136 * Prepare and fetch issues with various counts.
137 * This method retrieves active and ignored issues based on the specified type, page, and items per page.
138 * It prepares a detailed summary including total counts and filtered counts by type.
139 *
140 * @param int|null $per_page Number of items per page. Default null.
141 * @param int|null $paged Current page number. Default null.
142 * @param string|null $type Type of issues to filter. Default null.
143 *
144 * @return array An array containing the list of issues, ignored issues, and various count statistics.
145 * - 'ignored' (array): List of ignored issues.
146 * - 'issues' (array): List of active issues.
147 * - 'count_total' (int): Total number of active issues.
148 * - 'count_issues' (int): Total number of issues of all types.
149 * - 'count_issues_filtered' (int): Number of issues filtered by type.
150 * - 'count_ignored' (int): Total number of ignored issues.
151 * - 'count_core' (int): Number of core integrity issues.
152 * - 'count_plugin' (int): Number of plugin check issues.
153 * - 'count_malware' (int): Number of suspicious/malware issues.
154 * - 'count_vuln' (int): Number of vulnerability issues.
155 * - 'count_outdated_plugin' (int): Number of outdated plugin issues.
156 * - 'count_closed_plugin' (int): Number of closed plugin issues.
157 */
158 public function prepare_issues( $per_page = null, $paged = null, $type = null ): array {
159 $ignored_models = $this->get_issues( $type, Scan_Item::STATUS_IGNORE, $per_page, $paged );
160 $active_models = $this->get_issues( $type, Scan_Item::STATUS_ACTIVE, $per_page, $paged );
161
162 $issues = array();
163 $ignored = array();
164 $count_total = count( $active_models );
165 $count_issues_filtered = 0;
166
167 $scan_item_group_total = wd_di()->get( Scan_Item::class )->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
168
169 $count_issues = isset( $scan_item_group_total['all'] ) ?
170 $scan_item_group_total['all'] : 0;
171 $count_core = isset( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
172 $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
173 $count_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
174 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
175 $count_malware = isset( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
176 $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
177 $count_vuln = isset( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
178 $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
179 // New counts since v5.5.0.
180 $count_outdated_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] ) ?
181 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] : 0;
182 $count_closed_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] ) ?
183 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] : 0;
184
185 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
186
187 $count_ignored = isset( $scan_item_ignore_total['all'] ) ?
188 $scan_item_ignore_total['all'] : 0;
189
190 foreach ( $ignored_models as $model ) {
191 $ignored[] = $model->to_array();
192 }
193 foreach ( $active_models as $active_model ) {
194 $issues[] = $active_model->to_array();
195
196 // We will now count all issues again by type filter for pagination usage.
197 if ( null !== $type && 'all' !== $type ) {
198 if ( $type === $active_model->type ) {
199 ++$count_issues_filtered;
200 }
201 } else {
202 ++$count_issues_filtered;
203 }
204 }
205
206 return array(
207 'ignored' => $ignored,
208 'issues' => $issues,
209 'count_total' => $count_total,
210 'count_issues' => $count_issues,
211 'count_issues_filtered' => $count_issues_filtered,
212 'count_ignored' => $count_ignored,
213 'count_core' => $count_core,
214 'count_plugin' => $count_plugin,
215 'count_malware' => $count_malware,
216 'count_vuln' => $count_vuln,
217 'count_outdated_plugin' => $count_outdated_plugin,
218 'count_closed_plugin' => $count_closed_plugin,
219 );
220 }
221
222 /**
223 * Get all Scan types.
224 *
225 * @return array
226 */
227 private static function get_all_scan_types(): array {
228 return array(
229 Scan_Item::TYPE_VULNERABILITY,
230 Scan_Item::TYPE_INTEGRITY,
231 Scan_Item::TYPE_PLUGIN_CHECK,
232 Scan_Item::TYPE_SUSPICIOUS,
233 Scan_Item::TYPE_PLUGIN_CLOSED,
234 Scan_Item::TYPE_PLUGIN_OUTDATED,
235 );
236 }
237
238 /**
239 * Get abandoned plugin types.
240 *
241 * @return array
242 */
243 public static function get_abandoned_types(): array {
244 return array(
245 Scan_Item::TYPE_PLUGIN_CLOSED,
246 Scan_Item::TYPE_PLUGIN_OUTDATED,
247 );
248 }
249
250 /**
251 * Retrieves scan issues based on provided filters.
252 * This method fetches scan items related to the current object's ID,
253 * filtered by type, status, and pagination parameters.
254 * The retrieved items are then attached with relevant behaviors based on their type.
255 *
256 * @param string|array|null $type Optional. The type of scan issue to filter by.
257 * Accepts 'vulnerability', 'integrity', 'plugin_check', or 'suspicious'. Default null.
258 * @param string|null $status Optional. The status of the scan issue to filter by.
259 * Accepts 'ignore' or 'active'. Default null.
260 * @param int|null $per_page Optional. The number of items to retrieve per page. Default null.
261 * @param int|null $paged Optional. The page number of items to retrieve. Default null.
262 *
263 * @return array An array of scan issue models with attached behaviors.
264 */
265 public function get_issues( $type = null, $status = null, $per_page = null, $paged = null ) {
266 $orm = self::get_orm();
267 $builder = $orm->get_repository( Scan_Item::class )
268 ->where( 'parent_id', $this->id );
269
270 if ( null !== $type ) {
271 $valid_types = self::get_all_scan_types();
272 if ( is_array( $type ) ) {
273 $filtered_types = array_intersect( $type, $valid_types );
274 if ( ! empty( $filtered_types ) ) {
275 $builder->where( 'type', 'IN', $filtered_types );
276 }
277 } elseif ( in_array( $type, $valid_types, true ) ) {
278 $builder->where( 'type', $type );
279 }
280 }
281
282 if ( null !== $status ) {
283 static $valid_statuses = array( Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE );
284 if ( in_array( $status, $valid_statuses, true ) ) {
285 $builder->where( 'status', $status );
286 }
287 }
288
289 if ( is_int( $per_page ) && is_int( $paged ) && $per_page > 0 && $paged > 0 ) {
290 $offset = ( $paged - 1 ) * $per_page;
291 $builder->limit( $per_page, $offset );
292 }
293
294 $models = $builder->get();
295
296 static $behavior_map = array(
297 Scan_Item::TYPE_INTEGRITY => Core_Integrity::class,
298 Scan_Item::TYPE_PLUGIN_CHECK => Plugin_Integrity::class,
299 Scan_Item::TYPE_PLUGIN_CLOSED => Abandoned_Result::class,
300 Scan_Item::TYPE_PLUGIN_OUTDATED => Abandoned_Result::class,
301 );
302
303 foreach ( $models as $model ) {
304 if ( isset( $behavior_map[ $model->type ] ) ) {
305 $behavior_class = $behavior_map[ $model->type ];
306 $model->attach_behavior( $behavior_class, $behavior_class );
307 }
308 }
309
310 return $models;
311 }
312
313 /**
314 * Counts the number of Scan_Item models that match the given type and status.
315 *
316 * @param string|null $type The type of Scan_Item to count. Must be one of the following:
317 * Scan_Item::TYPE_VULNERABILITY, Scan_Item::TYPE_INTEGRITY,
318 * Scan_Item::TYPE_PLUGIN_CHECK, Scan_Item::TYPE_SUSPICIOUS.
319 * @param string|null $status The status of the Scan_Item to count. Must be one of the following:
320 * Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE.
321 *
322 * @return mixed The number of matching Scan_Item models.
323 */
324 public function count( $type = null, $status = null ) {
325 $orm = self::get_orm();
326 $builder = $orm->get_repository( Scan_Item::class )->where( 'parent_id', $this->id );
327
328 if (
329 ! is_null( $type )
330 && in_array(
331 $type,
332 self::get_all_scan_types(),
333 true
334 )
335 ) {
336 $builder->where( 'type', $type );
337 }
338 if (
339 ! is_null( $status )
340 && in_array( $status, array( Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ), true )
341 ) {
342 $builder->where( 'status', $status );
343 }
344
345 return $builder->count();
346 }
347
348 /**
349 * Allow a specific issue by updating its status and removing it from the global ignore indexer.
350 *
351 * @param int $id The ID of the issue to Allow.
352 *
353 * @return bool|void Returns false if the issue does not exist, otherwise void.
354 */
355 public function unignore_issue( $id ) {
356 $issue = $this->get_issue( $id );
357 if ( ! is_object( $issue ) ) {
358 return false;
359 }
360 $issue->status = Scan_Item::STATUS_ACTIVE;
361 $issue->save();
362
363 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
364 $data = $issue->raw_data;
365 if ( isset( $data['file'] ) ) {
366 unset( $ignore_lists[ array_search( $data['file'], $ignore_lists, true ) ] );
367 } elseif ( isset( $data['slug'] ) ) {
368 unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists, true ) ] );
369 }
370 $this->update_ignore_list( $ignore_lists );
371 }
372
373 /**
374 * Check if a slug is ignored, we use a global indexer, so we can check while
375 * the active scan is running.
376 *
377 * @param string $slug The path to file.
378 *
379 * @return bool
380 */
381 public function is_issue_ignored( $slug ) {
382 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
383
384 return in_array( $slug, $ignore_lists, true );
385 }
386
387 /**
388 * Ignore a specific issue by updating its status and adding it to the global ignore indexer.
389 *
390 * @param int $id The ID of the issue to ignore.
391 *
392 * @return bool|void Returns false if the issue does not exist, otherwise void.
393 */
394 public function ignore_issue( $id ) {
395 $issue = $this->get_issue( $id );
396 if ( ! is_object( $issue ) ) {
397 return false;
398 }
399
400 $issue->status = Scan_Item::STATUS_IGNORE;
401 $issue->save();
402
403 // Add this into global ignore index.
404 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
405 $data = $issue->raw_data;
406 if ( isset( $data['file'] ) ) {
407 $ignore_lists[] = $data['file'];
408 } elseif ( isset( $data['slug'] ) ) {
409 $ignore_lists[] = $data['slug'];
410 }
411 $this->update_ignore_list( $ignore_lists );
412 }
413
414 /**
415 * Retrieves a Scan_Item object based on the given ID.
416 *
417 * @param int $id The ID of the Scan_Item.
418 *
419 * @return Scan_Item|null The Scan_Item object if found, null otherwise.
420 */
421 public function get_issue( $id ) {
422 $orm = self::get_orm();
423 $model = $orm->get_repository( Scan_Item::class )
424 ->where( 'id', $id )
425 ->first();
426
427 if ( is_object( $model ) ) {
428 static $behavior_map = array(
429 Scan_Item::TYPE_INTEGRITY => Core_Integrity::class,
430 Scan_Item::TYPE_PLUGIN_CHECK => Plugin_Integrity::class,
431 Scan_Item::TYPE_PLUGIN_CLOSED => Abandoned_Result::class,
432 Scan_Item::TYPE_PLUGIN_OUTDATED => Abandoned_Result::class,
433 );
434
435 if ( isset( $behavior_map[ $model->type ] ) ) {
436 $behavior_class = $behavior_map[ $model->type ];
437 $model->attach_behavior( $behavior_class, $behavior_class );
438 }
439 }
440
441 return $model;
442 }
443
444 /**
445 * Remove an issue, this will happen when that issue is resolve, or the file link to this issue get deleted.
446 *
447 * @param int $id The ID of the issue to remove.
448 */
449 public function remove_issue( $id ) {
450 $orm = self::get_orm();
451 $orm->get_repository( Scan_Item::class )->delete( array( 'id' => $id ) );
452 }
453
454 /**
455 * Get all scan types where the target entity is the whole folder.
456 *
457 * @return array
458 */
459 private function get_scan_types_for_whole_folder(): array {
460 return array(
461 Scan_Item::TYPE_VULNERABILITY,
462 Scan_Item::TYPE_PLUGIN_CLOSED,
463 Scan_Item::TYPE_PLUGIN_OUTDATED,
464 );
465 }
466
467 /**
468 * Remove other Scan issue(-s) for the same file.
469 *
470 * @param string $path The path to file.
471 * @param string $type The type of scan issue.
472 *
473 * @return void
474 */
475 public function remove_related_issue_by( string $path, string $type ) {
476 $orm = self::get_orm();
477 $builder = $orm->get_repository( Scan_Item::class )
478 ->where( 'parent_id', $this->id );
479 // No needs to separate check VULNERABILITY, PLUGIN_CLOSED and PLUGIN_OUTDATED because we do not delete per file for such types.
480 $arr_excluded_types = $this->get_scan_types_for_whole_folder();
481 if ( '' !== $path ) {
482 $arr_excluded_types[] = $type;
483 }
484 $builder->where( 'type', 'NOT IN', $arr_excluded_types );
485 $models = $builder->get();
486
487 if ( is_array( $models ) && array() !== $models ) {
488 foreach ( $models as $model ) {
489 if ( isset( $model->raw_data['file'] ) && $model->raw_data['file'] === $path ) {
490 $this->remove_issue( $model->id );
491 }
492 }
493 }
494 }
495
496 /**
497 * Converts the object to an array representation.
498 *
499 * @param int|null $per_page The number of items to retrieve per page. Default null.
500 * @param int|null $paged The page number of items to retrieve. Default null.
501 * @param string|null $type The type of scan issue to filter by. Default null.
502 *
503 * @return array The array representation of the object.
504 */
505 public function to_array( $per_page = null, $paged = null, $type = null ) {
506 if ( ! in_array( $this->status, array( self::STATUS_ERROR, self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
507
508 return array(
509 'status' => $this->status,
510 'status_text' => $this->get_status_text(),
511 'percent' => $this->percent,
512 'task_checkpoint' => $this->task_checkpoint,
513 // This only for hub, when a scan running.
514 'count' => array( 'total' => 0 ),
515 'run_background' => (int) get_site_transient( 'defender_run_background' ) === $this->id,
516 );
517 } elseif ( in_array( $this->status, array( self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
518 $total_filtered = (int) $this->count( $type );
519 $count_issues_filtered = (int) $this->count( $type, Scan_Item::STATUS_ACTIVE );
520 $total_count = (int) $this->count( null, Scan_Item::STATUS_ACTIVE );
521
522 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )
523 ->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
524
525 $count_ignored = isset( $scan_item_ignore_total['all'] ) ?
526 $scan_item_ignore_total['all'] : 0;
527
528 $total_issue_pages = 1;
529 $total_ignored_pages = 1;
530 if ( ! is_null( $per_page ) && ( $total_count > $per_page ) ) {
531 $data = $this->prepare_issues( $per_page, $paged, $type );
532 if ( ! is_null( $paged ) ) {
533 $total_issue_pages = ceil( $count_issues_filtered / $per_page );
534 $total_ignored_pages = ceil( $count_ignored / $per_page );
535 }
536 } else {
537 $data = $this->prepare_issues( null, null, $type );
538 }
539
540 $scan_item_group_total = wd_di()->get( Scan_Item::class )
541 ->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
542
543 $count_issues = isset( $scan_item_group_total['all'] ) ?
544 $scan_item_group_total['all'] : 0;
545 $count_core = isset( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
546 $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
547 $count_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
548 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
549 $count_malware = isset( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
550 $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
551 $count_vuln = isset( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
552 $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
553 // New counts since v5.5.0.
554 $count_outdated_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] ) ?
555 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] : 0;
556 $count_closed_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] ) ?
557 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] : 0;
558
559 return array(
560 'status' => $this->status,
561 'issues_items' => $data['issues'],
562 'ignored_items' => $data['ignored'],
563 'last_scan' => $this->format_date_time( $this->date_start ),
564 'count' => array(
565 'total' => is_array( $data['issues'] ) || $data['issues'] instanceof Countable ? count( $data['issues'] ) : 0,
566 'total_filtered' => $total_filtered,
567 'issues_total' => $count_issues,
568 'issues_total_filtered' => $count_issues_filtered,
569 'ignored_total' => $count_ignored,
570 'core' => $count_core + $count_plugin,
571 'content' => $count_malware,
572 'vuln' => $count_vuln,
573 'outdated_plugin' => $count_outdated_plugin,
574 'closed_plugin' => $count_closed_plugin,
575 ),
576 'paging' => array(
577 'issue' => array(
578 'paged' => $paged,
579 'total_pages' => $total_issue_pages,
580 ),
581 'ignored' => array(
582 'paged' => $paged,
583 'total_pages' => $total_ignored_pages,
584 ),
585 'per_page' => $per_page,
586 ),
587 'task_checkpoint' => $this->task_checkpoint,
588 );
589 } else {
590 return array();
591 }
592 }
593
594 /**
595 * Creates a new record in the database.
596 *
597 * @param bool $from_report Is this a scan from report.
598 *
599 * @return bool|WP_Error|Scan
600 */
601 public static function create( $from_report = false ) {
602 $orm = self::get_orm();
603 $active = self::get_active();
604 if ( is_object( $active ) ) {
605 return new WP_Error( Error_Code::INVALID, esc_html__( 'A scan is already in progress.', 'defender-security' ) );
606 }
607 $model = new Scan();
608 $model->status = self::STATUS_INIT;
609 $model->date_start = gmdate( 'Y-m-d H:i:s' );
610 $model->date_end = gmdate( 'Y-m-d H:i:s' );
611 $model->is_automation = $from_report;
612
613 $orm->save( $model );
614
615 return $model;
616 }
617
618 /**
619 * Delete current scan.
620 *
621 * @param int|null $id Table primary key id.
622 */
623 public function delete( $id = null ) {
624 if ( ! $this->is_positive_int( $id ) ) {
625 $id = $this->id;
626 }
627
628 // Delete all the related result items.
629 $orm = self::get_orm();
630
631 $orm->get_repository( Scan_Item::class )->delete(
632 array( 'parent_id' => $id )
633 );
634
635 $orm->get_repository( self::class )->delete(
636 array( 'id' => $id )
637 );
638 }
639
640 /**
641 * Get the current active scan if any.
642 *
643 * @return self|null
644 */
645 public static function get_active() {
646 $orm = self::get_orm();
647
648 return $orm->get_repository( self::class )
649 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR, self::STATUS_IDLE ) )
650 ->first();
651 }
652
653 /**
654 * Check if the current state is Core integrity.
655 *
656 * @return self|null
657 */
658 public static function get_core_check() {
659 $orm = self::get_orm();
660
661 return $orm->get_repository( self::class )
662 ->where( 'status', self::STEP_CHECK_CORE )
663 ->first();
664 }
665
666 /**
667 * Get last result.
668 *
669 * @return self|null
670 */
671 public static function get_last() {
672 $orm = self::get_orm();
673
674 return $orm->get_repository( self::class )
675 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
676 ->order_by( 'id', 'desc' )
677 ->first();
678 }
679
680 /**
681 * Get last results.
682 *
683 * @return array
684 */
685 public static function get_last_all() {
686 $orm = self::get_orm();
687
688 return $orm->get_repository( self::class )
689 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
690 ->order_by( 'id', 'desc' )
691 ->get();
692 }
693
694 /**
695 * Adds an item to the scan.
696 *
697 * @param mixed $type The type of the item.
698 * @param mixed $data The data of the item.
699 * @param string $status The status of the item. Default is Scan_Item::STATUS_ACTIVE.
700 *
701 * @return bool Returns true if the item is successfully added, false otherwise.
702 */
703 public function add_item( $type, $data, $status = Scan_Item::STATUS_ACTIVE ) {
704 $model = new Scan_Item();
705 $model->type = $type;
706 $model->parent_id = $this->id;
707 $model->raw_data = $data;
708 $model->status = $status;
709 $ret = $model->save();
710
711 return $ret;
712 }
713
714 /**
715 * Return current status as readable string.
716 *
717 * @return string
718 */
719 public function get_status_text() {
720 switch ( $this->status ) {
721 case self::STATUS_INIT:
722 return esc_html__( 'Initializing...', 'defender-security' );
723 case self::STEP_GATHER_INFO:
724 return esc_html__( 'Gathering information...', 'defender-security' );
725 case self::STEP_CHECK_CORE:
726 return esc_html__( 'Analyzing WordPress Core...', 'defender-security' );
727 case self::STEP_CHECK_PLUGIN:
728 return esc_html__( 'Analyzing WordPress Plugins...', 'defender-security' );
729 case self::STEP_VULN_CHECK:
730 return esc_html__( 'Checking for any published vulnerabilities in your plugins and themes...', 'defender-security' );
731 case self::STEP_SUSPICIOUS_CHECK:
732 return esc_html__( 'Analyzing WordPress Content...', 'defender-security' );
733 case self::STEP_ABANDONED_PLUGIN_CHECK:
734 return esc_html__( 'Checking for any outdated & removed plugins...', 'defender-security' );
735 default:
736 return esc_html__( 'The scan is running', 'defender-security' );
737 }
738 }
739
740 /**
741 * Calculates the percentage of a task based on its progress and position.
742 *
743 * @param int $task_percent The percentage of the task completed.
744 * @param int $pos The position of the task in the list of tasks. Default is 1.
745 *
746 * @return float The calculated percentage.
747 */
748 public function calculate_percent( $task_percent, $pos = 1 ) {
749 $task_max = ( 0 !== $this->total_tasks ) ? ( 100 / $this->total_tasks ) : 0;
750 $task_base = $task_max * ( $pos - 1 );
751 $micro = $task_percent * $task_max / 100;
752 $this->percent = (int) round( $task_base + $micro, 2 );
753 if ( $this->percent > 100 ) {
754 $this->percent = 100;
755 }
756
757 return $this->percent;
758 }
759
760 /**
761 * Get list of whitelisted files.
762 *
763 * @return array
764 */
765 private function whitelisted_files() {
766 return array(
767 // Configuration files.
768 'user.ini',
769 'php.ini',
770 'robots.txt',
771 '.htaccess',
772 'nginx.conf',
773 // Hidden system files and directories.
774 '.well-known',
775 '.idea',
776 '.DS_Store',
777 '.svn',
778 '.git',
779 '.quarantine',
780 '.tmb',
781 '.vscode',
782 );
783 }
784
785 /**
786 * Check if a slug is whitelisted.
787 *
788 * @param string $slug The path to file.
789 *
790 * @return bool
791 */
792 public function is_issue_whitelisted( string $slug ): bool {
793 $whitelisted_files = $this->whitelisted_files();
794 foreach ( $whitelisted_files as $file ) {
795 if ( false !== stristr( $slug, $file ) ) {
796 return true;
797 }
798 }
799
800 return false;
801 }
802
803 /**
804 * Update ignore list.
805 *
806 * @param array $ignore_lists Items to be added to the ignore list.
807 */
808 public function update_ignore_list( $ignore_lists ) {
809 $ignore_lists = array_unique( $ignore_lists );
810 $ignore_lists = array_filter( $ignore_lists, 'strlen' );
811 update_site_option( self::IGNORE_INDEXER, $ignore_lists );
812 }
813
814 /**
815 * Get the threshold time limit as DateTime object.
816 *
817 * @return DateTime Threshold time limit as DateTime object.
818 */
819 public function threshold_date_time_object() {
820 $timezone = new DateTimeZone( 'UTC' );
821
822 /**
823 * Filter to override scan threshold period.
824 *
825 * @param string $threshold Any valid relative Date and Time formats.
826 *
827 * @link https://www.php.net/manual/en/datetime.formats.relative.php
828 * @since 2.6.1
829 */
830 $threshold = apply_filters( 'wd_scan_threshold', self::THRESHOLD_PERIOD );
831
832 return new DateTime( $threshold, $timezone );
833 }
834
835 /**
836 * Threshold time limit in mysql string format.
837 *
838 * @return string Threshold time limit as mysql string format.
839 */
840 public function threshold_date_time_mysql() {
841 $type = 'Y-m-d H:i:s';
842 $threshold_date_time_object = $this->threshold_date_time_object();
843 $mysql_format = $threshold_date_time_object->format( $type );
844
845 return $mysql_format;
846 }
847
848 /**
849 * Get the idle scan if any.
850 *
851 * @return self|null
852 */
853 public function get_idle() {
854 $orm = self::get_orm();
855
856 $mysql_date = $this->threshold_date_time_mysql();
857
858 return $orm->get_repository( self::class )
859 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR ) )
860 ->where( 'date_start', '<', $mysql_date )
861 ->first();
862 }
863
864 /**
865 * Delete all idle scan and scan items
866 *
867 * @since 2.6.1
868 */
869 public function delete_idle() {
870 $idle_scans = self::get_orm()
871 ->get_repository( self::class )
872 ->where( 'status', self::STATUS_IDLE )
873 ->get();
874
875 foreach ( $idle_scans as $idle_scan ) {
876 $this->delete( $idle_scan->id );
877 }
878 }
879
880 /**
881 * Verify positive integer or not.
882 *
883 * @param mixed $id Argument to check for a positive number.
884 *
885 * @return bool Return true on positive integer else false.
886 * @since 2.6.1
887 */
888 private function is_positive_int( $id ): bool {
889 return is_int( $id ) && $id > 0;
890 }
891 }
892