PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.0
Defender Security – Malware Scanner, Login Security & Firewall v5.7.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-cli.php

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

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