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

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