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

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

1,168 lines 35.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This file contains the Cli class which is used to handle WP-CLI commands for the WP Defender plugin.
4 * It provides methods to manage scans, audits, firewall settings, and more through the command line.
5 *
6 * @package WP_Defender\Component
7 */
8
9 namespace WP_Defender\Component;
10
11 use WP_CLI;
12 use Exception;
13 use Countable;
14 use Throwable;
15 use Faker\Factory;
16 use WP_CLI\ExitException;
17 use WP_Defender\Traits\IO;
18 use WP_Defender\Traits\Theme;
19 use WP_Defender\Traits\Plugin;
20 use WP_Defender\Traits\Formats;
21 use WP_Defender\Model\Audit_Log;
22 use WP_Defender\Model\Scan_Item;
23 use WP_Defender\Model\Lockout_Ip;
24 use WP_Defender\Model\Lockout_Log;
25 use WP_Defender\Controller\Dashboard;
26 use WP_Defender\Controller\Two_Factor;
27 use WP_Defender\Model\Scan as Model_Scan;
28 use WP_Defender\Controller\Main_Setting;
29 use WP_Defender\Controller\Login_Access;
30 use WP_Defender\Controller\Security_Tweaks;
31 use WP_Defender\Model\Setting\Login_Lockout;
32 use WP_Defender\Model\Setting\Password_Reset;
33 use WP_Defender\Model\Setting\Notfound_Lockout;
34 use WP_Defender\Model\Setting\Security_Headers;
35 use WP_Defender\Model\Setting\User_Agent_Lockout;
36 use WP_Defender\Component\Logger\Rotation_Logger;
37 use WP_Defender\Component\Scan as Scan_Component;
38 use WP_Filesystem_Base;
39 use function WP_CLI\Utils\format_items;
40
41 if ( ! defined( 'WPINC' ) ) {
42 die;
43 }
44
45 /**
46 * Class Cli
47 */
48 class Cli {
49
50 use Formats {
51 calculate_date_interval as protected;
52 format_bytes_into_readable as protected;
53 format_date_time as protected;
54 get_date as protected;
55 get_days_of_week as protected;
56 get_times as protected;
57 get_timezone_string as protected;
58 local_to_utc as protected;
59 moment_datetime_format_from as protected;
60 persistent_hub_datetime_format as protected;
61 time_since as protected;
62 get_local_human_date as protected;
63 release_cron_lock as protected;
64 create_lock as protected;
65 has_lock as protected;
66 remove_lock as protected;
67 check_plugin_on_wp_org as protected;
68 check_by_readme_file as protected;
69 acquire_cron_lock as protected;
70 compare_hashes as protected;
71 delete_dir as protected;
72 detect_line_ending as protected;
73 get_abs_plugin_path_by_slug as protected;
74 get_active_plugin_names as protected;
75 get_path_of_themes_dir as protected;
76 get_log_path as protected;
77 get_plugin_details_by as protected;
78 get_plugin_directory_name as protected;
79 get_plugin_headers as protected;
80 get_plugin_relative_path as protected;
81 get_plugin_slugs as protected;
82 get_plugins as protected;
83 get_plugin_slug_by as protected;
84 get_theme as protected;
85 get_theme_slugs as protected;
86 get_themes as protected;
87 get_time_diff as protected;
88 handle_wp_org_response_by as protected;
89 is_active_plugin as protected;
90 is_active_theme as protected;
91 is_likely_wporg_slug as protected;
92 ping_wp_org_by_plugin_slug as protected;
93 }
94 use IO;
95 use Theme;
96 use Plugin;
97
98 /**
99 * This is a helper for scan module.
100 * #Options
101 * <command>
102 * : Value can be run - Perform a scan, e.g. 'run'-command or 'run ----type=detailed' for detailed result,
103 * or (un)ignore|delete|resolve to do the relevant task,
104 * or clear_logs to remove completed schedule logs.
105 * [--type=<type>]
106 * : Default, without values, is for all items, or core_integrity|plugin_integrity|vulnerability|suspicious_code|plugin_outdated|plugin_closed.
107 *
108 * @param mixed $args Command arguments.
109 * @param mixed $options Command options.
110 *
111 * @throws ExitException If an invalid command is provided.
112 */
113 public function scan( $args, $options ) {
114 if ( ! is_array( $args ) || array() === $args ) {
115 WP_CLI::error( 'Invalid command' );
116
117 return;
118 }
119 [ $command ] = $args;
120 switch ( $command ) {
121 case 'run':
122 $this->scan_all( $options );
123 break;
124 case 'clear_logs':
125 $this->scan_clear_logs();
126 break;
127 default:
128 $commands = array(
129 'ignore',
130 'unignore',
131 'resolve',
132 'delete',
133 );
134 if ( in_array( $command, $commands, true ) ) {
135 WP_CLI::confirm(
136 'This can cause your site get fatal error and can\'t restore back unless you have a backup, are you sure to continue?',
137 $options
138 );
139 $this->scan_task( $command, $options );
140 } else {
141 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
142 }
143 break;
144 }
145 }
146
147 /**
148 * Split scan issue into file and dir.
149 *
150 * @param string|null $type Scan type.
151 * @param array $raw_data Array of raw scan data.
152 *
153 * @return array
154 */
155 private function split_scan_issue_into_file_and_dir( $type, $raw_data ): array {
156 // General case without type-param.
157 if ( null === $type ) {
158 if ( isset( $raw_data['file'] ) ) {
159 return array(
160 'type' => 'file',
161 'path' => $raw_data['file'],
162 );
163 } elseif ( isset( $raw_data['base_slug'] ) ) {
164 return array(
165 'type' => 'folder',
166 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
167 );
168 } elseif ( isset( $raw_data['slug'] ) ) {
169 return array(
170 'type' => 'folder',
171 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
172 );
173 }
174 }
175
176 if ( in_array(
177 $type,
178 array(
179 Scan_Item::TYPE_PLUGIN_OUTDATED,
180 Scan_Item::TYPE_PLUGIN_CLOSED,
181 ),
182 true
183 ) ) {
184 return array(
185 'type' => 'folder',
186 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
187 );
188 } elseif ( Scan_Item::TYPE_VULNERABILITY === $type ) {
189 return array(
190 'type' => 'folder',
191 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
192 );
193 } else {
194 return array(
195 'type' => 'file',
196 'path' => $raw_data['file'],
197 );
198 }
199 }
200
201 /**
202 * Executes tasks based on the type of scan.
203 *
204 * @param string $command The task to perform.
205 * @param mixed $options Command options.
206 */
207 private function scan_task( $command, $options ) {
208 $type = $options['type'] ? strtolower( $options['type'] ) : null;
209 if ( defender_is_wp_org_version()
210 && in_array(
211 $type,
212 array(
213 Scan_Item::TYPE_VULNERABILITY,
214 // TYPE_SUSPICIOUS const is not suitable for use.
215 'suspicious_code',
216 ),
217 true
218 )
219 ) {
220 WP_CLI::warning( 'A WPMU DEV subscription is required to use this command.' );
221 return;
222 }
223
224 switch ( $type ) {
225 case null:
226 // All items.
227 $type = null;
228 break;
229 case 'core_integrity':
230 $type = Scan_Item::TYPE_INTEGRITY;
231 break;
232 case 'plugin_integrity':
233 $type = Scan_Item::TYPE_PLUGIN_CHECK;
234 break;
235 case 'vulnerability':
236 $type = Scan_Item::TYPE_VULNERABILITY;
237 break;
238 case 'suspicious_code':
239 $type = Scan_Item::TYPE_SUSPICIOUS;
240 break;
241 case 'plugin_outdated':
242 $type = Scan_Item::TYPE_PLUGIN_OUTDATED;
243 break;
244 case 'plugin_closed':
245 $type = Scan_Item::TYPE_PLUGIN_CLOSED;
246 break;
247 default:
248 WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
249 break;
250 }
251 $active = Model_Scan::get_active();
252 if ( is_object( $active ) ) {
253 return WP_CLI::error( 'A scan is running, you need to wait till it complete to continue' );
254 }
255 $model = Model_Scan::get_last();
256 if ( ! is_object( $model ) ) {
257 return;
258 }
259 switch ( $command ) {
260 case 'ignore':
261 $issues = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
262 foreach ( $issues as $issue ) {
263 $issue_data = $this->split_scan_issue_into_file_and_dir( $type, $issue->raw_data );
264 if ( $model->ignore_issue( $issue->id ) ) {
265 WP_CLI::log( sprintf( 'Ignoring %s: %s', $issue_data['type'], $issue_data['path'] ) );
266 }
267 }
268 WP_CLI::log( sprintf( 'Ignored %s items', count( $issues ) ) );
269 break;
270 case 'unignore':
271 $issues = $model->get_issues( $type, Scan_Item::STATUS_IGNORE );
272 foreach ( $issues as $issue ) {
273 $issue_data = $this->split_scan_issue_into_file_and_dir( $type, $issue->raw_data );
274 if ( $model->unignore_issue( $issue->id ) ) {
275 WP_CLI::log( sprintf( 'Unignoring %s: %s', $issue_data['type'], $issue_data['path'] ) );
276 }
277 }
278 WP_CLI::log( sprintf( 'Unignored %s items', count( $issues ) ) );
279 break;
280 case 'resolve':
281 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
282 $resolved = array();
283 foreach ( $items as $item ) {
284 if (
285 in_array(
286 $item->type,
287 array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ),
288 true
289 )
290 ) {
291 WP_CLI::log( sprintf( 'Reverting %s to original', $item->raw_data['file'] ) );
292 $ret = $item->resolve();
293 if ( ! is_wp_error( $ret ) ) {
294 $resolved[] = $item;
295 } else {
296 return WP_CLI::error( $ret->get_error_message() );
297 }
298 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $item->type ) {
299 // If this is content, we will try to delete them.
300 $whitelist = array(
301 // wordfence waf.
302 ABSPATH . '/wordfence-waf.php',
303 // Any files inside plugins, if removed, can cause fatal error.
304 WP_CONTENT_DIR . '/plugins/',
305 // Any files inside themes.
306 $this->get_path_of_themes_dir(),
307 );
308 $path = $item->raw_data['file'];
309 $can_delete = true;
310 $current = '';
311 foreach ( $whitelist as $value ) {
312 $current = $value;
313 if ( strpos( $value, $path ) > 0 ) {
314 // Ignore this.
315 $can_delete = false;
316 break;
317 }
318 }
319 if ( false === $can_delete ) {
320 WP_CLI::log( sprintf( 'Ignore file %s as it is in %s', $path, $current ) );
321 } elseif ( wp_delete_file( $path ) ) {
322 WP_CLI::log( sprintf( 'Delete file %s', $path ) );
323 $model->remove_issue( $item->id );
324 $resolved[] = $item;
325 } else {
326 return WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
327 }
328 }
329 // No result for Vulnerability, Outdated or Closed plugin types.
330 }
331 WP_CLI::log( sprintf( 'Resolved %s items', count( $resolved ) ) );
332 break;
333 case 'delete':
334 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
335 $deleted = array();
336 foreach ( $items as $item ) {
337 $issue_data = $this->split_scan_issue_into_file_and_dir( $type, $item->raw_data );
338 $path = $issue_data['path'];
339 $issue_type = $issue_data['type'];
340 if ( ! file_exists( $path ) ) {
341 continue;
342 }
343 // Work with plugin dir or single file, e.g. for Vulnerability, Outdated or Closed plugin types.
344 if ( 'folder' === $issue_type ) {
345 if ( $this->is_active_plugin( $path ) ) {
346 WP_CLI::warning( sprintf( 'This plugin %s cannot be removed because it is active.', $path ) );
347 continue;
348 }
349
350 if ( is_dir( $path ) ) {
351 if ( $this->delete_dir( $path ) ) {
352 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
353 $model->remove_issue( $item->id );
354 $deleted[] = $item;
355 }
356 } elseif ( wp_delete_file( $path ) ) {
357 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
358 $model->remove_issue( $item->id );
359 $deleted[] = $item;
360 } else {
361
362 return WP_CLI::error( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
363 }
364 } elseif ( 'file' === $issue_type ) {
365 // Work with core_integrity, plugin_integrity or suspicious_code types.
366 if ( wp_delete_file( $path ) ) {
367 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
368 $model->remove_issue( $item->id );
369 $deleted[] = $item;
370 } else {
371 return WP_CLI::error( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
372 }
373 }
374 }
375 WP_CLI::log( sprintf( 'Deleted %s items', count( $deleted ) ) );
376 break;
377 default:
378 break;
379 }
380 }
381
382 /**
383 * Generate dummy data, use in unit tests.
384 * DO NOT USE IN PRODUCTION.
385 *
386 * @param mixed $args Command arguments.
387 */
388 public function seed( $args ) {
389 global $wp_filesystem;
390 // Initialize the WP filesystem, no more using 'file-put-contents' function.
391 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
392 require_once ABSPATH . '/wp-admin/includes/file.php';
393 WP_Filesystem();
394 }
395 if ( ! is_array( $args ) || array() === $args ) {
396 WP_CLI::error( 'Invalid command' );
397
398 return;
399 }
400 [ $command ] = $args;
401 switch ( $command ) {
402 case 'scan:core':
403 $file_path = ABSPATH . 'wp-load.php';
404 $content = '//this make different';
405 if ( $wp_filesystem->exists( $file_path ) ) {
406 $content = $wp_filesystem->get_contents( $file_path ) . $content;
407 }
408 $wp_filesystem->put_contents( $file_path, $content );
409 break;
410 case 'ip:logs':
411 // We will generate randomly 10k logs in 3 months.
412 $types = array(
413 Lockout_Log::AUTH_FAIL,
414 Lockout_Log::AUTH_LOCK,
415 Lockout_Log::ERROR_404,
416 Lockout_Log::LOCKOUT_404,
417 Lockout_Log::LOCKOUT_UA,
418 );
419 $is_lock = array(
420 Lockout_Log::AUTH_LOCK,
421 Lockout_Log::LOCKOUT_404,
422 Lockout_Log::LOCKOUT_UA,
423 );
424 $faker = Factory::create();
425 WP_CLI::log( $faker->ipv4 );
426 $range = array(
427 'today midnight' => array( 'now', 100 ),
428 '-6 days' => array( 'yesterday', 50 ),
429 '-30 days' => array( '-7 days', 70 ),
430 );
431 $counter = array(
432 'last_24_hours' => 0,
433 'last_30_days' => 0,
434 'login_lockout' => 0,
435 '404_lockout' => 0,
436 'ua_lockout' => 0,
437 );
438 $last_lockout = 0;
439 foreach ( $range as $date => $to ) {
440 [ $to, $count ] = $to;
441 for ( $i = 0; $i < $count; $i++ ) {
442 $model = new Lockout_Log();
443 $model->ip = $faker->ipv4;
444 $model->type = $types[ array_rand( $types ) ];
445 $model->log = $faker->sentence( 20 );
446 $model->date = $faker->dateTimeBetween( $date, $to )->getTimestamp();
447 $model->blog_id = 1;
448 $model->tried = $faker->userName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
449 $model->country_iso_code = $faker->countryCode; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
450 $model->save();
451 if ( ( $model->date > $last_lockout ) ) {
452 $last_lockout = $model->date;
453 }
454 if ( in_array( $model->type, $is_lock, true ) ) {
455 $counter['last_30_days'] += 1;
456 if ( $model->date > strtotime( 'yesterday midnight' ) ) {
457 $counter['last_24_hours'] += 1;
458 }
459 if ( $model->date > strtotime( '-6 days', strtotime( 'today midnight' ) ) ) {
460 if ( Lockout_Log::AUTH_LOCK === $model->type ) {
461 $counter['login_lockout'] += 1;
462 } elseif ( Lockout_Log::LOCKOUT_404 === $model->type ) {
463 $counter['404_lockout'] += 1;
464 } else {
465 $counter['ua_lockout'] += 1;
466 }
467 }
468 }
469 }
470 }
471 $counter['last_lockout'] = $this->format_date_time( $last_lockout );
472 echo wp_json_encode( $counter );
473 break;
474 default:
475 WP_CLI::error( 'Invalid command' );
476 break;
477 }
478 }
479
480 /**
481 * Clean up dummy data.
482 * DO NOT USE IN PRODUCTION.
483 *
484 * @param mixed $args Command arguments.
485 */
486 public function unseed( $args ) {
487 global $wp_filesystem;
488 // Initialize the WP filesystem, no more using 'file-put-contents' function.
489 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
490 require_once ABSPATH . '/wp-admin/includes/file.php';
491 WP_Filesystem();
492 }
493 if ( ! is_array( $args ) || array() === $args ) {
494 WP_CLI::error( 'Invalid command' );
495
496 return;
497 }
498 [ $command ] = $args;
499 switch ( $command ) {
500 case 'scan:core':
501 $content = file_get_contents( ABSPATH . 'wp-load.php' );
502 $wp_filesystem->put_contents( ABSPATH . 'wp-load.php', str_replace( '//this make different', '', $content ) );
503 break;
504 case 'scan:suspicious':
505 wp_delete_file( WP_CONTENT_DIR . '/false-positive.php' );
506 break;
507 default:
508 break;
509 }
510 }
511
512 /**
513 * Clears the audit log from Database.
514 * <command> reset
515 *
516 * @param mixed $args Command arguments.
517 */
518 public function audit( $args ) {
519 if ( defender_is_wp_org_version() ) {
520 WP_CLI::warning( 'A WPMU DEV subscription is required to use this command.' );
521 return;
522 }
523 }
524
525 /**
526 * Starts a full scan based on the provided options.
527 *
528 * @param array $options Command options.
529 */
530 private function scan_all( $options ) {
531 $type = $options['type'] ?? null;
532 $is_detailed = false;
533 switch ( $type ) {
534 case null:
535 // All items.
536 $type = null;
537 break;
538 case 'detailed':
539 $is_detailed = true;
540 break;
541 default:
542 WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
543 break;
544 }
545 WP_CLI::log( 'Check if there is a scan ongoing...' );
546 $scan = Model_Scan::get_active();
547 if ( ! is_object( $scan ) ) {
548 WP_CLI::log( 'No active scan, creating...' );
549 $scan = Model_Scan::create();
550 if ( is_wp_error( $scan ) ) {
551 return WP_CLI::error( $scan->get_error_message() );
552 }
553 wd_di()->get( Scan_Component::class )->gather_actioned_plugin_details();
554 } else {
555 WP_CLI::log( 'Continue from last scan' );
556 }
557 // Start detailed scan.
558 if ( $is_detailed ) {
559 $start = microtime( true );
560 }
561 $handler = wd_di()->get( Scan_Component::class );
562 $ret = false;
563 while ( $handler->process() === false ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedWhile
564 }
565 $scan = Model_Scan::get_last();
566 if ( ! is_object( $scan ) || is_wp_error( $scan ) ) {
567 return;
568 }
569 $results = $scan->to_array();
570 if ( is_array( $results ) && isset( $results['issues_items'] ) && array() !== $results['issues_items'] ) {
571 $count = is_array( $results['issues_items'] ) || $results['issues_items'] instanceof Countable
572 ? count( $results['issues_items'] )
573 : 0;
574 // Finish detailed scan.
575 if ( $is_detailed ) {
576 format_items( 'table', $results['issues_items'], array( 'type', 'short_desc', 'full_path' ) );
577 WP_CLI::log( sprintf( 'Saved %d items.', $count ) );
578 $finish = microtime( true ) - $start;
579 WP_CLI::log( 'Scan takes ' . round( $finish, 2 ) . 's to process.' );
580 } else {
581 WP_CLI::log( sprintf( 'Found %d issues.', $count ) );
582 }
583 }
584 WP_CLI::success( 'All done!' );
585 }
586
587 /**
588 * This is a helper for Security header actions.
589 * #Options
590 * <command>
591 * : Value can be run - Check headers, or activate|deactivate all headers
592 * [--type=<type>]
593 * : Default is all
594 * ## EXAMPLES
595 * wp defender security_headers check
596 *
597 * @param mixed $args Command arguments.
598 *
599 * @throws ExitException|Exception If an invalid command is provided.
600 */
601 public function security_headers( $args ) {
602 if ( ! is_array( $args ) || array() === $args ) {
603 WP_CLI::error( 'Invalid command.' );
604
605 return;
606 }
607 $model = new Security_Headers();
608 if ( ! is_object( $model ) ) {
609 WP_CLI::error( 'Invalid model.' );
610
611 return;
612 }
613 [ $command ] = $args;
614 switch ( $command ) {
615 case 'check':
616 $i = 1;
617 foreach ( $model->get_headers() as $header ) {
618 $state = true === $header->check() ? 'enabled' : 'disabled';
619 WP_CLI::log( sprintf( '#%s - %s is %s', $i, $header->get_title(), $state ) );
620 ++$i;
621 }
622 WP_CLI::success( 'Checking is ready.' );
623 break;
624 case 'activate':
625 foreach ( $model->get_headers() as $header ) {
626 $model->{$header::$rule_slug} = true;
627 }
628 $model->save();
629 WP_CLI::log( 'Activating is ready.' );
630 break;
631 case 'deactivate':
632 foreach ( $model->get_headers() as $header ) {
633 $model->{$header::$rule_slug} = false;
634 }
635 $model->save();
636 WP_CLI::log( 'Deactivating is ready.' );
637 break;
638 default:
639 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
640 break;
641 }
642 }
643
644 /**
645 * This is a helper command to reset plugin settings.
646 * #Options
647 * <command>
648 * Only allowed value is reset.
649 * Syntax: wp defender settings <command>
650 * Example: wp defender settings reset
651 *
652 * @param mixed $args Command arguments.
653 * @param mixed $options Command options.
654 */
655 public function settings( $args, $options ) {
656 if ( ! is_array( $args ) || array() === $args ) {
657 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
658 WP_CLI::runcommand( 'defender settings --help' );
659
660 return;
661 }
662
663 [ $command ] = $args;
664 switch ( $command ) {
665 case 'reset':
666 WP_CLI::confirm(
667 'This will completely reset the plugin settings, are you sure to continue?',
668 $options
669 );
670 // Analog Settings > Reset Settings.
671 wd_di()->get( Login_Access::class )->remove_settings();
672 wd_di()->get( Dashboard::class )->remove_settings();
673 wd_di()->get( Security_Tweaks::class )->remove_settings();
674 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
675 // Parent and submodules.
676 wd_di()->get( \WP_Defender\Controller\Firewall::class )->remove_settings();
677
678 wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings();
679 wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings();
680 wd_di()->get( Two_Factor::class )->remove_settings();
681 wd_di()->get( Main_Setting::class )->remove_settings();
682 WP_CLI::log( 'All cleared!' );
683 break;
684 default:
685 WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
686 WP_CLI::runcommand( 'defender settings --help' );
687 break;
688 }
689 }
690
691 /**
692 * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
693 * Syntax: wp defender firewall <command> <args_1> <args_2>
694 * <command> clear|unblock|list|activate|deactivate
695 *
696 * <args_1> Allowed values are: ip, user_agent, files and maxmind
697 * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist, lockout and license_key
698 * Example: wp defender firewall clear ip allowlist
699 * Example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
700 * Example: wp defender firewall list user_agent <status>
701 * Example: wp defender firewall activate submodule <submodule>
702 * Example: wp defender firewall deactivate submodule login_protection
703 * <status> Allowed values are: all, allowlist, blocklist.
704 * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
705 *
706 * @param mixed $args Command arguments.
707 * @param mixed $options Command options.
708 */
709 public function firewall( $args, $options ) {
710 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) <= 2 ) {
711 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
712 WP_CLI::runcommand( 'defender firewall --help' );
713
714 return;
715 }
716
717 [ $command, $type, $field ] = $args;
718 if ( ! is_string( $type ) || '' === $type || ! is_string( $field ) || '' === $field ) {
719 WP_CLI::log( 'Invalid option.' );
720 WP_CLI::runcommand( 'defender firewall --help' );
721
722 return;
723 }
724 switch ( $command ) {
725 case 'clear':
726 $this->clear_firewall( $type, $field );
727 break;
728 case 'unblock':
729 $this->unblock_firewall( $type, $field, $options );
730 break;
731 case 'list':
732 $this->list_firewall( $type, $field );
733 break;
734 case 'activate':
735 $this->toggle_firewall_submodule( $type, $field, 'activate' );
736 break;
737 case 'deactivate':
738 $this->toggle_firewall_submodule( $type, $field, 'deactivate' );
739 break;
740 default:
741 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
742 break;
743 }
744 }
745
746 /**
747 * This clears the mask login settings.
748 * <command> clear
749 * This command must have this command
750 * Syntax: wp defender mask_login <command>
751 * Example: wp defender mask_login clear
752 *
753 * @param mixed $args Command arguments.
754 */
755 public function mask_login( $args ) {
756 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
757 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
758 WP_CLI::runcommand( 'defender mask_login --help' );
759
760 return;
761 }
762
763 [ $command ] = $args;
764 switch ( $command ) {
765 case 'clear':
766 wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
767 WP_CLI::log( 'Mask login settings cleared!' );
768 break;
769 default:
770 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
771 break;
772 }
773 }
774
775 /**
776 * Clears the firewall data based on the specified type and field.
777 *
778 * @param string $type The type of data to clear.
779 * @param string $field The specific field to clear.
780 */
781 private function clear_firewall( $type, $field ) {
782 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
783 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
784
785 if ( ! in_array( $type, $type_default, true ) ) {
786 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
787 WP_CLI::runcommand( 'defender firewall --help' );
788
789 return;
790 }
791
792 if ( ! in_array( $field, $field_default, true ) ) {
793 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
794 WP_CLI::runcommand( 'defender firewall --help' );
795
796 return;
797 }
798
799 // Rename the field's name to original model field name.
800 $original_field = $this->rename_field( $field );
801 if ( 'ip' === $type ) {
802 // Get the model instance.
803 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
804 $data = $model->export();
805 // Rename the field to match with the appropriate model field name.
806 $mod_field = $this->is_country( $original_field ) ? $original_field : 'ip_' . $original_field;
807 // Reset to default data with correct data type.
808 $default_data = $this->is_country( $original_field ) ? array() : '';
809 // Empty the $field option of field data.
810 $data[ $mod_field ] = $default_data;
811 $model->import( $data );
812 $model->save();
813 } elseif ( 'files' === $type ) {
814 // Get the model instance.
815 $model = wd_di()->get( Notfound_Lockout::class );
816 $data = $model->export();
817 // Empty the $field option of field data.
818 $data[ $original_field ] = '';
819 $model->import( $data );
820 $model->save();
821 } elseif ( 'user_agent' === $type ) {
822 $model = wd_di()->get( User_Agent_Lockout::class );
823 $data = $model->export();
824 $data[ $original_field ] = '';
825 $model->import( $data );
826 $model->save();
827 } elseif ( 'maxmind' === $type ) {
828 try {
829 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
830 if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
831 wp_delete_file( $model->geodb_path );
832 }
833 $model->maxmind_license_key = '';
834 $model->geodb_path = '';
835 $model->save();
836 } catch ( Throwable $th ) {
837 WP_CLI::log( $th->getMessage() );
838 }
839 }
840
841 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
842 }
843
844 /**
845 * Unblocks the specified IPs from the firewall.
846 *
847 * @param string $type The type of data to unblock.
848 * @param string $field The specific field to unblock.
849 * @param array $options Command options including IPs to unblock.
850 */
851 private function unblock_firewall( $type, $field, $options ) {
852 $type_default = array( 'ip' );
853 $field_default = array( 'lockout' );
854
855 if ( ! in_array( $type, $type_default, true ) ) {
856 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
857 WP_CLI::runcommand( 'defender firewall --help' );
858
859 return;
860 }
861
862 if ( ! in_array( $field, $field_default, true ) ) {
863 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
864 WP_CLI::runcommand( 'defender firewall --help' );
865
866 return;
867 }
868
869 if ( array_key_exists( 'ips', $options ) ) {
870 $ips = array_map( 'trim', explode( ',', $options['ips'] ) );
871 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
872
873 foreach ( $models as $model ) {
874 $model->status = Lockout_Ip::STATUS_NORMAL;
875 $model->save();
876 }
877 } else {
878 WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
879 WP_CLI::runcommand( 'defender firewall --help' );
880
881 return;
882 }
883
884 WP_CLI::log( sprintf( 'Firewall %s %s unblocked', str_replace( '_', ' ', $field ), $type ) );
885 }
886
887 /**
888 * Lists details for the firewall based on the specified type and field.
889 * Example: wp defender firewall list user_agent all
890 *
891 * @param string $type The type of data to list.
892 * @param string $field The specific field to list.
893 *
894 * @since v2.6.4. Add the details for User Agent Banning.
895 */
896 private function list_firewall( $type, $field ) {
897 $type_default = array( 'user_agent' );
898 $field_default = array( 'all', 'allowlist', 'blocklist' );
899 if ( ! in_array( $type, $type_default, true ) ) {
900 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
901 WP_CLI::runcommand( 'defender firewall --help' );
902
903 return;
904 }
905 if ( ! in_array( $field, $field_default, true ) ) {
906 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
907 WP_CLI::runcommand( 'defender firewall --help' );
908
909 return;
910 }
911 $model = wd_di()->get( User_Agent_Lockout::class );
912 $data = $model->export();
913 if ( 'all' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
914 WP_CLI::log( 'ALLOWLIST:' );
915 WP_CLI::log( $data['whitelist'] );
916 WP_CLI::log( 'BLOCKLIST:' );
917 WP_CLI::log( $data['blacklist'] );
918 } elseif ( 'allowlist' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] ) {
919 WP_CLI::log( $data['whitelist'] );
920 } elseif ( 'blocklist' === $field && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
921 WP_CLI::log( $data['blacklist'] );
922 } else {
923 WP_CLI::log( 'No data.' );
924 }
925 }
926
927 /**
928 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
929 * Example: wp defender firewall activate submodule user_agent
930 * Example: wp defender firewall deactivate submodule login_protection
931 *
932 * @param string $key_word The keyword to identify the action.
933 * @param string $submodule The submodule to toggle.
934 * @param string $action The action to perform (activate or deactivate).
935 */
936 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
937 if ( 'submodule' !== $key_word ) {
938 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
939 WP_CLI::runcommand( 'defender firewall --help' );
940
941 return;
942 }
943 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
944 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
945 WP_CLI::runcommand( 'defender firewall --help' );
946
947 return;
948 }
949 // Get submodule slug.
950 if ( 'login_protection' === $submodule ) {
951 $model = wd_di()->get( Login_Lockout::class );
952 $submodule = Login_Lockout::get_module_name();
953 } elseif ( '404_detection' === $submodule ) {
954 $model = wd_di()->get( Notfound_Lockout::class );
955 $submodule = Notfound_Lockout::get_module_name();
956 } else {
957 $model = wd_di()->get( User_Agent_Lockout::class );
958 $submodule = User_Agent_Lockout::get_module_name();
959 }
960 // Activate/deactivate submodule.
961 if ( 'activate' === $action ) {
962 $text = 'activated';
963 // Check if the submodule is not yet activated.
964 if ( true !== $model->enabled ) {
965 $model->enabled = true;
966 $model->save();
967 }
968 } else {
969 $text = 'deactivated';
970 // Check if the submodule is not yet deactivated.
971 if ( false !== $model->enabled ) {
972 $model->enabled = false;
973 $model->save();
974 }
975 }
976
977 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
978 }
979
980 /**
981 * Rename a field to its original model field name.
982 *
983 * @param string $field The field name to rename.
984 *
985 * @return string The renamed field name.
986 */
987 private function rename_field( $field ) {
988 if ( '' !== $field ) {
989 return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
990 }
991
992 return '';
993 }
994
995 /**
996 * Check if the specified field is related to country settings.
997 *
998 * @param string $field The field to check.
999 *
1000 * @return bool True if the field is related to country settings, false otherwise.
1001 */
1002 private function is_country( $field ) {
1003 return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
1004 }
1005
1006 /**
1007 * Force Bulk Password Reset.
1008 * <command>
1009 * : Value can be force|undo
1010 * Syntax: wp defender password_reset <command>
1011 * Example: wp defender password_reset force
1012 *
1013 * @param mixed $args Command arguments.
1014 */
1015 public function password_reset( $args ) {
1016 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1017 WP_CLI::log( 'Invalid command.' );
1018
1019 return;
1020 }
1021
1022 [ $command ] = $args;
1023 switch ( $command ) {
1024 case 'force':
1025 // Get the model instance.
1026 $model = wd_di()->get( Password_Reset::class );
1027 $model->expire_force = true;
1028 $model->force_time = time();
1029 $model->save();
1030 $message = sprintf(
1031 'Passwords created before %s are required to be reset upon next login.',
1032 $this->format_date_time( $model->force_time )
1033 );
1034 WP_CLI::log( $message );
1035 break;
1036 case 'undo':
1037 $model = wd_di()->get( Password_Reset::class );
1038 $model->expire_force = false;
1039 $model->save();
1040 WP_CLI::log( 'Passwords reset is no longer required.' );
1041 break;
1042 default:
1043 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1044 break;
1045 }
1046 }
1047
1048 /**
1049 * Clear completed action scheduler logs.
1050 */
1051 private function scan_clear_logs() {
1052 $scan_component = wd_di()->get( Scan_Component::class );
1053 $result = $scan_component::clear_logs();
1054 $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
1055
1056 WP_CLI::log( $message );
1057 }
1058
1059 /**
1060 * Delete old logs.
1061 * <command> delete
1062 * This command must have this command
1063 * Syntax: wp defender logs <command>
1064 * Example: wp defender logs delete
1065 *
1066 * @param mixed $args Command arguments.
1067 */
1068 public function logs( $args ) {
1069 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1070 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
1071 WP_CLI::runcommand( 'defender logs --help' );
1072
1073 return;
1074 }
1075
1076 [ $command ] = $args;
1077
1078 switch ( $command ) {
1079 case 'delete':
1080 $rotation_logger = wd_di()->get( Rotation_Logger::class );
1081 $rotation_logger->purge_old_log();
1082 WP_CLI::log( 'Logs older than a week have been deleted.' );
1083 break;
1084 default:
1085 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1086 break;
1087 }
1088 }
1089
1090 /**
1091 * This is a helper for Captcha actions.
1092 * Syntax: wp defender captcha <command>
1093 * <command> activate|deactivate|clear
1094 * Example: wp defender captcha activate
1095 *
1096 * @param mixed $args Command arguments.
1097 */
1098 public function captcha( $args ) {
1099 if ( ! is_array( $args ) || array() === $args ) {
1100 WP_CLI::error( 'Invalid command.' );
1101 WP_CLI::runcommand( 'defender captcha --help' );
1102
1103 return;
1104 }
1105 $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1106 [ $command ] = $args;
1107 switch ( $command ) {
1108 case 'activate':
1109 if ( true !== $model->enabled ) {
1110 $model->enabled = true;
1111 $model->save();
1112 }
1113 WP_CLI::log( 'CAPTCHA is activated.' );
1114 break;
1115 case 'deactivate':
1116 if ( false !== $model->enabled ) {
1117 $model->enabled = false;
1118 $model->save();
1119 }
1120 $model->save();
1121 WP_CLI::log( 'CAPTCHA is deactivated.' );
1122 break;
1123 case 'clear':
1124 $default_values = $model->get_default_values();
1125 $model->message = $default_values['message'];
1126 $model->language = 'automatic';
1127 $model->provider = 'recaptcha';
1128 $model->data_v2_checkbox = array(
1129 'key' => '',
1130 'secret' => '',
1131 'size' => 'normal',
1132 'style' => 'light',
1133 );
1134 $model->data_v2_invisible = array(
1135 'key' => '',
1136 'secret' => '',
1137 );
1138 $model->data_v3_recaptcha = array(
1139 'key' => '',
1140 'secret' => '',
1141 'threshold' => '0.5',
1142 );
1143 $model->data_turnstile = array(
1144 'key' => '',
1145 'secret' => '',
1146 'size' => 'normal',
1147 'style' => 'auto',
1148 'message' => $default_values['turnstile_message'],
1149 'language' => 'auto',
1150 );
1151 $model->locations = array();
1152 $model->detect_woo = false;
1153 $model->woo_checked_locations = array();
1154 $model->detect_buddypress = false;
1155 $model->buddypress_checked_locations = array();
1156 $model->disable_for_known_users = true;
1157 $model->save();
1158
1159 WP_CLI::log( 'CAPTCHA is cleared.' );
1160 break;
1161 default:
1162 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1163 WP_CLI::runcommand( 'defender captcha --help' );
1164 break;
1165 }
1166 }
1167 }
1168