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

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