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

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