PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-scan.php

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

918 lines 26.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 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 = null;
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 // The division between PHP and JS files occurs when collecting files.
248 if ( $this->settings->scan_malware && $this->has_method( Scan_Model::STEP_SUSPICIOUS_CHECK )
249 ) {
250 $tasks[ Scan_Model::STEP_SUSPICIOUS_CHECK ] = 'suspicious_check';
251 }
252 }
253
254 return $tasks;
255 }
256
257 /**
258 * Handles individual scan tasks based on the task identifier.
259 *
260 * @param string $task The task identifier.
261 *
262 * @return bool Returns true if the task was handled successfully.
263 */
264 private function task_handler( $task ) {
265 switch ( $task ) {
266 case 'gather_info':
267 if ( ! ( $this->gather_fact instanceof Gather_Fact ) && class_exists( Gather_Fact::class ) ) {
268 $this->set_gather_fact(
269 wd_di()->make( Gather_Fact::class, array( 'scan' => $this->scan ) )
270 );
271 }
272
273 return $this->gather_info( $this->gather_fact );
274 case 'vuln_check':
275 if ( ! ( $this->known_vulnerability instanceof Known_Vulnerability ) && class_exists( Known_Vulnerability::class ) ) {
276 $this->set_known_vulnerability(
277 wd_di()->make( Known_Vulnerability::class, array( 'scan' => $this->scan ) )
278 );
279 }
280
281 return $this->vuln_check( $this->known_vulnerability );
282 case 'suspicious_check':
283 if ( class_exists( Malware_Scan::class ) ) {
284 $this->set_malware_scan(
285 wd_di()->make( Malware_Scan::class, array( 'scan' => $this->scan ) )
286 );
287 }
288
289 return $this->suspicious_check( $this->malware_scan );
290 case 'abandoned_plugin_check':
291 if ( ! ( $this->abandoned_plugin instanceof Abandoned_Plugin ) && class_exists( Abandoned_Plugin::class ) ) {
292 $this->set_abandoned_plugin(
293 wd_di()->make( Abandoned_Plugin::class, array( 'scan' => $this->scan ) )
294 );
295 }
296
297 return $this->abandoned_plugin_check( $this->abandoned_plugin );
298 default:
299 return $this->$task();
300 }
301 }
302
303 /**
304 * A wrapper method for Known_Vulnerability class method vuln_check.
305 *
306 * @param Known_Vulnerability $known_vulnerability An instance of Known_Vulnerability.
307 *
308 * @return bool True always as in wrapped method Known_Vulnerability::vuln_check.
309 */
310 private function vuln_check( Known_Vulnerability $known_vulnerability ): bool {
311 if ( method_exists( $known_vulnerability, 'vuln_check' ) ) {
312 return $known_vulnerability->vuln_check();
313 }
314
315 return true; // Followed Known_Vulnerability::vuln_check return pattern i.e. always true for skipped vuln check.
316 }
317
318 /**
319 * Setter injection method for Known_Vulnerability instance.
320 *
321 * @param Known_Vulnerability $known_vulnerability The Known_Vulnerability instance to set.
322 */
323 public function set_known_vulnerability( Known_Vulnerability $known_vulnerability ) {
324 if ( class_exists( Known_Vulnerability::class ) ) {
325 $this->known_vulnerability = $known_vulnerability;
326 }
327 }
328
329 /**
330 * A wrapper method for Malware_Scan class method suspicious_check.
331 *
332 * @param Malware_Scan $malware_scan An instance of Malware_Scan.
333 *
334 * @return bool True if method Malware_Scan::suspicious_check not exists else bool value returned by that method.
335 */
336 private function suspicious_check( Malware_Scan $malware_scan ): bool {
337 if ( method_exists( $malware_scan, 'suspicious_check' ) ) {
338 $quick_scan = wd_di()->get( Malware_Quick_Scan::class );
339 $deep_scan = wd_di()->get( Malware_Deep_Scan::class );
340
341 return $malware_scan->suspicious_check( $quick_scan, $deep_scan );
342 }
343
344 return true;
345 }
346
347 /**
348 * Setter injection method for Malware_Scan instance.
349 *
350 * @param Malware_Scan $malware_scan The Malware_Scan instance to set.
351 */
352 public function set_malware_scan( Malware_Scan $malware_scan ) {
353 if ( class_exists( Malware_Scan::class ) ) {
354 $this->malware_scan = $malware_scan;
355 }
356 }
357
358 /**
359 * Set the Abandoned_Plugin object.
360 *
361 * @param Abandoned_Plugin $abandoned_plugin The Abandoned_Plugin object to set.
362 */
363 public function set_abandoned_plugin( Abandoned_Plugin $abandoned_plugin ) {
364 if ( class_exists( Abandoned_Plugin::class ) ) {
365 $this->abandoned_plugin = $abandoned_plugin;
366 }
367 }
368
369 /**
370 * A wrapper method for Abandoned_Plugin class method abandoned_plugin_check.
371 *
372 * @param Abandoned_Plugin $abandoned_plugin An instance of Abandoned_Plugin.
373 *
374 * @return bool
375 */
376 private function abandoned_plugin_check( Abandoned_Plugin $abandoned_plugin ): bool {
377 if ( method_exists( $abandoned_plugin, 'abandoned_plugin_check' ) ) {
378 return $abandoned_plugin->abandoned_plugin_check();
379 }
380
381 return true;
382 }
383
384 /**
385 * Cancels an active scan and cleans up related data.
386 */
387 public function cancel_a_scan() {
388 $scan = Scan_Model::get_active();
389 if ( is_object( $scan ) ) {
390 $scan->delete();
391 }
392 $this->clean_up();
393 $this->remove_lock( $this->lock_filename );
394
395 $scan_analytics = wd_di()->get( Scan_Analytics::class );
396
397 $scan_analytics->track_feature(
398 $scan_analytics::EVENT_SCAN_FAILED,
399 array(
400 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_CANCEL,
401 )
402 );
403 }
404
405 /**
406 * Track the event if we have a failed checksum.
407 */
408 public function maybe_track_failed_checksum() {
409 // If there is a Checksum issue, then check DB's value from time().
410 $checksum_issue = (int) get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 0 );
411 if ( $checksum_issue > 0 ) {
412 $scan_analytics = wd_di()->get( Scan_Analytics::class );
413 $reason = 'Failed to fetch checksums from wp.org';
414
415 $scan_analytics->track_feature(
416 $scan_analytics::EVENT_SCAN_FAILED,
417 array(
418 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
419 'Error_Reason' => $reason,
420 )
421 );
422
423 $this->log( $reason, Scan_Controller::SCAN_LOG );
424 }
425 }
426
427 /**
428 * Clean up data generate by current scan.
429 */
430 public function clean_up() {
431 $this->delete_interim_data();
432
433 $models = Scan_Model::get_last_all();
434 if ( is_array( $models ) && array() !== $models ) {
435 // Remove the latest. Don't remove code to find the first value.
436 $current = array_shift( $models );
437 foreach ( $models as $model ) {
438 $model->delete();
439 }
440 }
441 }
442
443 /**
444 * Get the total scanning active issues.
445 *
446 * @return int $count
447 */
448 public function indicator_issue_count(): int {
449 $count = 0;
450 $scan = Scan_Model::get_last();
451 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
452 // Only Scan issues.
453 $count = (int) $scan->count( null, Scan_Item::STATUS_ACTIVE );
454 }
455
456 return $count;
457 }
458
459 /**
460 * Checks if any scan type is active based on the scan settings and the user's membership status.
461 *
462 * @param array $scan_settings The scan settings.
463 *
464 * @return bool Returns true if any scan type is active, false otherwise.
465 */
466 public function is_any_scan_active( $scan_settings ): bool {
467 if ( ! isset( $scan_settings['integrity_check'] ) || ! $scan_settings['integrity_check'] ) {
468 // Check the parent type.
469 $file_change_check = false;
470 } elseif (
471 $scan_settings['integrity_check']
472 && ( ! isset( $scan_settings['check_core'] ) || ! $scan_settings['check_core'] )
473 && ( ! isset( $scan_settings['check_plugins'] ) || ! $scan_settings['check_plugins'] )
474 ) {
475 // Check the parent and child types.
476 $file_change_check = false;
477 } else {
478 $file_change_check = true;
479 }
480 // Similar to is_any_active(...) method from the controller.
481 if ( $this->is_pro ) {
482 // Pro version. Check all parent types.
483 return $file_change_check || ( isset( $scan_settings['check_known_vuln'] ) && $scan_settings['check_known_vuln'] )
484 || ( isset( $scan_settings['scan_malware'] ) && $scan_settings['scan_malware'] );
485 } else {
486 // Free version.
487 return $file_change_check || ( isset( $scan_settings['check_abandoned_plugin'] ) && $scan_settings['check_abandoned_plugin'] );
488 }
489 }
490
491 /**
492 * Update the idle scan status due the time limit.
493 *
494 * @since 2.6.1
495 */
496 public function update_idle_scan_status() {
497 $idle_scan = wd_di()->get( Scan_Model::class )->get_idle();
498
499 if ( is_object( $idle_scan ) ) {
500 $ready_to_send = false;
501 if ( Scan_Model::STATUS_IDLE === $idle_scan->status ) {
502 $ready_to_send = true;
503 }
504 $this->delete_interim_data();
505
506 as_unschedule_all_actions( 'defender/async_scan' );
507
508 $idle_scan->status = Scan_Model::STATUS_IDLE;
509 $idle_scan->task_checkpoint = 'time_limit';
510 $idle_scan->save();
511
512 $this->remove_lock( $this->lock_filename );
513 if ( $ready_to_send ) {
514 do_action( 'defender_notify', 'malware-notification', $idle_scan );
515 }
516 }
517 }
518
519 /**
520 * Update the idle scan status due the checksum issue.
521 *
522 * @param object $scan The current scan.
523 *
524 * @since 4.9.0
525 */
526 public function update_idle_scan_status_by_checksum_issue( $scan ) {
527 $ready_to_send = false;
528 if ( Scan_Model::STATUS_IDLE === $scan->status ) {
529 $ready_to_send = true;
530 }
531 $this->delete_interim_data();
532
533 as_unschedule_all_actions( 'defender/async_scan' );
534
535 $scan->status = Scan_Model::STATUS_IDLE;
536 $scan->task_checkpoint = 'checksum_issue';
537 $scan->save();
538
539 $this->remove_lock( $this->lock_filename );
540 if ( $ready_to_send ) {
541 do_action( 'defender_notify', 'malware-notification', $scan );
542 }
543 }
544
545 /**
546 * Clear all temporary scan data.
547 *
548 * @since 2.6.1
549 */
550 private function delete_interim_data() {
551 delete_site_option( Gather_Fact::CACHE_CORE );
552 delete_site_option( Gather_Fact::CACHE_CONTENT );
553 delete_site_option( Malware_Scan::YARA_RULES );
554 delete_site_option( Core_Integrity::CACHE_CHECKSUMS );
555 delete_site_option( Plugin_Integrity::PLUGIN_SLUGS );
556 delete_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS );
557 delete_site_option( self::PLUGINS_ACTIONED );
558 $this->maybe_track_failed_checksum();
559 }
560
561 /**
562 * Display styles on the Plugins page.
563 */
564 public function show_plugin_admin_styles() {
565 $custom_css = '.vulnerability-indent{ padding-left: 26px; }
566 .plugins .plugin-update-tr .plugin-update.plugin-vulnerability{box-shadow: inset 0 0px 0 rgb(0 0 0 / 10%);
567 border-bottom: rgb(0 0 0 / 10%) solid 1px;}';
568 wp_add_inline_style( 'defender-menu', $custom_css );
569 }
570
571 /**
572 * Display update information for a plugin.
573 *
574 * @param string $file Plugin basename.
575 * @param array $plugin_data Plugin information.
576 *
577 * @return void
578 */
579 public function attach_plugin_vulnerability_warning( $file, $plugin_data ) {
580 /**
581 * WP Plugin list table instance.
582 *
583 * @var WP_Plugins_List_Table $wp_list_table
584 */
585 $wp_list_table = _get_list_table(
586 'WP_Plugins_List_Table',
587 array( 'screen' => get_current_screen() )
588 );
589 $bugs = $this->vulnerability_details[ $file ]['bugs'];
590 if ( ! is_array( $bugs ) || array() === $bugs ) {
591 return;
592 }
593 $last_fixed_in = '0';
594 // Check if there have been updates since the last scan.
595 $exist_update = true;
596 if ( isset( $plugin_data['Version'] ) && '' !== $plugin_data['Version'] ) {
597 // The current plugin version.
598 $current_version = $plugin_data['Version'];
599 foreach ( $bugs as $bug_details ) {
600 // If the fixed version is existed then get the latest one.
601 if ( isset( $bug_details['fixed_in'] ) && '' !== $bug_details['fixed_in']
602 && version_compare( $bug_details['fixed_in'], $last_fixed_in, '>' )
603 ) {
604 $last_fixed_in = $bug_details['fixed_in'];
605 }
606 }
607 if ( version_compare( $last_fixed_in, $current_version, '>' ) ) {
608 $exist_update = false;
609 }
610 }
611 // If there were updates, do not display notice.
612 if ( $exist_update ) {
613 return;
614 }
615 if ( ( ! isset( $plugin_data['slug'] ) || '' === $plugin_data['slug'] ) && isset( $this->vulnerability_details[ $file ]['base_slug'] ) ) {
616 $plugin_data['slug'] = $this->vulnerability_details[ $file ]['base_slug'];
617 }
618
619 if ( is_network_admin() || ! is_multisite() ) {
620 if ( is_network_admin() ) {
621 $active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
622 } else {
623 $active_class = is_plugin_active( $file ) ? ' active' : '';
624 }
625
626 printf(
627 '<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>',
628 esc_attr( $active_class ),
629 esc_attr( $plugin_data['slug'] ),
630 esc_attr( $plugin_data['slug'] ),
631 esc_attr( $file ),
632 esc_attr( $wp_list_table->get_column_count() ),
633 'notice-error'
634 );
635
636 $notice = sprintf(
637 /* translators: %s - Plugin name. */
638 esc_html__(
639 '%s has detected a vulnerability in this plugin that may cause harm to your site.',
640 'defender-security'
641 ),
642 '<b>' . esc_html__( 'Defender Pro', 'defender-security' ) . '</b>'
643 );
644 if ( ( is_array( $bugs ) || $bugs instanceof Countable ? count( $bugs ) : 0 ) > 1 ) {
645 $notice .= '<hr/>';
646 $lines = array();
647 foreach ( $bugs as $bug ) {
648 $lines[] = '<span class="vulnerability-indent"></span>' . $bug['title'];
649 }
650 $notice .= implode( '<br/>', $lines );
651 $notice .= '<hr/><span class="vulnerability-indent"></span>';
652 if ( '0' !== $last_fixed_in ) {
653 $notice .= sprintf(
654 /* translators: %s - Version number. */
655 esc_html__(
656 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.',
657 'defender-security'
658 ),
659 $last_fixed_in
660 );
661 } else {
662 $notice .= esc_html__(
663 'Important! We recommend that you deactivate this plugin until the vulnerability has been fixed.',
664 'defender-security'
665 );
666 }
667 } else {
668 $notice .= '<br/><span class="vulnerability-indent"></span>' . $bugs[0]['title'] . '<br/><span class="vulnerability-indent"></span>';
669 $notice .= '0' === $last_fixed_in
670 ? esc_html__(
671 'We recommend that you deactivate this plugin until the vulnerability has been fixed.',
672 'defender-security'
673 )
674 : sprintf(
675 /* translators: 1: Version number. */
676 esc_html__(
677 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.',
678 'defender-security'
679 ),
680 $last_fixed_in
681 );
682 }
683
684 printf( wp_kses_post( $notice ) );
685 }
686 }
687
688 /**
689 * Display warnings.
690 *
691 * @since 2.6.2
692 */
693 public function display_vulnerability_warnings() {
694 if ( ! current_user_can( 'update_plugins' ) ) {
695 return;
696 }
697
698 $last = Scan_Model::get_last();
699 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
700 $vulnerability_issues = $last->get_issues( Scan_Item::TYPE_VULNERABILITY );
701 if ( ! is_array( $vulnerability_issues ) || array() === $vulnerability_issues ) {
702 return;
703 }
704
705 add_action( 'admin_print_styles-plugins.php', array( $this, 'show_plugin_admin_styles' ) );
706 // Vulnerability list.
707 foreach ( $vulnerability_issues as $vulnerability_obj ) {
708 $plugin_slug = $vulnerability_obj->raw_data['slug'];
709 // Get the details so that you can apply them later for each plugin.
710 $this->vulnerability_details[ $plugin_slug ] = $vulnerability_obj->raw_data;
711 add_action(
712 "after_plugin_row_$plugin_slug",
713 array(
714 $this,
715 'attach_plugin_vulnerability_warning',
716 ),
717 100,
718 2
719 );
720 }
721 }
722 }
723
724 /**
725 * Clear completed action scheduler logs.
726 *
727 * @since 2.6.5
728 */
729 public static function clear_logs() {
730 global $wpdb;
731
732 $table_actions = isset( $wpdb->actionscheduler_actions ) && '' !== $wpdb->actionscheduler_actions ?
733 $wpdb->actionscheduler_actions :
734 $wpdb->prefix . 'actionscheduler_actions';
735 $table_logs = isset( $wpdb->actionscheduler_logs ) && '' !== $wpdb->actionscheduler_logs ?
736 $wpdb->actionscheduler_logs :
737 $wpdb->prefix . 'actionscheduler_logs';
738
739 $table_count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
740 $wpdb->prepare(
741 'SELECT count(*)
742 FROM information_schema.tables
743 WHERE table_schema = %s AND table_name IN (%s, %s);',
744 $wpdb->dbname,
745 $table_actions,
746 $table_logs
747 )
748 );
749
750 if ( 2 !== $table_count ) {
751 return array( 'error' => esc_html__( 'Action scheduler is not setup', 'defender-security' ) );
752 }
753
754 $hook = 'defender/async_scan';
755 $status = 'complete';
756 $limit = 100;
757 $action_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
758 $wpdb->prepare(
759 "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
760 $hook,
761 $status,
762 $limit
763 )
764 );
765 while ( $action_ids ) {
766 if ( ! is_array( $action_ids ) || array() === $action_ids ) {
767 break;
768 }
769
770 $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(
771 ', ',
772 array_fill(
773 0,
774 is_array( $action_ids ) || $action_ids instanceof Countable ? count( $action_ids ) : 0,
775 '%s'
776 )
777 ) . ' )';
778 $query = call_user_func_array(
779 array( $wpdb, 'prepare' ),
780 array_merge(
781 array( $query ),
782 $action_ids
783 )
784 );
785 // SQL is prepared here, so we will ignore WordPress.DB.PreparedSQL.NotPrepared.
786 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
787
788 $action_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
789 $wpdb->prepare(
790 "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
791 $hook,
792 $status,
793 $limit
794 )
795 );
796 }
797
798 return array( 'success' => esc_html__( 'Malware scan logs are cleared', 'defender-security' ) );
799 }
800
801 /**
802 * Set the Gather_Fact object.
803 *
804 * @param Gather_Fact $gather_fact The Gather_Fact object to set.
805 */
806 public function set_gather_fact( Gather_Fact $gather_fact ) {
807 if ( class_exists( Gather_Fact::class ) ) {
808 $this->gather_fact = $gather_fact;
809 }
810 }
811
812 /**
813 * Gathers information using the Gather_Fact class.
814 *
815 * @param Gather_Fact $gather_fact The Gather_Fact instance.
816 *
817 * @return bool Returns true if information gathering is successful.
818 */
819 public function gather_info( Gather_Fact $gather_fact ): bool {
820 if ( method_exists( $gather_fact, 'gather_info' ) ) {
821 return $gather_fact->gather_info();
822 }
823
824 return true;
825 }
826
827 /**
828 * Gey intentions.
829 *
830 * @since 4.11.0
831 * @return array
832 */
833 public static function get_intentions(): array {
834 return array(
835 'resolve',
836 'ignore',
837 'delete',
838 'unignore',
839 'quarantine',
840 );
841 }
842
843 /**
844 * Get the list of actioned plugins taking into account the Ignored and Excluded.
845 *
846 * @return array
847 */
848 public function gather_actioned_plugin_details(): array {
849 $cache = get_site_option( self::PLUGINS_ACTIONED );
850 if ( self::are_actioned_plugins( $cache ) ) {
851 return $cache;
852 }
853
854 $items = array();
855 $is_plugin_used = false;
856 if ( $this->settings->integrity_check && $this->settings->check_plugins ) {
857 $is_plugin_used = true;
858 } elseif ( $this->settings->check_abandoned_plugin ) {
859 $is_plugin_used = true;
860 } elseif ( $this->is_pro && ( $this->settings->check_known_vuln || $this->settings->scan_malware ) ) {
861 $is_plugin_used = true;
862 }
863
864 if ( $is_plugin_used ) {
865 $model = Scan_Model::get_last();
866 /**
867 * Exclude plugin slugs.
868 *
869 * @param array $slugs Slugs of excluded plugins.
870 *
871 * @since 3.1.0
872 */
873 $excluded_slugs = apply_filters( 'wd_scan_excluded_plugin_slugs', array() );
874 $excluded_slugs = ! is_array( $excluded_slugs ) ? (array) $excluded_slugs : $excluded_slugs;
875
876 foreach ( $this->get_plugins() as $slug => $item ) {
877 if ( is_object( $model ) && $model->is_issue_ignored( $slug ) ) {
878 continue;
879 }
880 $base_slug = $this->get_plugin_slug_by( $slug );
881 if ( in_array( $base_slug, $excluded_slugs, true ) ) {
882 continue;
883 }
884 // Use keys with the first capital letter to match default plugin header keys.
885 $items[ $base_slug ] = array(
886 'Name' => $item['Name'],
887 'Version' => $item['Version'],
888 'Slug' => $slug,
889 );
890 }
891
892 update_site_option( self::PLUGINS_ACTIONED, $items );
893 }
894
895 return $items;
896 }
897
898 /**
899 * Does the list of actioned plugins exist and no empty?
900 *
901 * @param array|false $actioned_plugins The actioned plugins.
902 *
903 * @return bool
904 */
905 public static function are_actioned_plugins( $actioned_plugins ): bool {
906 return is_array( $actioned_plugins ) && array() !== $actioned_plugins;
907 }
908
909 /**
910 * Get the lock filename.
911 *
912 * @return string
913 */
914 public function get_lock_filename(): string {
915 return $this->lock_filename;
916 }
917 }
918