PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.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 / scan.php

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

544 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Component;
4
5 use WP_Defender\Behavior\Scan\Core_Integrity;
6 use WP_Defender\Behavior\Scan\Gather_Fact;
7 use WP_Defender\Behavior\Scan\Known_Vulnerability;
8 use WP_Defender\Behavior\Scan\Malware_Scan;
9 use WP_Defender\Behavior\Scan\Plugin_Integrity;
10 use WP_Defender\Behavior\WPMUDEV;
11 use WP_Defender\Component;
12 use WP_Defender\Model\Scan_Item;
13 use WP_Defender\Model\Scan as Model_Scan;
14
15 class Scan extends Component {
16
17 /**
18 * Cache the current scan.
19 *
20 * @var \WP_Defender\Model\Scan
21 */
22 public $scan;
23
24 /**
25 * @var \WP_Defender\Model\Setting\Scan
26 */
27 public $settings;
28
29 /**
30 * @var array
31 */
32 protected $vulnerability_details = array();
33
34 public function __construct() {
35 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
36 $this->attach_behavior( Gather_Fact::class, Gather_Fact::class );
37 $this->attach_behavior( Core_Integrity::class, Core_Integrity::class );
38 $this->attach_behavior( Plugin_Integrity::class, Plugin_Integrity::class );
39 if ( class_exists( Known_Vulnerability::class ) ) {
40 $this->attach_behavior( Known_Vulnerability::class, new Known_Vulnerability() );
41 }
42 if ( class_exists( Malware_Scan::class ) ) {
43 $this->attach_behavior( Malware_Scan::class, new Malware_Scan() );
44 }
45 }
46
47 /**
48 * @param object $model
49 */
50 public function advanced_scan_actions( $model ) {
51 $this->reindex_ignored_issues( $model );
52 $this->clean_up();
53 }
54
55 /**
56 * Process current scan.
57 *
58 * @return bool
59 * @throws \ReflectionException
60 */
61 public function process() {
62 $scan = Model_Scan::get_active();
63 if ( ! is_object( $scan ) ) {
64 // This case can be a scan get cancel.
65 return - 1;
66 }
67 $this->scan = $scan;
68 $this->settings = new \WP_Defender\Model\Setting\Scan();
69 $tasks = $this->get_tasks();
70 $runner = new \ArrayIterator( $tasks );
71 $task = $this->scan->status;
72 if ( Model_Scan::STATUS_INIT === $scan->status ) {
73 // Get the first.
74 $this->log( 'Prepare facts for a scan', 'scan.log' );
75 $task = 'gather_fact';
76 $this->scan->percent = 0;
77 $this->scan->total_tasks = $runner->count();
78 $this->scan->save();
79 }
80 if (
81 in_array(
82 $this->scan->status,
83 array(
84 Model_Scan::STATUS_ERROR,
85 Model_Scan::STATUS_IDLE,
86 ),
87 true
88 )
89 ) {
90 // Stop and return true to abort the process.
91 return true;
92 }
93 // Find the current task.
94 $offset = array_search( $task, array_values( $tasks ) );
95 if ( false === $offset ) {
96 $this->log( sprintf( 'offset is not found, search %s', $task ), 'scan.log' );
97
98 return false;
99 }
100 // Reset the tasks to current.
101 $runner->seek( $offset );
102 $this->log( sprintf( 'Current task %s', $runner->current() ), 'scan.log' );
103 if ( $this->has_method( $task ) ) {
104 $this->log( sprintf( 'processing %s', $runner->key() ), 'scan.log' );
105 $result = $this->$task();
106 if ( true === $result ) {
107 $this->log( sprintf( 'task %s processed', $runner->key() ), 'scan.log' );
108 // Task is done, move to next.
109 $runner->next();
110 if ( $runner->valid() ) {
111 $this->log( sprintf( 'queue %s for next', $runner->key() ), 'scan.log' );
112 $this->scan->status = $runner->key();
113 $this->scan->task_checkpoint = '';
114 $this->scan->date_end = gmdate( 'Y-m-d H:i:s' );
115 $this->scan->save();
116 // Queue for next run.
117 return false;
118 }
119 $this->log( 'All done!', 'scan.log' );
120 // No more task in the queue, we are done.
121 $this->scan->status = Model_Scan::STATUS_FINISH;
122 $this->scan->save();
123 $this->advanced_scan_actions( $this->scan );
124 do_action( 'defender_notify', 'malware-notification', $this->scan );
125
126 return true;
127 }
128 $this->scan->status = $task;
129 $this->scan->save();
130 }
131
132 return false;
133 }
134
135 /**
136 * @param \WP_Defender\Model\Scan $model
137 */
138 private function reindex_ignored_issues( $model ) {
139 $issues = $model->get_issues( null, Scan_Item::STATUS_IGNORE );
140 $ignore_lists = array();
141 foreach ( $issues as $issue ) {
142 $data = $issue->raw_data;
143 if ( isset( $data['file'] ) ) {
144 $ignore_lists[] = $data['file'];
145 } elseif ( isset( $data['slug'] ) ) {
146 $ignore_lists[] = $data['slug'];
147 }
148 }
149 $model->update_ignore_list( $ignore_lists );
150 }
151
152 /**
153 * Get a list of tasks will run in a scan.
154 *
155 * @return array
156 */
157 public function get_tasks() {
158 $tasks = array(
159 'gather_fact' => 'gather_fact',
160 );
161 if ( $this->settings->integrity_check ) {
162 /**
163 * @since 2.4.7
164 */
165 if ( $this->settings->check_core ) {
166 $tasks['core_integrity_check'] = 'core_integrity_check';
167 }
168 if ( $this->settings->check_plugins ) {
169 $tasks['plugin_integrity_check'] = 'plugin_integrity_check';
170 }
171 }
172 if ( $this->is_pro() ) {
173 if ( $this->settings->check_known_vuln ) {
174 if ( $this->has_method( 'vuln_check' ) ) {
175 $tasks['vuln_check'] = 'vuln_check';
176 }
177 }
178 if ( $this->settings->scan_malware ) {
179 if ( $this->has_method( 'suspicious_check' ) ) {
180 $tasks['suspicious_check'] = 'suspicious_check';
181 }
182 }
183 }
184
185 return $tasks;
186 }
187
188 public function cancel_a_scan() {
189 $scan = Model_Scan::get_active();
190 if ( is_object( $scan ) ) {
191 $scan->delete();
192 }
193 $this->clean_up();
194 $this->remove_lock();
195 }
196
197 /**
198 * Clean up data generate by current scan.
199 */
200 public function clean_up() {
201 $this->delete_interim_data();
202
203 $models = Model_Scan::get_last_all();
204 if ( ! empty( $models ) ) {
205 // Remove the latest. Don't remove code to find the first value.
206 $current = array_shift( $models );
207 foreach ( $models as $model ) {
208 $model->delete();
209 }
210 }
211 }
212
213 /**
214 * Create a file lock, so we can check if a process already running.
215 */
216 public function create_lock() {
217 file_put_contents( $this->get_lock_path(), time(), LOCK_EX );
218 }
219
220 /**
221 * Delete file lock.
222 */
223 public function remove_lock() {
224 @unlink( $this->get_lock_path() );
225 }
226
227 /**
228 * Check if a lock is valid.
229 *
230 * @return bool
231 */
232 public function has_lock() {
233 if ( ! file_exists( $this->get_lock_path() ) ) {
234 return false;
235 }
236 $time = file_get_contents( $this->get_lock_path() );
237 if ( strtotime( '+90 seconds', $time ) < time() ) {
238 // Usually a timeout window is 30 seconds, so we should allow lock at 1.30min for safe.
239 return false;
240 }
241
242 return true;
243 }
244
245 /**
246 * Get the total scanning active issues.
247 *
248 * @return integer $count
249 */
250 public function indicator_issue_count() {
251 $count = 0;
252 $scan = Model_Scan::get_last();
253 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
254 // Only Scan issues.
255 $count = $scan->count( null, Scan_Item::STATUS_ACTIVE );
256 }
257
258 return $count;
259 }
260
261 /**
262 * @param array $scan_settings
263 * @param bool $is_pro
264 *
265 * @return bool
266 */
267 public function is_any_scan_active( $scan_settings, $is_pro ) {
268 if ( empty( $scan_settings['integrity_check'] ) ) {
269 $integrity_check = false;
270 } elseif (
271 ! empty( $scan_settings['integrity_check'] )
272 && empty( $scan_settings['check_core'] )
273 && empty( $scan_settings['check_plugins'] )
274 ) {
275 $integrity_check = false;
276 } else {
277 $integrity_check = true;
278 }
279
280 if ( ! $integrity_check && ! $is_pro ) {
281
282 return false;
283 } elseif (
284 ! $integrity_check
285 && empty( $scan_settings['check_known_vuln'] )
286 && empty( $scan_settings['scan_malware'] )
287 && ! $is_pro
288 ) {
289
290 return false;
291 }
292
293 return true;
294 }
295
296 /**
297 * Update the idle scan status.
298 *
299 * @since 2.6.1
300 */
301 public function update_idle_scan_status() {
302 $idle_scan = wd_di()->get( Model_Scan::class )->get_idle();
303
304 if ( is_object( $idle_scan ) ) {
305 $ready_to_send = false;
306 if ( Model_Scan::STATUS_IDLE === $idle_scan->status ) {
307 $ready_to_send = true;
308 }
309 $this->delete_interim_data();
310
311 as_unschedule_all_actions( 'defender/async_scan' );
312
313 $idle_scan->status = Model_Scan::STATUS_IDLE;
314 $idle_scan->save();
315
316 $this->remove_lock();
317 if ( $ready_to_send ) {
318 do_action( 'defender_notify', 'malware-notification', $idle_scan );
319 }
320 }
321 }
322
323 /**
324 * Clear all temporary scan data.
325 *
326 * @since 2.6.1
327 */
328 private function delete_interim_data() {
329 delete_site_option( Gather_Fact::CACHE_CORE );
330 delete_site_option( Gather_Fact::CACHE_CONTENT );
331 delete_site_option( Malware_Scan::YARA_RULES );
332 delete_site_option( Core_Integrity::CACHE_CHECKSUMS );
333 delete_site_option( Plugin_Integrity::PLUGIN_SLUGS );
334 delete_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS );
335 }
336
337 /**
338 * Display styles on the Plugins page.
339 */
340 public function show_plugin_admin_styles() {
341 $custom_css = '.vulnerability-indent{ padding-left: 26px; }
342 .plugins .plugin-update-tr .plugin-update.plugin-vulnerability{box-shadow: inset 0 0px 0 rgb(0 0 0 / 10%);
343 border-bottom: rgb(0 0 0 / 10%) solid 1px;}';
344 wp_add_inline_style( 'defender-menu', $custom_css );
345 }
346
347 /**
348 * Display update information for a plugin.
349 * @param string $file Plugin basename.
350 * @param array $plugin_data Plugin information.
351 *
352 * @return void
353 */
354 public function attach_plugin_vulnerability_warning( $file, $plugin_data ) {
355 /** @var WP_Plugins_List_Table $wp_list_table */
356 $wp_list_table = _get_list_table(
357 'WP_Plugins_List_Table',
358 array(
359 'screen' => get_current_screen(),
360 )
361 );
362 $bugs = $this->vulnerability_details[ $file ]['bugs'];
363 if ( empty( $bugs ) ) {
364
365 return;
366 }
367 $last_fixed_in = '0';
368 // Check if there have been updates since the last scan.
369 $exist_update = true;
370 if ( isset( $plugin_data['Version'] ) && ! empty( $plugin_data['Version'] ) ) {
371 // The current plugin version.
372 $current_version = $plugin_data['Version'];
373 foreach ( $bugs as $bug_details ) {
374 // If the fixed version is existed then get the latest one.
375 if( isset( $bug_details['fixed_in'] )
376 && version_compare( $bug_details['fixed_in'], $last_fixed_in, '>' )
377 ) {
378 $last_fixed_in = $bug_details['fixed_in'];
379 }
380 }
381 if( version_compare( $last_fixed_in, $current_version, '>' ) ) {
382 $exist_update = false;
383 }
384 }
385 // If there were updates, do not display notice.
386 if ( $exist_update ) {
387
388 return;
389 }
390 // Sometimes $plugin_data['slug'] is empty.
391 if ( empty( $plugin_data['slug'] ) && isset( $this->vulnerability_details[ $file ]['base_slug'] ) ) {
392 $plugin_data['slug'] = $this->vulnerability_details[ $file ]['base_slug'];
393 }
394
395 if ( is_network_admin() || ! is_multisite() ) {
396 if ( is_network_admin() ) {
397 $active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
398 } else {
399 $active_class = is_plugin_active( $file ) ? ' active' : '';
400 }
401
402 printf(
403 '<tr class="plugin-update-tr%s" id="vulnerability-%s" data-slug="%s" data-plugin="%s">' .
404 '<td colspan="%s" class="plugin-update colspanchange plugin-vulnerability">' .
405 '<div class="update-message notice inline %s notice-alt"><p>',
406 $active_class,
407 esc_attr( $plugin_data['slug'] ),
408 esc_attr( $plugin_data['slug'] ),
409 esc_attr( $file ),
410 esc_attr( $wp_list_table->get_column_count() ),
411 'notice-error'
412 );
413
414 $notice = sprintf(
415 /* translators: %s - Plugin name. */
416 __( '%s has detected a vulnerability in this plugin that may cause harm to your site.', 'wpdef' ),
417 '<b>' . __( 'Defender Pro', 'wpdef' ) . '</b>'
418 );
419 if ( (is_array($bugs) || $bugs instanceof \Countable ? count( $bugs ) : 0) > 1 ) {
420 $notice .= '<hr/>';
421 $lines = array();
422 foreach ( $bugs as $bug ) {
423 $lines[] = '<span class="vulnerability-indent"></span>' . $bug['title'];
424 }
425 $notice .= implode( '<br/>', $lines );
426 $notice .= '<hr/><span class="vulnerability-indent"></span>';
427 if ( '0' !== $last_fixed_in ) {
428 $notice .= sprintf(
429 /* translators: %s - Version number. */
430 __( 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.', 'wpdef' ),
431 $last_fixed_in
432 );
433 } else {
434 $notice .= __( 'Important! We recommend that you deactivate this plugin until the vulnerability has been fixed.', 'wpdef' );
435 }
436 } else {
437 $notice .= '<br/><span class="vulnerability-indent"></span>' . $bugs[0]['title'] . '<br/><span class="vulnerability-indent"></span>';
438 $notice .= empty( $last_fixed_in )
439 ? __( 'We recommend that you deactivate this plugin until the vulnerability has been fixed.', 'wpdef' )
440 : sprintf(
441 /* translators: 1: Version number. */
442 __( 'The vulnerability has been fixed in version %s. We recommend that you update this plugin accordingly.', 'wpdef' ),
443 $last_fixed_in
444 );
445 }
446
447 printf( $notice );
448 }
449 }
450
451 /**
452 * Display warnings.
453 * @since 2.6.2
454 */
455 public function display_vulnerability_warnings() {
456 if ( ! current_user_can( 'update_plugins' ) ) {
457
458 return;
459 }
460
461 $last = \WP_Defender\Model\Scan::get_last();
462 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
463 $vulnerability_issues = $last->get_issues( Scan_Item::TYPE_VULNERABILITY );
464 if ( empty( $vulnerability_issues ) ) {
465 return;
466 }
467
468 add_action( 'admin_print_styles-plugins.php', array( $this, 'show_plugin_admin_styles' ) );
469 foreach ( $vulnerability_issues as $vulnerability_obj ) {
470 $plugin_slug = $vulnerability_obj->raw_data['slug'];
471 // Get the details so that you can apply them later for each plugin.
472 $this->vulnerability_details[ $plugin_slug ] = $vulnerability_obj->raw_data;
473 add_action(
474 "after_plugin_row_$plugin_slug",
475 array(
476 $this,
477 'attach_plugin_vulnerability_warning',
478 ),
479 100,
480 2
481 );
482 }
483 }
484 }
485
486 /**
487 * Clear completed action scheduler logs.
488 *
489 * @since 2.6.5
490 */
491 public static function clear_logs() {
492 global $wpdb;
493
494 $table_actions = ! empty( $wpdb->actionscheduler_actions ) ?
495 $wpdb->actionscheduler_actions :
496 $wpdb->prefix . 'actionscheduler_actions';
497 $table_logs = ! empty( $wpdb->actionscheduler_logs ) ?
498 $wpdb->actionscheduler_logs :
499 $wpdb->prefix . 'actionscheduler_logs';
500
501 $table_count = (int) $wpdb->get_var(
502 $wpdb->prepare(
503 "SELECT count(*)
504 FROM information_schema.tables
505 WHERE table_schema = %s AND table_name IN (%s, %s);",
506 $wpdb->dbname,
507 $table_actions,
508 $table_logs
509 )
510 );
511
512 if ( 2 !== $table_count ) {
513 return array(
514 'error' => __( 'Action scheduler is not setup', 'wpdef' ),
515 );
516 }
517
518 $hook = 'defender/async_scan';
519 $status = 'complete';
520 $limit = 100;
521 while ( $action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$table_actions} as_actions WHERE as_actions.hook = %s AND as_actions.status = %s LIMIT %d", $hook, $status, $limit ) ) ) {
522 if ( empty( $action_ids ) ) {
523 break;
524 }
525
526 $where_in = implode( ', ', array_fill( 0, is_array($action_ids) || $action_ids instanceof \Countable ? count( $action_ids ) : 0, '%s' ) );
527 $r = $wpdb->query(
528 $wpdb->prepare(
529 "DELETE as_actions, as_logs
530 FROM {$table_actions} as_actions
531 LEFT JOIN {$table_logs} as_logs
532 ON as_actions.action_id = as_logs.action_id
533 WHERE as_actions.action_id IN ( {$where_in} )",
534 $action_ids
535 )
536 );
537 }
538
539 return array(
540 'success' => __( 'Malware scan logs are cleared', 'wpdef' ),
541 );
542 }
543 }
544