PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.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 5.5.1, at src/component/class-cli.php

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