PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.0.1
Defender Security – Malware Scanner, Login Security & Firewall v3.0.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 / scan.php

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

725 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Model;
4
5 use WP_Defender\Behavior\Scan_Item\Core_Integrity;
6 use WP_Defender\Behavior\Scan_Item\Plugin_Integrity;
7 use WP_Defender\Behavior\Scan_Item\Theme_Integrity;
8 use WP_Defender\Behavior\Scan_Item\Malware_Result;
9 use WP_Defender\Behavior\Scan_Item\Vuln_Result;
10 use WP_Defender\Component\Error_Code;
11 use WP_Defender\DB;
12 use WP_Defender\Traits\Formats;
13 use WP_Defender\Traits\IO;
14
15 class Scan extends DB {
16 use IO, Formats;
17
18 public const STATUS_INIT = 'init', STATUS_ERROR = 'error', STATUS_FINISH = 'finish';
19 public const IGNORE_INDEXER = 'defender_scan_ignore_index';
20 protected $table = 'defender_scan';
21
22 /**
23 * Any valid relative Date and Time formats.
24 *
25 * @link https://www.php.net/manual/en/datetime.formats.relative.php
26 *
27 * @since 2.6.1
28 *
29 * @var string
30 */
31 public const THRESHOLD_PERIOD = '3 hours ago';
32
33 /**
34 * Constant to notate the scan is idle or crossed the threshold limit.
35 *
36 * @since 2.6.1
37 *
38 * @var string
39 */
40 public const STATUS_IDLE = 'idle';
41
42 /**
43 * @var int
44 * @defender_property
45 */
46 public $id;
47 /**
48 * Scan status, the native status is init, error, and finish, we can have other status base on the
49 * task the scan is running, like gather_fact, core_integrity etc.
50 *
51 * @var string
52 * @defender_property
53 */
54 public $status;
55 /**
56 * Mysql time.
57 * @var string
58 * @defender_property
59 */
60 public $date_start;
61
62 /**
63 * Store the current percent.
64 * @var int
65 * @defender_property
66 */
67 public $percent = 0;
68
69 /**
70 * Store how many tasks we process.
71 * @var int
72 * @defender_property
73 */
74 public $total_tasks = 0;
75
76 /**
77 * We will use this so internal task can store the current checkpoint.
78 *
79 * @var string
80 * @defender_property
81 */
82 public $task_checkpoint = '';
83
84 /**
85 * Mysql time.
86 * @var string
87 * @defender_property
88 */
89 public $date_end;
90
91 /**
92 * This only true when a scan trigger by report schedule.
93 * @var bool
94 * @defender_property
95 */
96 public $is_automation = false;
97
98 /**
99 * Return an array with various params, mostly this will be use.
100 *
101 * @return array
102 */
103 public function prepare_issues( $per_page = null, $paged = null, $type = null ) {
104 $ignored_models = $this->get_issues( $type, Scan_Item::STATUS_IGNORE, $per_page, $paged );
105 $active_models = $this->get_issues( $type, Scan_Item::STATUS_ACTIVE, $per_page, $paged );
106 $summary_models = $this->get_issues();
107 $issues = array();
108 $ignored = array();
109 $count_ignored = 0;
110 $count_total = $count_ignored + count( $active_models );
111 $count_issues = 0;
112 $count_issues_filtered = 0;
113 $count_core = 0;
114 $count_plugin = 0;
115 $count_malware = 0;
116 $count_vuln = 0;
117 // Info for Summary box.
118 foreach ( $summary_models as $summary_model ) {
119 if ( Scan_Item::STATUS_ACTIVE === $summary_model->status ) {
120 switch ($summary_model->type) {
121 case Scan_Item::TYPE_INTEGRITY:
122 $count_core++;
123 break;
124 case Scan_Item::TYPE_PLUGIN_CHECK:
125 $count_plugin++;
126 break;
127 case Scan_Item::TYPE_SUSPICIOUS:
128 $count_malware++;
129 break;
130 case Scan_Item::TYPE_VULNERABILITY:
131 default:
132 $count_vuln++;
133 break;
134 }
135 // Count all issues.
136 $count_issues++;
137 } elseif ( Scan_Item::STATUS_IGNORE === $summary_model->status ) {
138 $count_ignored ++;
139 }
140 }
141
142 foreach ( $ignored_models as $model ) {
143 $ignored[] = $model->to_array();
144 }
145 foreach ( $active_models as $active_model ) {
146 $issues[] = $active_model->to_array();
147
148 if ( Scan_Item::STATUS_ACTIVE === $active_model->status ) {
149 // We will now count all issues again by type filter for pagination usage.
150 if ( null !== $type && 'all' !== $type ) {
151 if ( $type === $active_model->type ) {
152 $count_issues_filtered++;
153 }
154 } else {
155 $count_issues_filtered++;
156 }
157 }
158 }
159
160 return array(
161 'ignored' => $ignored,
162 'issues' => $issues,
163 'count_total' => $count_total,
164 'count_issues' => $count_issues,
165 'count_issues_filtered' => $count_issues_filtered,
166 'count_ignored' => $count_ignored,
167 'count_core' => $count_core,
168 'count_plugin' => $count_plugin,
169 'count_malware' => $count_malware,
170 'count_vuln' => $count_vuln,
171 );
172 }
173
174 /**
175 * @param null $type
176 * @param null $status
177 * @param null $per_page
178 * @param null $paged
179 *
180 * @return Scan_Item[]
181 */
182 public function get_issues( $type = null, $status = null, $per_page = null, $paged = null ) {
183 $orm = self::get_orm();
184 $builder = $orm->get_repository( Scan_Item::class )
185 ->where( 'parent_id', $this->id );
186
187 if (
188 ! is_null( $type )
189 && in_array(
190 $type,
191 array(
192 Scan_Item::TYPE_VULNERABILITY,
193 Scan_Item::TYPE_INTEGRITY,
194 Scan_Item::TYPE_PLUGIN_CHECK,
195 // Leave for migration to 2.5.0.
196 Scan_Item::TYPE_THEME_CHECK,
197 Scan_Item::TYPE_SUSPICIOUS,
198 ),
199 true
200 )
201 ) {
202 $builder->where( 'type', $type );
203 }
204 if (
205 ! is_null( $status )
206 && in_array( $status, array( Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ), true )
207 ) {
208 $builder->where( 'status', $status );
209 }
210 if ( ! is_null( $per_page ) && ! is_null( $paged ) ) {
211 $limit = ( ( $paged - 1 ) * $per_page ) . ',' . $per_page;
212 $builder->limit( $limit );
213 }
214 $models = $builder->get();
215 foreach ( $models as $key => $model ) {
216 switch ( $model->type ) {
217 case Scan_Item::TYPE_INTEGRITY:
218 $model->attach_behavior( Core_Integrity::class, Core_Integrity::class );
219 break;
220 // Leave for migration to 2.5.0.
221 case Scan_Item::TYPE_THEME_CHECK:
222 $model->attach_behavior( Theme_Integrity::class, Theme_Integrity::class );
223 break;
224 case Scan_Item::TYPE_PLUGIN_CHECK:
225 $model->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
226 break;
227 case Scan_Item::TYPE_SUSPICIOUS:
228 $model->attach_behavior( Malware_Result::class, Malware_Result::class );
229 break;
230 case Scan_Item::TYPE_VULNERABILITY:
231 default:
232 $model->attach_behavior( Vuln_Result::class, Vuln_Result::class );
233 break;
234 }
235 $models[ $key ] = $model;
236 }
237
238 return $models;
239 }
240
241 public function count( $type = null, $status = null ) {
242 $orm = self::get_orm();
243 $builder = $orm->get_repository( Scan_Item::class )->where( 'parent_id', $this->id );
244
245 if (
246 ! is_null( $type )
247 && in_array(
248 $type,
249 array(
250 Scan_Item::TYPE_VULNERABILITY,
251 Scan_Item::TYPE_INTEGRITY,
252 Scan_Item::TYPE_PLUGIN_CHECK,
253 Scan_Item::TYPE_SUSPICIOUS,
254 ),
255 true
256 )
257 ) {
258 $builder->where( 'type', $type );
259 }
260 if (
261 ! is_null( $status )
262 && in_array( $status, array( Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_ACTIVE ), true )
263 ) {
264 $builder->where( 'status', $status );
265 }
266
267 return $builder->count();
268 }
269 /**
270 * @param int $id
271 *
272 * @return bool
273 */
274 public function unignore_issue( $id ) {
275 $issue = $this->get_issue( $id );
276 if ( ! is_object( $issue ) ) {
277 return false;
278 }
279 $issue->status = Scan_Item::STATUS_ACTIVE;
280 $issue->save();
281
282 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
283 $data = $issue->raw_data;
284 if ( isset( $data['file'] ) ) {
285 unset( $ignore_lists[ array_search( $data['file'], $ignore_lists ) ] );
286 } elseif ( isset( $data['slug'] ) ) {
287 unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists ) ] );
288 }
289 $this->update_ignore_list( $ignore_lists );
290 }
291
292 /**
293 * Check if a slug is ignored, we use a global indexer, so we can check while
294 * the active scan is running.
295 *
296 * @param string $slug
297 *
298 * @return bool
299 */
300 public function is_issue_ignored( $slug ) {
301 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
302
303 return in_array( $slug, $ignore_lists, true );
304 }
305
306 /**
307 * @param int $id
308 *
309 * @return bool
310 */
311 public function ignore_issue( $id ) {
312 $issue = $this->get_issue( $id );
313 if ( ! is_object( $issue ) ) {
314 return false;
315 }
316
317 $issue->status = Scan_Item::STATUS_IGNORE;
318 $issue->save();
319
320 // Add this into global ingnore index.
321 $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
322 $data = $issue->raw_data;
323 if ( isset( $data['file'] ) ) {
324 $ignore_lists[] = $data['file'];
325 } elseif ( isset( $data['slug'] ) ) {
326 $ignore_lists[] = $data['slug'];
327 }
328 $this->update_ignore_list( $ignore_lists );
329 }
330
331 /**
332 * @param int $id
333 *
334 * @return Scan_Item|null
335 */
336 public function get_issue( $id ) {
337 $orm = self::get_orm();
338 $model = $orm->get_repository( Scan_Item::class )
339 ->where( 'id', $id )
340 ->first();
341 if ( is_object( $model ) ) {
342 switch ( $model->type ) {
343 case Scan_Item::TYPE_INTEGRITY:
344 $model->attach_behavior( Core_Integrity::class, Core_Integrity::class );
345 break;
346 case Scan_Item::TYPE_PLUGIN_CHECK:
347 $model->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
348 break;
349 case Scan_Item::TYPE_SUSPICIOUS:
350 $model->attach_behavior( Malware_Result::class, Malware_Result::class );
351 break;
352 case Scan_Item::TYPE_VULNERABILITY:
353 default:
354 $model->attach_behavior( Vuln_Result::class, Vuln_Result::class );
355 break;
356 }
357 }
358
359 return $model;
360 }
361
362 /**
363 * Remove an issue, this will happen when that issue is resolve, or the file link
364 * to this issue get deleted.
365 *
366 * @param int $id
367 */
368 public function remove_issue( $id ) {
369 $orm = self::get_orm();
370 $orm->get_repository( Scan_Item::class )->delete( array( 'id' => $id ) );
371 }
372
373 /**
374 * This will build the data we use to output to frontend, base on the current scenario.
375 * @param null|int $per_page
376 * @param null|int $paged
377 * @param null|string $type
378 *
379 * @return array
380 */
381 public function to_array( $per_page = null, $paged = null, $type = null ) {
382 if ( ! in_array( $this->status, array( self::STATUS_ERROR, self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
383
384 return array(
385 'status' => $this->status,
386 'status_text' => $this->get_status_text(),
387 'percent' => $this->percent,
388 // This only for hub, when a scan running.
389 'count' => array(
390 'total' => 0,
391 ),
392 );
393 } elseif ( in_array( $this->status, array( self::STATUS_FINISH, self::STATUS_IDLE ), true ) ) {
394 $total_data = $this->prepare_issues( null, null, $type );
395 $total_filtered = $this->count( $type );
396 $count_issues_filtered = $total_data['count_issues_filtered'];
397 $ignored_count = $total_data['count_ignored'];
398 $total_count = $total_data['count_total'];
399
400 $total_issue_pages = 1;
401 $total_ignored_pages = 1;
402 if( ! is_null( $per_page ) && ( $total_count > $per_page ) ) {
403 $data = $this->prepare_issues( $per_page, $paged, $type );
404 if ( ! is_null( $paged ) ) {
405 $total_issue_pages = ceil( $count_issues_filtered / $per_page );
406 $total_ignored_pages = ceil( $ignored_count / $per_page );
407 }
408 } else {
409 $data = $total_data;
410 }
411
412 return array(
413 'status' => $this->status,
414 'issues_items' => $data['issues'],
415 'ignored_items' => $data['ignored'],
416 'last_scan' => $this->format_date_time( $this->date_start ),
417 'count' => array(
418 'total' => is_array($data['issues']) || $data['issues'] instanceof \Countable ? count( $data['issues'] ) : 0,
419 'total_filtered' => $total_filtered,
420 'issues_total' => $total_data['count_issues'],
421 'issues_total_filtered' => $count_issues_filtered,
422 'ignored_total' => $ignored_count,
423 'core' => $total_data['count_core'] + $total_data['count_plugin'],
424 'content' => $total_data['count_malware'],
425 'vuln' => $total_data['count_vuln'],
426 ),
427 'paging' => array(
428 'issue' => array(
429 'paged' => $paged,
430 'total_pages' => $total_issue_pages,
431 ),
432 'ignored' => array(
433 'paged' => $paged,
434 'total_pages' => $total_ignored_pages,
435 ),
436 'per_page' => $per_page,
437 ),
438 );
439 }
440 }
441
442 /**
443 * @param false $from_report
444 *
445 * @return Scan|\WP_Error
446 */
447 public static function create( $from_report = false ) {
448 $orm = self::get_orm();
449 $active = self::get_active();
450 if ( is_object( $active ) ) {
451 return new \WP_Error( Error_Code::INVALID, __( 'A scan is already in progress.', 'wpdef' ) );
452 }
453 $model = new Scan();
454 $model->status = self::STATUS_INIT;
455 $model->date_start = gmdate( 'Y-m-d H:i:s' );
456 $model->date_end = gmdate( 'Y-m-d H:i:s' );
457 $model->is_automation = $from_report;
458
459 $orm->save( $model );
460
461 return $model;
462 }
463
464 /**
465 * Delete current scan.
466 *
467 * @param int $id Table primary key id.
468 */
469 public function delete( $id = null ) {
470 if ( ! $this->is_positive_int( $id ) ) {
471 $id = $this->id;
472 }
473
474 // Delete all the related result items.
475 $orm = self::get_orm();
476
477 $orm->get_repository( Scan_Item::class )->delete(
478 array(
479 'parent_id' => $id,
480 )
481 );
482
483 $orm->get_repository( self::class )->delete(
484 array(
485 'id' => $id,
486 )
487 );
488 }
489
490 /**
491 * Get the current active scan if any.
492 *
493 * @return self|null
494 */
495 public static function get_active() {
496 $orm = self::get_orm();
497
498 return $orm->get_repository( self::class )
499 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR, self::STATUS_IDLE ) )
500 ->first();
501 }
502
503 /**
504 * Get last result.
505 *
506 * @return self|null
507 */
508 public static function get_last() {
509 $orm = self::get_orm();
510
511 return $orm->get_repository( self::class )
512 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
513 ->order_by( 'id', 'desc' )
514 ->first();
515 }
516
517 /**
518 * @return array
519 */
520 public static function get_last_all() {
521 $orm = self::get_orm();
522
523 return $orm->get_repository( self::class )
524 ->where( 'status', 'IN', array( self::STATUS_FINISH, self::STATUS_IDLE ) )
525 ->order_by( 'id', 'desc' )
526 ->get();
527 }
528
529 /**
530 * If the scan find any, we will use this to add the issue.
531 *
532 * @param $type
533 * @param $data
534 * @param $status
535 */
536 public function add_item( $type, $data, $status = Scan_Item::STATUS_ACTIVE ) {
537 $model = new Scan_Item();
538 $model->type = $type;
539 $model->parent_id = $this->id;
540 $model->raw_data = $data;
541 $model->status = $status;
542 $ret = $model->save();
543
544 return $ret;
545 }
546
547 /**
548 * Return current status as readable string.
549 *
550 * @return string
551 */
552 public function get_status_text() {
553 switch ( $this->status ) {
554 case self::STATUS_INIT:
555 return __( 'Initializing...', 'wpdef' );
556 case 'gather_fact':
557 return __( 'Gathering information...', 'wpdef' );
558 case 'core_integrity_check':
559 return __( 'Analyzing WordPress Core...', 'wpdef' );
560 case 'plugin_integrity_check':
561 return __( 'Analyzing WordPress Plugins...', 'wpdef' );
562 case 'vuln_check':
563 return __( 'Checking for any published vulnerabilities in your plugins & themes...', 'wpdef' );
564 case 'suspicious_check':
565 return __( 'Analyzing WordPress Content...', 'wpdef' );
566 default:
567 return __( 'The scan is running', 'wpdef' );
568 }
569 }
570
571 /**
572 * Calculation scan percentage base on the tasks percent.
573 *
574 * @param $task_percent
575 * @param $pos
576 *
577 * @return float
578 */
579 public function calculate_percent( $task_percent, $pos = 1 ) {
580 $task_max = 100 / $this->total_tasks;
581 $task_base = $task_max * ( $pos - 1 );
582 $micro = $task_percent * $task_max / 100;
583 $this->percent = round( $task_base + $micro, 2 );
584 if ( $this->percent > 100 ) {
585 $this->percent = 100;
586 }
587
588 return $this->percent;
589 }
590
591 /**
592 * Get list of whitelisted files.
593 *
594 * @return array
595 */
596 private function whitelisted_files() {
597
598 return array(
599 // Configuration files.
600 'user.ini',
601 'php.ini',
602 'robots.txt',
603 '.htaccess',
604 'nginx.conf',
605 // hidden system files and directories
606 '.well_known',
607 '.idea',
608 '.DS_Store',
609 '.svn',
610 '.git',
611 '.quarantine',
612 '.tmb',
613 );
614 }
615
616 /**
617 * Check if a slug is whitelisted.
618 *
619 * @param string $slug path to file
620 *
621 * @return bool
622 */
623 public function is_issue_whitelisted( $slug ) {
624 $whitelisted_files = $this->whitelisted_files();
625 foreach ( $whitelisted_files as $file ) {
626 if ( false !== stristr( $slug, $file ) ) {
627 return true;
628 }
629 }
630
631 return false;
632 }
633
634 /**
635 * @param array $ignore_lists
636 */
637 public function update_ignore_list( $ignore_lists ) {
638 $ignore_lists = array_unique( $ignore_lists );
639 $ignore_lists = array_filter( $ignore_lists );
640 update_site_option( self::IGNORE_INDEXER, $ignore_lists );
641 }
642
643 /**
644 * Get the threshold time limit as DateTime object.
645 *
646 * @return \DateTime Threshold time limit as DateTime object.
647 */
648 public function threshold_date_time_object() {
649 $timezone = new \DateTimeZone( 'UTC' );
650
651 /**
652 * Filter to override scan threshold period.
653 *
654 * @since 2.6.1
655 *
656 * @link https://www.php.net/manual/en/datetime.formats.relative.php
657 *
658 * @param string $threshold Any valid relative Date and Time formats.
659 */
660 $threshold = apply_filters( 'wd_scan_threshold', self::THRESHOLD_PERIOD );
661
662 return new \DateTime( $threshold, $timezone );
663 }
664
665 /**
666 * Threshold time limit in mysql string format.
667 *
668 * @return string Threshold time limit as mysql string format.
669 */
670 public function threshold_date_time_mysql() {
671 $type = 'Y-m-d H:i:s';
672
673 $threshold_date_time_object = $this->threshold_date_time_object();
674
675 $mysql_format = $threshold_date_time_object->format( $type );
676
677 return $mysql_format;
678 }
679
680 /**
681 * Get the idle scan if any.
682 *
683 * @return self|null
684 */
685 public function get_idle() {
686 $orm = self::get_orm();
687
688 $mysql_date = $this->threshold_date_time_mysql();
689
690 return $orm->get_repository( self::class )
691 ->where( 'status', 'NOT IN', array( self::STATUS_FINISH, self::STATUS_ERROR ) )
692 ->where( 'date_start', '<', $mysql_date )
693 ->first();
694 }
695
696 /**
697 * Delete all idle scan and scan items
698 *
699 * @since 2.6.1
700 */
701 public function delete_idle() {
702 $idle_scans = self::get_orm()
703 ->get_repository( self::class )
704 ->where( 'status', self::STATUS_IDLE )
705 ->get();
706
707 foreach ( $idle_scans as $idle_scan ) {
708 $this->delete( $idle_scan->id );
709 }
710 }
711
712 /**
713 * Verify positive integer or not.
714 *
715 * @since 2.6.1
716 *
717 * @param mixed $id Argument to check for a positive number.
718 *
719 * @return bool Return true on positive integer else false.
720 */
721 private function is_positive_int( $id ) {
722 return is_int( $id ) && $id > 0;
723 }
724 }
725