PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.0
Defender Security – Malware Scanner, Login Security & Firewall v5.5.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.5.0, at src/component/class-scan.php

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