PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
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.8.1, at src/model/class-scan.php

909 lines 27.4 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 ( array() !== $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
354 */
355 public function unignore_issue( $id ): bool {
356 $issue = $this->get_issue( $id );
357 if ( ! is_object( $issue ) ) {
358 return false;
359 }
360 // Check if the current issue already exists in the Issues list, there is no need to add a duplicate.
361 $current_issue_arr = $issue->to_array();
362 foreach ( $this->get_issues( null, Scan_Item::STATUS_ACTIVE ) as $active_issue ) {
363 $active_issue_arr = $active_issue->to_array();
364 if ( $issue->type === $active_issue_arr['type'] && $current_issue_arr['file_name'] === $active_issue_arr['file_name'] ) {
365 return false;
366 }
367 }
368
369 $issue->status = Scan_Item::STATUS_ACTIVE;
370 $issue->save();
371
372 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
373 $data = $issue->raw_data;
374 if ( isset( $data['file'] ) ) {
375 unset( $ignore_lists[ array_search( $data['file'], $ignore_lists, true ) ] );
376 } elseif ( isset( $data['slug'] ) ) {
377 unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists, true ) ] );
378 }
379 $this->update_ignore_list( $ignore_lists );
380
381 return true;
382 }
383
384 /**
385 * Check if a slug is ignored, we use a global indexer, so we can check while
386 * the active scan is running.
387 *
388 * @param string $slug The path to file.
389 *
390 * @return bool
391 */
392 public function is_issue_ignored( $slug ) {
393 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
394
395 return in_array( $slug, $ignore_lists, true );
396 }
397
398 /**
399 * Ignore a specific issue by updating its status and adding it to the global ignored indexer.
400 *
401 * @param int $id The ID of the issue to ignore.
402 *
403 * @return bool
404 */
405 public function ignore_issue( $id ): bool {
406 $issue = $this->get_issue( $id );
407 if ( ! is_object( $issue ) ) {
408 return false;
409 }
410 // Check if the current issue already exists in the Ignored list, there is no need to add a duplicate.
411 $current_issue_arr = $issue->to_array();
412 foreach ( $this->get_issues( null, Scan_Item::STATUS_IGNORE ) as $ignore_issue ) {
413 $ignore_issue_arr = $ignore_issue->to_array();
414 if ( $current_issue_arr['type'] === $ignore_issue_arr['type'] &&
415 $current_issue_arr['full_path'] === $ignore_issue_arr['full_path']
416 ) {
417 return false;
418 }
419 }
420
421 $issue->status = Scan_Item::STATUS_IGNORE;
422 $issue->save();
423
424 // Add this into a global ignored index and update the ignored list.
425 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
426 $ignore_lists[] = $current_issue_arr['full_path'];
427 $this->update_ignore_list( $ignore_lists );
428
429 return true;
430 }
431
432 /**
433 * Retrieves a Scan_Item object based on the given ID.
434 *
435 * @param int $id The ID of the Scan_Item.
436 *
437 * @return Scan_Item|null The Scan_Item object if found, null otherwise.
438 */
439 public function get_issue( $id ) {
440 $orm = self::get_orm();
441 $model = $orm->get_repository( Scan_Item::class )
442 ->where( 'id', $id )
443 ->first();
444
445 if ( is_object( $model ) ) {
446 static $behavior_map = array(
447 Scan_Item::TYPE_INTEGRITY => Core_Integrity::class,
448 Scan_Item::TYPE_PLUGIN_CHECK => Plugin_Integrity::class,
449 Scan_Item::TYPE_PLUGIN_CLOSED => Abandoned_Result::class,
450 Scan_Item::TYPE_PLUGIN_OUTDATED => Abandoned_Result::class,
451 );
452
453 if ( isset( $behavior_map[ $model->type ] ) ) {
454 $behavior_class = $behavior_map[ $model->type ];
455 $model->attach_behavior( $behavior_class, $behavior_class );
456 }
457 }
458
459 return $model;
460 }
461
462 /**
463 * Remove an issue, this will happen when that issue is resolve, or the file link to this issue get deleted.
464 *
465 * @param int $id The ID of the issue to remove.
466 */
467 public function remove_issue( $id ) {
468 $orm = self::get_orm();
469 $orm->get_repository( Scan_Item::class )->delete( array( 'id' => $id ) );
470 }
471
472 /**
473 * Get all scan types where the target entity is the whole folder.
474 *
475 * @return array
476 */
477 private function get_scan_types_for_whole_folder(): array {
478 return array(
479 Scan_Item::TYPE_VULNERABILITY,
480 Scan_Item::TYPE_PLUGIN_CLOSED,
481 Scan_Item::TYPE_PLUGIN_OUTDATED,
482 );
483 }
484
485 /**
486 * Remove other Scan issue(-s) for the same file.
487 *
488 * @param string $path The path to file.
489 * @param string $type The type of scan issue.
490 *
491 * @return void
492 */
493 public function remove_related_issue_by( string $path, string $type ) {
494 $orm = self::get_orm();
495 $builder = $orm->get_repository( Scan_Item::class )
496 ->where( 'parent_id', $this->id );
497 // No needs to separate check VULNERABILITY, PLUGIN_CLOSED and PLUGIN_OUTDATED because we do not delete per file for such types.
498 $arr_excluded_types = $this->get_scan_types_for_whole_folder();
499 if ( '' !== $path ) {
500 $arr_excluded_types[] = $type;
501 }
502 $builder->where( 'type', 'NOT IN', $arr_excluded_types );
503 $models = $builder->get();
504
505 if ( is_array( $models ) && array() !== $models ) {
506 foreach ( $models as $model ) {
507 if ( isset( $model->raw_data['file'] ) && $model->raw_data['file'] === $path ) {
508 $this->remove_issue( $model->id );
509 }
510 }
511 }
512 }
513
514 /**
515 * Converts the object to an array representation.
516 *
517 * @param int|null $per_page The number of items to retrieve per page. Default null.
518 * @param int|null $paged The page number of items to retrieve. Default null.
519 * @param string|null $type The type of scan issue to filter by. Default null.
520 *
521 * @return array The array representation of the object.
522 */
523 public function to_array( $per_page = null, $paged = null, $type = null ) {
524 if ( ! in_array( $this->status, array( self::STATUS_ERROR, self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
525
526 return array(
527 'status' => $this->status,
528 'status_text' => $this->get_status_text(),
529 'percent' => $this->percent,
530 'task_checkpoint' => $this->task_checkpoint,
531 // This only for hub, when a scan running.
532 'count' => array( 'total' => 0 ),
533 );
534 } elseif ( in_array( $this->status, array( self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
535 $total_filtered = (int) $this->count( $type );
536 $count_issues_filtered = (int) $this->count( $type, Scan_Item::STATUS_ACTIVE );
537 $total_count = (int) $this->count( null, Scan_Item::STATUS_ACTIVE );
538
539 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )
540 ->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
541
542 $count_ignored = isset( $scan_item_ignore_total['all'] ) ?
543 $scan_item_ignore_total['all'] : 0;
544
545 $total_issue_pages = 1;
546 $total_ignored_pages = 1;
547 if ( ! is_null( $per_page ) && ( $total_count > $per_page ) ) {
548 $data = $this->prepare_issues( $per_page, $paged, $type );
549 if ( ! is_null( $paged ) ) {
550 $total_issue_pages = ceil( $count_issues_filtered / $per_page );
551 $total_ignored_pages = ceil( $count_ignored / $per_page );
552 }
553 } else {
554 $data = $this->prepare_issues( null, null, $type );
555 }
556
557 $scan_item_group_total = wd_di()->get( Scan_Item::class )
558 ->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
559
560 $count_issues = isset( $scan_item_group_total['all'] ) ?
561 $scan_item_group_total['all'] : 0;
562 $count_core = isset( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
563 $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
564 $count_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
565 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
566 $count_malware = isset( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
567 $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
568 $count_vuln = isset( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
569 $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
570 // New counts since v5.5.0.
571 $count_outdated_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] ) ?
572 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] : 0;
573 $count_closed_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] ) ?
574 $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] : 0;
575
576 return array(
577 'status' => $this->status,
578 'issues_items' => $data['issues'],
579 'ignored_items' => $data['ignored'],
580 'last_scan' => $this->format_date_time( $this->date_start ),
581 'count' => array(
582 'total' => is_array( $data['issues'] ) || $data['issues'] instanceof Countable ? count( $data['issues'] ) : 0,
583 'total_filtered' => $total_filtered,
584 'issues_total' => $count_issues,
585 'issues_total_filtered' => $count_issues_filtered,
586 'ignored_total' => $count_ignored,
587 'core' => $count_core + $count_plugin,
588 'content' => $count_malware,
589 'vuln' => $count_vuln,
590 'outdated_plugin' => $count_outdated_plugin,
591 'closed_plugin' => $count_closed_plugin,
592 ),
593 'paging' => array(
594 'issue' => array(
595 'paged' => $paged,
596 'total_pages' => $total_issue_pages,
597 ),
598 'ignored' => array(
599 'paged' => $paged,
600 'total_pages' => $total_ignored_pages,
601 ),
602 'per_page' => $per_page,
603 ),
604 'task_checkpoint' => $this->task_checkpoint,
605 );
606 } else {
607 return array();
608 }
609 }
610
611 /**
612 * Creates a new record in the database.
613 *
614 * @param bool $from_report Is this a scan from report.
615 *
616 * @return bool|WP_Error|Scan
617 */
618 public static function create( $from_report = false ) {
619 $orm = self::get_orm();
620 $active = self::get_active();
621 if ( is_object( $active ) ) {
622 return new WP_Error( Error_Code::INVALID, esc_html__( 'A scan is already in progress.', 'defender-security' ) );
623 }
624 $model = new Scan();
625 $model->status = self::STATUS_INIT;
626 $model->date_start = gmdate( 'Y-m-d H:i:s' );
627 $model->date_end = gmdate( 'Y-m-d H:i:s' );
628 $model->is_automation = $from_report;
629
630 $orm->save( $model );
631
632 return $model;
633 }
634
635 /**
636 * Delete current scan.
637 *
638 * @param int|null $id Table primary key id.
639 */
640 public function delete( $id = null ) {
641 if ( ! $this->is_positive_int( $id ) ) {
642 $id = $this->id;
643 }
644
645 // Delete all the related result items.
646 $orm = self::get_orm();
647
648 $orm->get_repository( Scan_Item::class )->delete(
649 array( 'parent_id' => $id )
650 );
651
652 $orm->get_repository( self::class )->delete(
653 array( 'id' => $id )
654 );
655 }
656
657 /**
658 * Get the current active scan if any.
659 *
660 * @return self|null
661 */
662 public static function get_active() {
663 $orm = self::get_orm();
664
665 return $orm->get_repository( self::class )
666 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR, self::STATUS_IDLE ) )
667 ->first();
668 }
669
670 /**
671 * Check if the current state is Core integrity.
672 *
673 * @return self|null
674 */
675 public static function get_core_check() {
676 $orm = self::get_orm();
677
678 return $orm->get_repository( self::class )
679 ->where( 'status', self::STEP_CHECK_CORE )
680 ->first();
681 }
682
683 /**
684 * Get last result.
685 *
686 * @return self|null
687 */
688 public static function get_last() {
689 $orm = self::get_orm();
690
691 return $orm->get_repository( self::class )
692 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
693 ->order_by( 'id', 'desc' )
694 ->first();
695 }
696
697 /**
698 * Get last results.
699 *
700 * @return array
701 */
702 public static function get_last_all() {
703 $orm = self::get_orm();
704
705 return $orm->get_repository( self::class )
706 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
707 ->order_by( 'id', 'desc' )
708 ->get();
709 }
710
711 /**
712 * Adds an item to the scan.
713 *
714 * @param mixed $type The type of the item.
715 * @param mixed $data The data of the item.
716 * @param string $status The status of the item. Default is Scan_Item::STATUS_ACTIVE.
717 *
718 * @return bool Returns true if the item is successfully added, false otherwise.
719 */
720 public function add_item( $type, $data, $status = Scan_Item::STATUS_ACTIVE ) {
721 $model = new Scan_Item();
722 $model->type = $type;
723 $model->parent_id = $this->id;
724 $model->raw_data = $data;
725 $model->status = $status;
726 $ret = $model->save();
727
728 return $ret;
729 }
730
731 /**
732 * Return current status as readable string.
733 *
734 * @return string
735 */
736 public function get_status_text() {
737 switch ( $this->status ) {
738 case self::STATUS_INIT:
739 return esc_html__( 'Initializing...', 'defender-security' );
740 case self::STEP_GATHER_INFO:
741 return esc_html__( 'Gathering information...', 'defender-security' );
742 case self::STEP_CHECK_CORE:
743 return esc_html__( 'Analyzing WordPress Core...', 'defender-security' );
744 case self::STEP_CHECK_PLUGIN:
745 return esc_html__( 'Analyzing WordPress Plugins...', 'defender-security' );
746 case self::STEP_VULN_CHECK:
747 return esc_html__( 'Checking for any published vulnerabilities in your plugins and themes...', 'defender-security' );
748 case self::STEP_SUSPICIOUS_CHECK:
749 return esc_html__( 'Analyzing WordPress Content...', 'defender-security' );
750 case self::STEP_ABANDONED_PLUGIN_CHECK:
751 return esc_html__( 'Checking for any outdated & removed plugins...', 'defender-security' );
752 default:
753 return esc_html__( 'The scan is running', 'defender-security' );
754 }
755 }
756
757 /**
758 * Calculates the percentage of a task based on its progress and position.
759 *
760 * @param int $task_percent The percentage of the task completed.
761 * @param int $pos The position of the task in the list of tasks. Default is 1.
762 *
763 * @return float The calculated percentage.
764 */
765 public function calculate_percent( $task_percent, $pos = 1 ) {
766 $task_max = ( 0 !== $this->total_tasks ) ? ( 100 / $this->total_tasks ) : 0;
767 $task_base = $task_max * ( $pos - 1 );
768 $micro = $task_percent * $task_max / 100;
769 $this->percent = (int) round( $task_base + $micro, 2 );
770 if ( $this->percent > 100 ) {
771 $this->percent = 100;
772 }
773
774 return $this->percent;
775 }
776
777 /**
778 * Get list of whitelisted files.
779 *
780 * @return array
781 */
782 private function whitelisted_files() {
783 return array(
784 // Configuration files.
785 'user.ini',
786 'php.ini',
787 'robots.txt',
788 '.htaccess',
789 'nginx.conf',
790 // Hidden system files and directories.
791 '.well-known',
792 '.idea',
793 '.DS_Store',
794 '.svn',
795 '.git',
796 '.quarantine',
797 '.tmb',
798 '.vscode',
799 );
800 }
801
802 /**
803 * Check if a slug is whitelisted.
804 *
805 * @param string $slug The path to file.
806 *
807 * @return bool
808 */
809 public function is_issue_whitelisted( string $slug ): bool {
810 $whitelisted_files = $this->whitelisted_files();
811 foreach ( $whitelisted_files as $file ) {
812 if ( false !== stristr( $slug, $file ) ) {
813 return true;
814 }
815 }
816
817 return false;
818 }
819
820 /**
821 * Update ignore list.
822 *
823 * @param array $ignore_lists Items to be added to the ignore list.
824 */
825 public function update_ignore_list( $ignore_lists ) {
826 $ignore_lists = array_unique( $ignore_lists );
827 $ignore_lists = array_filter( $ignore_lists, 'strlen' );
828 update_site_option( self::IGNORE_INDEXER, $ignore_lists );
829 }
830
831 /**
832 * Get the threshold time limit as DateTime object.
833 *
834 * @return DateTime Threshold time limit as DateTime object.
835 */
836 public function threshold_date_time_object() {
837 $timezone = new DateTimeZone( 'UTC' );
838
839 /**
840 * Filter to override scan threshold period.
841 *
842 * @param string $threshold Any valid relative Date and Time formats.
843 *
844 * @link https://www.php.net/manual/en/datetime.formats.relative.php
845 * @since 2.6.1
846 */
847 $threshold = apply_filters( 'wd_scan_threshold', self::THRESHOLD_PERIOD );
848
849 return new DateTime( $threshold, $timezone );
850 }
851
852 /**
853 * Threshold time limit in mysql string format.
854 *
855 * @return string Threshold time limit as mysql string format.
856 */
857 public function threshold_date_time_mysql() {
858 $type = 'Y-m-d H:i:s';
859 $threshold_date_time_object = $this->threshold_date_time_object();
860 $mysql_format = $threshold_date_time_object->format( $type );
861
862 return $mysql_format;
863 }
864
865 /**
866 * Get the idle scan if any.
867 *
868 * @return self|null
869 */
870 public function get_idle() {
871 $orm = self::get_orm();
872
873 $mysql_date = $this->threshold_date_time_mysql();
874
875 return $orm->get_repository( self::class )
876 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR ) )
877 ->where( 'date_start', '<', $mysql_date )
878 ->first();
879 }
880
881 /**
882 * Delete all idle scan and scan items
883 *
884 * @since 2.6.1
885 */
886 public function delete_idle() {
887 $idle_scans = self::get_orm()
888 ->get_repository( self::class )
889 ->where( 'status', self::STATUS_IDLE )
890 ->get();
891
892 foreach ( $idle_scans as $idle_scan ) {
893 $this->delete( $idle_scan->id );
894 }
895 }
896
897 /**
898 * Verify positive integer or not.
899 *
900 * @param mixed $id Argument to check for a positive number.
901 *
902 * @return bool Return true on positive integer else false.
903 * @since 2.6.1
904 */
905 private function is_positive_int( $id ): bool {
906 return is_int( $id ) && $id > 0;
907 }
908 }
909