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 / component / class-scan.php

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

906 lines 25.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handling the scanning process.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use Countable;
11 use ArrayIterator;
12 use WP_Defender\Component;
13 use WP_Plugins_List_Table;
14 use WP_Defender\Model\Scan_Item;
15 use WP_Defender\Behavior\WPMUDEV;
16 use WP_Defender\Model\Scan as Scan_Model;
17 use WP_Defender\Behavior\Scan\Gather_Fact;
18 use WP_Defender\Behavior\Scan\Malware_Scan;
19 use WP_Defender\Behavior\Scan\Core_Integrity;
20 use WP_Defender\Behavior\Scan\Plugin_Integrity;
21 use WP_Defender\Behavior\Scan\Malware_Deep_Scan;
22 use WP_Defender\Behavior\Scan\Malware_Quick_Scan;
23 use WP_Defender\Behavior\Scan\Known_Vulnerability;
24 use WP_Defender\Behavior\Scan\Abandoned_Plugin;
25 use WP_Defender\Model\Setting\Scan as Scan_Settings;
26 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
27 use WP_Defender\Controller\Scan as Scan_Controller;
28
29 /**
30 * The Scan class handles the scanning process, managing tasks, and coordinating different types of scans.
31 */
32 class Scan extends Component {
33 use \WP_Defender\Traits\Plugin;
34
35 // For all Scan types where plugins are used.
36 public const PLUGINS_ACTIONED = 'wp-defender-actioned-plugins';
37
38 /**
39 * The current scan model.
40 *
41 * @var Scan_Model
42 */
43 public $scan;
44
45 /**
46 * Scan settings model.
47 *
48 * @var Scan_Settings
49 */
50 public $settings;
51
52 /**
53 * Details of vulnerabilities found during the scan.
54 *
55 * @var array
56 */
57 protected $vulnerability_details = array();
58
59 /**
60 * Instance of Known_Vulnerability to handle known vulnerability checks.
61 *
62 * @var Known_Vulnerability
63 */
64 private $known_vulnerability;
65
66 /**
67 * Instance of Malware_Scan to handle malware scanning.
68 *
69 * @var Malware_Scan
70 */
71 private $malware_scan;
72
73 /**
74 * Instance of Gather_Fact to gather necessary information before scanning.
75 *
76 * @var Gather_Fact|null
77 */
78 private ?Gather_Fact $gather_fact;
79
80 /**
81 * Instance of Abandoned_Plugin to handle abandoned plugin checks.
82 *
83 * @var Abandoned_Plugin
84 */
85 private $abandoned_plugin;
86
87 /**
88 * Lock file name for scanning.
89 *
90 * @var string
91 */
92 protected string $lock_filename = 'scan.lock';
93
94 /**
95 * Indicate whether the current installation is a pro version.
96 *
97 * @var bool
98 */
99 private $is_pro;
100
101 /**
102 * Constructs the Scan object and initializes behaviors.
103 */
104 public function __construct() {
105 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
106 $this->attach_behavior( Core_Integrity::class, Core_Integrity::class );
107 $this->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
108 $this->is_pro = wd_di()->get( WPMUDEV::class )->is_pro();
109 $this->settings = wd_di()->get( Scan_Settings::class );
110 }
111
112 /**
113 * Performs additional actions after an advanced scan.
114 *
115 * @param Scan_Model $model The scan model.
116 */
117 public function advanced_scan_actions( $model ) {
118 $this->reindex_ignored_issues( $model );
119 $this->clean_up();
120
121 if ( defender_is_wp_org_version() ) {
122 Rate::run_counter_of_completed_scans();
123 }
124 }
125
126 /**
127 * Process current scan.
128 *
129 * @return bool|int
130 */
131 public function process() {
132 $scan = Scan_Model::get_active();
133 if ( ! is_object( $scan ) ) {
134 // This case can be a scan get cancel.
135 return - 1;
136 }
137 $this->scan = $scan;
138 $tasks = $this->get_tasks();
139 $runner = new ArrayIterator( $tasks );
140 $task = $this->scan->status;
141 if ( Scan_Model::STATUS_INIT === $scan->status ) {
142 // Get the first.
143 $this->log( 'Prepare facts for a scan', Scan_Controller::SCAN_LOG );
144 $task = Scan_Model::STEP_GATHER_INFO;
145 $this->scan->percent = 0;
146 $this->scan->total_tasks = $runner->count();
147 $this->scan->save();
148 }
149 if (
150 in_array(
151 $this->scan->status,
152 array(
153 Scan_Model::STATUS_ERROR,
154 Scan_Model::STATUS_IDLE,
155 ),
156 true
157 )
158 ) {
159 // Stop and return true to abort the process.
160 return true;
161 }
162 // Find the current task.
163 $offset = array_search( $task, array_values( $tasks ), true );
164 if ( false === $offset ) {
165 $this->log( sprintf( 'offset is not found, search %s', $task ), Scan_Controller::SCAN_LOG );
166
167 return false;
168 }
169 // Reset the tasks to current.
170 $runner->seek( $offset );
171 $this->log( sprintf( 'Current task %s', $runner->current() ), Scan_Controller::SCAN_LOG );
172 if ( $this->has_method( $task ) ) {
173 $this->log( sprintf( 'processing %s', $runner->key() ), Scan_Controller::SCAN_LOG );
174 $result = $this->task_handler( $task );
175 if ( true === $result ) {
176 $this->log( sprintf( 'task %s processed', $runner->key() ), Scan_Controller::SCAN_LOG );
177 // Task is done, move to next.
178 $runner->next();
179 if ( $runner->valid() ) {
180 $this->log( sprintf( 'queue %s for next', $runner->key() ), Scan_Controller::SCAN_LOG );
181 $this->scan->status = $runner->key();
182 $this->scan->task_checkpoint = '';
183 $this->scan->date_end = gmdate( 'Y-m-d H:i:s' );
184 $this->scan->save();
185 // Queue for next run.
186 return false;
187 }
188 $this->log( 'All done!', Scan_Controller::SCAN_LOG );
189 // No more task in the queue, we are done.
190 $this->scan->status = Scan_Model::STATUS_FINISH;
191 $this->scan->save();
192 $this->advanced_scan_actions( $this->scan );
193 do_action( 'defender_notify', 'malware-notification', $this->scan );
194
195 return true;
196 }
197 $this->scan->status = $task;
198 $this->scan->save();
199 }
200
201 return false;
202 }
203
204 /**
205 * Reindex ignored issues to update their status in the scan model.
206 *
207 * @param Scan_Model $model The scan model.
208 */
209 private function reindex_ignored_issues( $model ) {
210 $issues = $model->get_issues( null, Scan_Item::STATUS_IGNORE );
211 $ignore_lists = array();
212 foreach ( $issues as $issue ) {
213 $data = $issue->raw_data;
214 if ( isset( $data['file'] ) ) {
215 $ignore_lists[] = $data['file'];
216 } elseif ( isset( $data['slug'] ) ) {
217 $ignore_lists[] = $data['slug'];
218 }
219 }
220 $model->update_ignore_list( $ignore_lists );
221 }
222
223 /**
224 * Get a list of tasks will run in a scan.
225 *
226 * @return array
227 */
228 public function get_tasks(): array {
229 $tasks = array( Scan_Model::STEP_GATHER_INFO => 'gather_info' );
230 if ( $this->settings->integrity_check ) {
231 // Nested options.
232 if ( $this->settings->check_core ) {
233 $tasks[ Scan_Model::STEP_CHECK_CORE ] = 'core_integrity_check';
234 }
235 if ( $this->settings->check_plugins ) {
236 $tasks[ Scan_Model::STEP_CHECK_PLUGIN ] = 'plugin_integrity_check';
237 }
238 }
239
240 if ( $this->settings->check_abandoned_plugin ) {
241 $tasks[ Scan_Model::STEP_ABANDONED_PLUGIN_CHECK ] = 'abandoned_plugin_check';
242 }
243 if ( $this->is_pro ) {
244 if ( $this->settings->check_known_vuln && $this->has_method( Scan_Model::STEP_VULN_CHECK ) ) {
245 $tasks[ Scan_Model::STEP_VULN_CHECK ] = 'vuln_check';
246 }
247 if ( $this->settings->scan_malware && $this->has_method( Scan_Model::STEP_SUSPICIOUS_CHECK ) ) {
248 $tasks[ Scan_Model::STEP_SUSPICIOUS_CHECK ] = 'suspicious_check';
249 }
250 }
251
252 return $tasks;
253 }
254
255 /**
256 * Handles individual scan tasks based on the task identifier.
257 *
258 * @param string $task The task identifier.
259 *
260 * @return bool Returns true if the task was handled successfully.
261 */
262 private function task_handler( $task ) {
263 switch ( $task ) {
264 case 'gather_info':
265 if ( empty( $this->gather_fact ) && class_exists( Gather_Fact::class ) ) {
266 $this->set_gather_fact(
267 wd_di()->make( Gather_Fact::class, array( 'scan' => $this->scan ) )
268 );
269 }
270
271 return $this->gather_info( $this->gather_fact );
272 case 'vuln_check':
273 if ( empty( $this->known_vulnerability ) && class_exists( Known_Vulnerability::class ) ) {
274 $this->set_known_vulnerability(
275 wd_di()->make( Known_Vulnerability::class, array( 'scan' => $this->scan ) )
276 );
277 }
278
279 return $this->vuln_check( $this->known_vulnerability );
280 case 'suspicious_check':
281 if ( class_exists( Malware_Scan::class ) ) {
282 $this->set_malware_scan(
283 wd_di()->make( Malware_Scan::class, array( 'scan' => $this->scan ) )
284 );
285 }
286
287 return $this->suspicious_check( $this->malware_scan );
288 case 'abandoned_plugin_check':
289 if ( empty( $this->abandoned_plugin ) && class_exists( Abandoned_Plugin::class ) ) {
290 $this->set_abandoned_plugin(
291 wd_di()->make( Abandoned_Plugin::class, array( 'scan' => $this->scan ) )
292 );
293 }
294
295 return $this->abandoned_plugin_check( $this->abandoned_plugin );
296 default:
297 return $this->$task();
298 }
299 }
300
301 /**
302 * A wrapper method for Known_Vulnerability class method vuln_check.
303 *
304 * @param Known_Vulnerability $known_vulnerability An instance of Known_Vulnerability.
305 *
306 * @return bool True always as in wrapped method Known_Vulnerability::vuln_check.
307 */
308 private function vuln_check( Known_Vulnerability $known_vulnerability ): bool {
309 if ( method_exists( $known_vulnerability, 'vuln_check' ) ) {
310 return $known_vulnerability->vuln_check();
311 }
312
313 return true; // Followed Known_Vulnerability::vuln_check return pattern i.e. always true for skipped vuln check.
314 }
315
316 /**
317 * Setter injection method for Known_Vulnerability instance.
318 *
319 * @param Known_Vulnerability $known_vulnerability The Known_Vulnerability instance to set.
320 */
321 public function set_known_vulnerability( Known_Vulnerability $known_vulnerability ) {
322 if ( class_exists( Known_Vulnerability::class ) ) {
323 $this->known_vulnerability = $known_vulnerability;
324 }
325 }
326
327 /**
328 * A wrapper method for Malware_Scan class method vuln_check.
329 *
330 * @param Malware_Scan $malware_scan An instance of Malware_Scan.
331 *
332 * @return bool True if method Malware_Scan::suspicious_check not exists else bool value returned by that method.
333 */
334 private function suspicious_check( Malware_Scan $malware_scan ): bool {
335 if ( method_exists( $malware_scan, 'suspicious_check' ) ) {
336 $quick_scan = wd_di()->get( Malware_Quick_Scan::class );
337 $deep_scan = wd_di()->get( Malware_Deep_Scan::class );
338
339 return $malware_scan->suspicious_check( $quick_scan, $deep_scan );
340 }
341
342 return true;
343 }
344
345 /**
346 * Setter injection method for Malware_Scan instance.
347 *
348 * @param Malware_Scan $malware_scan The Malware_Scan instance to set.
349 */
350 public function set_malware_scan( Malware_Scan $malware_scan ) {
351 if ( class_exists( Malware_Scan::class ) ) {
352 $this->malware_scan = $malware_scan;
353 }
354 }
355
356 /**
357 * Set the Abandoned_Plugin object.
358 *
359 * @param Abandoned_Plugin $abandoned_plugin The Abandoned_Plugin object to set.
360 */
361 public function set_abandoned_plugin( Abandoned_Plugin $abandoned_plugin ) {
362 if ( class_exists( Abandoned_Plugin::class ) ) {
363 $this->abandoned_plugin = $abandoned_plugin;
364 }
365 }
366
367 /**
368 * A wrapper method for Abandoned_Plugin class method abandoned_plugin_check.
369 *
370 * @param Abandoned_Plugin $abandoned_plugin An instance of Abandoned_Plugin.
371 *
372 * @return bool
373 */
374 private function abandoned_plugin_check( Abandoned_Plugin $abandoned_plugin ): bool {
375 if ( method_exists( $abandoned_plugin, 'abandoned_plugin_check' ) ) {
376 return $abandoned_plugin->abandoned_plugin_check();
377 }
378
379 return true;
380 }
381
382 /**
383 * Cancels an active scan and cleans up related data.
384 */
385 public function cancel_a_scan() {
386 $scan = Scan_Model::get_active();
387 if ( is_object( $scan ) ) {
388 $scan->delete();
389 }
390 $this->clean_up();
391 $this->remove_lock();
392
393 $scan_analytics = wd_di()->get( Scan_Analytics::class );
394
395 $scan_analytics->track_feature(
396 $scan_analytics::EVENT_SCAN_FAILED,
397 array(
398 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_CANCEL,
399 )
400 );
401 }
402
403 /**
404 * Track the event if we have a failed checksum.
405 */
406 public function maybe_track_failed_checksum() {
407 // If there is a Checksum issue, then check DB's value from time().
408 $checksum_issue = (int) get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 0 );
409 if ( $checksum_issue > 0 ) {
410 $scan_analytics = wd_di()->get( Scan_Analytics::class );
411 $reason = 'Failed to fetch checksums from wp.org';
412
413 $scan_analytics->track_feature(
414 $scan_analytics::EVENT_SCAN_FAILED,
415 array(
416 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
417 'Error_Reason' => $reason,
418 )
419 );
420
421 $this->log( $reason, Scan_Controller::SCAN_LOG );
422 }
423 }
424
425 /**
426 * Clean up data generate by current scan.
427 */
428 public function clean_up() {
429 $this->delete_interim_data();
430
431 $models = Scan_Model::get_last_all();
432 if ( ! empty( $models ) ) {
433 // Remove the latest. Don't remove code to find the first value.
434 $current = array_shift( $models );
435 foreach ( $models as $model ) {
436 $model->delete();
437 }
438 }
439 }
440
441 /**
442 * Get the total scanning active issues.
443 *
444 * @return int $count
445 */
446 public function indicator_issue_count(): int {
447 $count = 0;
448 $scan = Scan_Model::get_last();
449 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
450 // Only Scan issues.
451 $count = (int) $scan->count( null, Scan_Item::STATUS_ACTIVE );
452 }
453
454 return $count;
455 }
456
457 /**
458 * Checks if any scan type is active based on the scan settings and the user's membership status.
459 *
460 * @param array $scan_settings The scan settings.
461 *
462 * @return bool Returns true if any scan type is active, false otherwise.
463 */
464 public function is_any_scan_active( $scan_settings ): bool {
465 if ( empty( $scan_settings['integrity_check'] ) ) {
466 // Check the parent type.
467 $file_change_check = false;
468 } elseif (
469 $scan_settings['integrity_check']
470 && empty( $scan_settings['check_core'] )
471 && empty( $scan_settings['check_plugins'] )
472 ) {
473 // Check the parent and child types.
474 $file_change_check = false;
475 } else {
476 $file_change_check = true;
477 }
478 // Similar to is_any_active(...) method from the controller.
479 if ( $this->is_pro ) {
480 // Pro version. Check all parent types.
481 return $file_change_check || ! empty( $scan_settings['check_known_vuln'] )
482 || ! empty( $scan_settings['scan_malware'] );
483 } else {
484 // Free version.
485 return $file_change_check || ! empty( $scan_settings['check_abandoned_plugin'] );
486 }
487 }
488
489 /**
490 * Update the idle scan status due the time limit.
491 *
492 * @since 2.6.1
493 */
494 public function update_idle_scan_status() {
495 $idle_scan = wd_di()->get( Scan_Model::class )->get_idle();
496
497 if ( is_object( $idle_scan ) ) {
498 $ready_to_send = false;
499 if ( Scan_Model::STATUS_IDLE === $idle_scan->status ) {
500 $ready_to_send = true;
501 }
502 $this->delete_interim_data();
503
504 as_unschedule_all_actions( 'defender/async_scan' );
505
506 $idle_scan->status = Scan_Model::STATUS_IDLE;
507 $idle_scan->task_checkpoint = 'time_limit';
508 $idle_scan->save();
509
510 $this->remove_lock();
511 if ( $ready_to_send ) {
512 do_action( 'defender_notify', 'malware-notification', $idle_scan );
513 }
514 }
515 }
516
517 /**
518 * Update the idle scan status due the checksum issue.
519 *
520 * @param object $scan The current scan.
521 *
522 * @since 4.9.0
523 */
524 public function update_idle_scan_status_by_checksum_issue( $scan ) {
525 $ready_to_send = false;
526 if ( Scan_Model::STATUS_IDLE === $scan->status ) {
527 $ready_to_send = true;
528 }
529 $this->delete_interim_data();
530
531 as_unschedule_all_actions( 'defender/async_scan' );
532
533 $scan->status = Scan_Model::STATUS_IDLE;
534 $scan->task_checkpoint = 'checksum_issue';
535 $scan->save();
536
537 $this->remove_lock();
538 if ( $ready_to_send ) {
539 do_action( 'defender_notify', 'malware-notification', $scan );
540 }
541 }
542
543 /**
544 * Clear all temporary scan data.
545 *
546 * @since 2.6.1
547 */
548 private function delete_interim_data() {
549 delete_site_option( Gather_Fact::CACHE_CORE );
550 delete_site_option( Gather_Fact::CACHE_CONTENT );
551 delete_site_option( Malware_Scan::YARA_RULES );
552 delete_site_option( Core_Integrity::CACHE_CHECKSUMS );
553 delete_site_option( Plugin_Integrity::PLUGIN_SLUGS );
554 delete_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS );
555 delete_site_option( self::PLUGINS_ACTIONED );
556 $this->maybe_track_failed_checksum();
557 }
558
559 /**
560 * Display styles on the Plugins page.
561 */
562 public function show_plugin_admin_styles() {
563 $custom_css = '.vulnerability-indent{ padding-left: 26px; }
564 .plugins .plugin-update-tr .plugin-update.plugin-vulnerability{box-shadow: inset 0 0px 0 rgb(0 0 0 / 10%);
565 border-bottom: rgb(0 0 0 / 10%) solid 1px;}';
566 wp_add_inline_style( 'defender-menu', $custom_css );
567 }
568
569 /**
570 * Display update information for a plugin.
571 *
572 * @param string $file Plugin basename.
573 * @param array $plugin_data Plugin information.
574 *
575 * @return void
576 */
577 public function attach_plugin_vulnerability_warning( $file, $plugin_data ) {
578 /**
579 * WP Plugin list table instance.
580 *
581 * @var WP_Plugins_List_Table $wp_list_table
582 */
583 $wp_list_table = _get_list_table(
584 'WP_Plugins_List_Table',
585 array( 'screen' => get_current_screen() )
586 );
587 $bugs = $this->vulnerability_details[ $file ]['bugs'];
588 if ( empty( $bugs ) ) {
589 return;
590 }
591 $last_fixed_in = '0';
592 // Check if there have been updates since the last scan.
593 $exist_update = true;
594 if ( isset( $plugin_data['Version'] ) && ! empty( $plugin_data['Version'] ) ) {
595 // The current plugin version.
596 $current_version = $plugin_data['Version'];
597 foreach ( $bugs as $bug_details ) {
598 // If the fixed version is existed then get the latest one.
599 if ( isset( $bug_details['fixed_in'] ) && ! empty( $bug_details['fixed_in'] )
600 && version_compare( $bug_details['fixed_in'], $last_fixed_in, '>' )
601 ) {
602 $last_fixed_in = $bug_details['fixed_in'];
603 }
604 }
605 if ( version_compare( $last_fixed_in, $current_version, '>' ) ) {
606 $exist_update = false;
607 }
608 }
609 // If there were updates, do not display notice.
610 if ( $exist_update ) {
611 return;
612 }
613 if ( empty( $plugin_data['slug'] ) && isset( $this->vulnerability_details[ $file ]['base_slug'] ) ) {
614 $plugin_data['slug'] = $this->vulnerability_details[ $file ]['base_slug'];
615 }
616
617 if ( is_network_admin() || ! is_multisite() ) {
618 if ( is_network_admin() ) {
619 $active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
620 } else {
621 $active_class = is_plugin_active( $file ) ? ' active' : '';
622 }
623
624 printf(
625 '<tr class="plugin-update-tr%s" id="vulnerability-%s" data-slug="%s" data-plugin="%s"><td colspan="%s" class="plugin-update colspanchange plugin-vulnerability"><div class="update-message notice inline %s notice-alt"><p>',
626 esc_attr( $active_class ),
627 esc_attr( $plugin_data['slug'] ),
628 esc_attr( $plugin_data['slug'] ),
629 esc_attr( $file ),
630 esc_attr( $wp_list_table->get_column_count() ),
631 'notice-error'
632 );
633
634 $notice = sprintf(
635 /* translators: %s - Plugin name. */
636 esc_html__(
637 '%s has detected a vulnerability in this plugin that may cause harm to your site.',
638 'defender-security'
639 ),
640 '<b>' . esc_html__( 'Defender Pro', 'defender-security' ) . '</b>'
641 );
642 if ( ( is_array( $bugs ) || $bugs instanceof Countable ? count( $bugs ) : 0 ) > 1 ) {
643 $notice .= '<hr/>';
644 $lines = array();
645 foreach ( $bugs as $bug ) {
646 $lines[] = '<span class="vulnerability-indent"></span>' . $bug['title'];
647 }
648 $notice .= implode( '<br/>', $lines );
649 $notice .= '<hr/><span class="vulnerability-indent"></span>';
650 if ( '0' !== $last_fixed_in ) {
651 $notice .= sprintf(
652 /* translators: %s - Version number. */
653 esc_html__(
654 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.',
655 'defender-security'
656 ),
657 $last_fixed_in
658 );
659 } else {
660 $notice .= esc_html__(
661 'Important! We recommend that you deactivate this plugin until the vulnerability has been fixed.',
662 'defender-security'
663 );
664 }
665 } else {
666 $notice .= '<br/><span class="vulnerability-indent"></span>' . $bugs[0]['title'] . '<br/><span class="vulnerability-indent"></span>';
667 $notice .= empty( $last_fixed_in )
668 ? esc_html__(
669 'We recommend that you deactivate this plugin until the vulnerability has been fixed.',
670 'defender-security'
671 )
672 : sprintf(
673 /* translators: 1: Version number. */
674 esc_html__(
675 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.',
676 'defender-security'
677 ),
678 $last_fixed_in
679 );
680 }
681
682 printf( wp_kses_post( $notice ) );
683 }
684 }
685
686 /**
687 * Display warnings.
688 *
689 * @since 2.6.2
690 */
691 public function display_vulnerability_warnings() {
692 if ( ! current_user_can( 'update_plugins' ) ) {
693 return;
694 }
695
696 $last = Scan_Model::get_last();
697 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
698 $vulnerability_issues = $last->get_issues( Scan_Item::TYPE_VULNERABILITY );
699 if ( empty( $vulnerability_issues ) ) {
700 return;
701 }
702
703 add_action( 'admin_print_styles-plugins.php', array( $this, 'show_plugin_admin_styles' ) );
704 // Vulnerability list.
705 foreach ( $vulnerability_issues as $vulnerability_obj ) {
706 $plugin_slug = $vulnerability_obj->raw_data['slug'];
707 // Get the details so that you can apply them later for each plugin.
708 $this->vulnerability_details[ $plugin_slug ] = $vulnerability_obj->raw_data;
709 add_action(
710 "after_plugin_row_$plugin_slug",
711 array(
712 $this,
713 'attach_plugin_vulnerability_warning',
714 ),
715 100,
716 2
717 );
718 }
719 }
720 }
721
722 /**
723 * Clear completed action scheduler logs.
724 *
725 * @since 2.6.5
726 */
727 public static function clear_logs() {
728 global $wpdb;
729
730 $table_actions = ! empty( $wpdb->actionscheduler_actions ) ?
731 $wpdb->actionscheduler_actions :
732 $wpdb->prefix . 'actionscheduler_actions';
733 $table_logs = ! empty( $wpdb->actionscheduler_logs ) ?
734 $wpdb->actionscheduler_logs :
735 $wpdb->prefix . 'actionscheduler_logs';
736
737 $table_count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
738 $wpdb->prepare(
739 'SELECT count(*)
740 FROM information_schema.tables
741 WHERE table_schema = %s AND table_name IN (%s, %s);',
742 $wpdb->dbname,
743 $table_actions,
744 $table_logs
745 )
746 );
747
748 if ( 2 !== $table_count ) {
749 return array( 'error' => esc_html__( 'Action scheduler is not setup', 'defender-security' ) );
750 }
751
752 $hook = 'defender/async_scan';
753 $status = 'complete';
754 $limit = 100;
755 $action_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
756 $wpdb->prepare(
757 "SELECT action_id FROM {$table_actions} as_actions WHERE as_actions.hook = %s AND as_actions.status = %s LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
758 $hook,
759 $status,
760 $limit
761 )
762 );
763 while ( $action_ids ) {
764 if ( empty( $action_ids ) ) {
765 break;
766 }
767
768 $query = "DELETE as_actions, as_logs FROM {$table_actions} as_actions LEFT JOIN {$table_logs} as_logs ON as_actions.action_id = as_logs.action_id WHERE as_actions.action_id IN ( " . implode(
769 ', ',
770 array_fill(
771 0,
772 is_array( $action_ids ) || $action_ids instanceof Countable ? count( $action_ids ) : 0,
773 '%s'
774 )
775 ) . ' )';
776 $query = call_user_func_array(
777 array( $wpdb, 'prepare' ),
778 array_merge(
779 array( $query ),
780 $action_ids
781 )
782 );
783 // SQL is prepared here, so we will ignore WordPress.DB.PreparedSQL.NotPrepared.
784 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
785
786 $action_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
787 $wpdb->prepare(
788 "SELECT action_id FROM {$table_actions} as_actions WHERE as_actions.hook = %s AND as_actions.status = %s LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
789 $hook,
790 $status,
791 $limit
792 )
793 );
794 }
795
796 return array( 'success' => esc_html__( 'Malware scan logs are cleared', 'defender-security' ) );
797 }
798
799 /**
800 * Set the Gather_Fact object.
801 *
802 * @param Gather_Fact $gather_fact The Gather_Fact object to set.
803 */
804 public function set_gather_fact( Gather_Fact $gather_fact ) {
805 if ( class_exists( Gather_Fact::class ) ) {
806 $this->gather_fact = $gather_fact;
807 }
808 }
809
810 /**
811 * Gathers information using the Gather_Fact class.
812 *
813 * @param Gather_Fact $gather_fact The Gather_Fact instance.
814 *
815 * @return bool Returns true if information gathering is successful.
816 */
817 public function gather_info( Gather_Fact $gather_fact ): bool {
818 if ( method_exists( $gather_fact, 'gather_info' ) ) {
819 return $gather_fact->gather_info();
820 }
821
822 return true;
823 }
824
825 /**
826 * Gey intentions.
827 *
828 * @since 4.11.0
829 * @return array
830 */
831 public static function get_intentions(): array {
832 return array(
833 'resolve',
834 'ignore',
835 'delete',
836 'unignore',
837 'quarantine',
838 );
839 }
840
841 /**
842 * Get the list of actioned plugins taking into account the Ignored and Excluded.
843 *
844 * @return array
845 */
846 public function gather_actioned_plugin_details(): array {
847 $cache = get_site_option( self::PLUGINS_ACTIONED );
848 if ( self::are_actioned_plugins( $cache ) ) {
849 return $cache;
850 }
851
852 $items = array();
853 $is_plugin_used = false;
854 if ( $this->settings->integrity_check && $this->settings->check_plugins ) {
855 $is_plugin_used = true;
856 } elseif ( $this->settings->check_abandoned_plugin ) {
857 $is_plugin_used = true;
858 } elseif ( $this->is_pro && ( $this->settings->check_known_vuln || $this->settings->scan_malware ) ) {
859 $is_plugin_used = true;
860 }
861
862 if ( $is_plugin_used ) {
863 $model = Scan_Model::get_last();
864 /**
865 * Exclude plugin slugs.
866 *
867 * @param array $slugs Slugs of excluded plugins.
868 *
869 * @since 3.1.0
870 */
871 $excluded_slugs = (array) apply_filters( 'wd_scan_excluded_plugin_slugs', array() );
872
873 foreach ( $this->get_plugins() as $slug => $item ) {
874 if ( is_object( $model ) && $model->is_issue_ignored( $slug ) ) {
875 continue;
876 }
877 $base_slug = $this->get_plugin_slug_by( $slug );
878 if ( in_array( $base_slug, $excluded_slugs, true ) ) {
879 continue;
880 }
881 // Use keys with the first capital letter to match default plugin header keys.
882 $items[ $base_slug ] = array(
883 'Name' => $item['Name'],
884 'Version' => $item['Version'],
885 'Slug' => $slug,
886 );
887 }
888
889 update_site_option( self::PLUGINS_ACTIONED, $items );
890 }
891
892 return $items;
893 }
894
895 /**
896 * Does the list of actioned plugins exist and no empty?
897 *
898 * @param array|false $actioned_plugins The actioned plugins.
899 *
900 * @return bool
901 */
902 public static function are_actioned_plugins( $actioned_plugins ): bool {
903 return is_array( $actioned_plugins ) && array() !== $actioned_plugins;
904 }
905 }
906