PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.1.0
Defender Security – Malware Scanner, Login Security & Firewall v6.1.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 6.1.0, at src/component/class-scan.php

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