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

1,062 lines 32.5 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\Tutorial;
24 use WP_Defender\Controller\Dashboard;
25 use WP_Defender\Controller\Two_Factor;
26 use WP_Defender\Model\Scan as Model_Scan;
27 use WP_Defender\Controller\Main_Setting;
28 use WP_Defender\Controller\Audit_Logging;
29 use WP_Defender\Controller\Advanced_Tools;
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\Controller\Blocklist_Monitor;
34 use WP_Defender\Model\Setting\Notfound_Lockout;
35 use WP_Defender\Model\Setting\Security_Headers;
36 use WP_Defender\Model\Setting\User_Agent_Lockout;
37 use WP_Defender\Component\Logger\Rotation_Logger;
38 use function WP_CLI\Utils\format_items;
39
40 if ( ! defined( 'WPINC' ) ) {
41 die;
42 }
43
44 /**
45 * Class Cli
46 */
47 class Cli {
48
49 use Formats {
50 calculate_date_interval as protected;
51 format_bytes_into_readable as protected;
52 format_date_time as protected;
53 get_date as protected;
54 get_days_of_week as protected;
55 get_times as protected;
56 get_timezone_string as protected;
57 local_to_utc as protected;
58 moment_datetime_format_from as protected;
59 persistent_hub_datetime_format as protected;
60 time_since as protected;
61 }
62 use Theme;
63
64 /**
65 * This is a helper for scan module.
66 * #Options
67 * <command>
68 * : Value can be run - Perform a scan, e.g. 'run'-command or 'run ----type=detailed' for detailed result,
69 * or (un)ignore|delete|resolve to do the relevant task,
70 * or clear_logs to remove completed schedule logs.
71 * [--type=<type>]
72 * : Default, without values, is for all items, or core_integrity|plugin_integrity|vulnerability|suspicious_code.
73 *
74 * @param mixed $args Command arguments.
75 * @param mixed $options Command options.
76 *
77 * @throws ExitException If an invalid command is provided.
78 */
79 public function scan( $args, $options ) {
80 if ( empty( $args ) ) {
81 WP_CLI::error( 'Invalid command' );
82
83 return;
84 }
85 [ $command ] = $args;
86 switch ( $command ) {
87 case 'run':
88 $this->scan_all( $options );
89 break;
90 case 'clear_logs':
91 $this->scan_clear_logs();
92 break;
93 default:
94 $commands = array(
95 'ignore',
96 'unignore',
97 'resolve',
98 'delete',
99 );
100 if ( in_array( $command, $commands, true ) ) {
101 WP_CLI::confirm(
102 'This can cause your site get fatal error and can\'t restore back unless you have a backup, are you sure to continue?',
103 $options
104 );
105 $this->scan_task( $command, $options );
106 } else {
107 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
108 }
109 break;
110 }
111 }
112
113 /**
114 * Executes tasks based on the type of scan.
115 *
116 * @param mixed $task The task to perform.
117 * @param mixed $options Command options.
118 */
119 private function scan_task( $task, $options ) {
120 $type = $options['type'] ?? null;
121 switch ( $type ) {
122 case null:
123 // All items.
124 $type = null;
125 break;
126 case 'core_integrity':
127 $type = Scan_Item::TYPE_INTEGRITY;
128 break;
129 case 'plugin_integrity':
130 $type = Scan_Item::TYPE_PLUGIN_CHECK;
131 break;
132 case 'vulnerability':
133 $type = Scan_Item::TYPE_VULNERABILITY;
134 break;
135 case 'suspicious_code':
136 $type = Scan_Item::TYPE_SUSPICIOUS;
137 break;
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( Tutorial::class )->remove_settings();
582 wd_di()->get( Two_Factor::class )->remove_settings();
583 wd_di()->get( Blocklist_Monitor::class )->remove_settings();
584 wd_di()->get( Main_Setting::class )->remove_settings();
585 WP_CLI::log( 'All cleared!' );
586 break;
587 default:
588 WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
589 WP_CLI::runcommand( 'defender settings --help' );
590 break;
591 }
592 }
593
594 /**
595 * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
596 * Syntax: wp defender firewall <command> <args_1> <args_2>
597 * <command> clear|unblock|list|activate|deactivate
598 *
599 * <args_1> Allowed values are: ip, user_agent, files and maxmind
600 * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist, lockout and license_key
601 * Example: wp defender firewall clear ip allowlist
602 * Example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
603 * Example: wp defender firewall list user_agent <status>
604 * Example: wp defender firewall activate submodule <submodule>
605 * Example: wp defender firewall deactivate submodule login_protection
606 * <status> Allowed values are: all, allowlist, blocklist.
607 * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
608 *
609 * @param mixed $args Command arguments.
610 * @param mixed $options Command options.
611 */
612 public function firewall( $args, $options ) {
613 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) <= 2 ) {
614 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
615 WP_CLI::runcommand( 'defender firewall --help' );
616
617 return;
618 }
619
620 [ $command, $type, $field ] = $args;
621 if ( empty( $type ) || empty( $field ) ) {
622 WP_CLI::log( 'Invalid option.' );
623 WP_CLI::runcommand( 'defender firewall --help' );
624
625 return;
626 }
627 switch ( $command ) {
628 case 'clear':
629 $this->clear_firewall( $type, $field );
630 break;
631 case 'unblock':
632 $this->unblock_firewall( $type, $field, $options );
633 break;
634 case 'list':
635 $this->list_firewall( $type, $field );
636 break;
637 case 'activate':
638 $this->toggle_firewall_submodule( $type, $field, 'activate' );
639 break;
640 case 'deactivate':
641 $this->toggle_firewall_submodule( $type, $field, 'deactivate' );
642 break;
643 default:
644 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
645 break;
646 }
647 }
648
649 /**
650 * This clears the mask login settings.
651 * <command> clear
652 * This command must have this command
653 * Syntax: wp defender mask_login <command>
654 * Example: wp defender mask_login clear
655 *
656 * @param mixed $args Command arguments.
657 */
658 public function mask_login( $args ) {
659 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
660 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
661 WP_CLI::runcommand( 'defender mask_login --help' );
662
663 return;
664 }
665
666 [ $command ] = $args;
667 switch ( $command ) {
668 case 'clear':
669 wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
670 WP_CLI::log( 'Mask login settings cleared!' );
671 break;
672 default:
673 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
674 break;
675 }
676 }
677
678 /**
679 * Clears the firewall data based on the specified type and field.
680 *
681 * @param string $type The type of data to clear.
682 * @param string $field The specific field to clear.
683 */
684 private function clear_firewall( $type, $field ) {
685 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
686 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
687
688 if ( ! in_array( $type, $type_default, true ) ) {
689 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
690 WP_CLI::runcommand( 'defender firewall --help' );
691
692 return;
693 }
694
695 if ( ! in_array( $field, $field_default, true ) ) {
696 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
697 WP_CLI::runcommand( 'defender firewall --help' );
698
699 return;
700 }
701
702 // Rename the field's name to original model field name.
703 $original_field = $this->rename_field( $field );
704 if ( 'ip' === $type ) {
705 // Get the model instance.
706 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
707 $data = $model->export();
708 // Rename the field to match with the appropriate model field name.
709 $mod_field = $this->is_country( $original_field ) ? $original_field : 'ip_' . $original_field;
710 // Reset to default data with correct data type.
711 $default_data = $this->is_country( $original_field ) ? array() : '';
712 // Empty the $field option of field data.
713 $data[ $mod_field ] = $default_data;
714 $model->import( $data );
715 $model->save();
716 } elseif ( 'files' === $type ) {
717 // Get the model instance.
718 $model = wd_di()->get( Notfound_Lockout::class );
719 $data = $model->export();
720 // Empty the $field option of field data.
721 $data[ $original_field ] = '';
722 $model->import( $data );
723 $model->save();
724 } elseif ( 'user_agent' === $type ) {
725 $model = wd_di()->get( User_Agent_Lockout::class );
726 $data = $model->export();
727 $data[ $original_field ] = '';
728 $model->import( $data );
729 $model->save();
730 } elseif ( 'maxmind' === $type ) {
731 try {
732 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
733 if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
734 wp_delete_file( $model->geodb_path );
735 }
736 $model->maxmind_license_key = '';
737 $model->geodb_path = '';
738 $model->save();
739 } catch ( Throwable $th ) {
740 WP_CLI::log( $th->getMessage() );
741 }
742 }
743
744 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
745 }
746
747 /**
748 * Unblocks the specified IPs from the firewall.
749 *
750 * @param string $type The type of data to unblock.
751 * @param string $field The specific field to unblock.
752 * @param array $options Command options including IPs to unblock.
753 */
754 private function unblock_firewall( $type, $field, $options ) {
755 $type_default = array( 'ip' );
756 $field_default = array( 'lockout' );
757
758 if ( ! in_array( $type, $type_default, true ) ) {
759 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
760 WP_CLI::runcommand( 'defender firewall --help' );
761
762 return;
763 }
764
765 if ( ! in_array( $field, $field_default, true ) ) {
766 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
767 WP_CLI::runcommand( 'defender firewall --help' );
768
769 return;
770 }
771
772 if ( array_key_exists( 'ips', $options ) ) {
773 $ips = array_map( 'trim', explode( ',', $options['ips'] ) );
774 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
775
776 foreach ( $models as $model ) {
777 $model->status = Lockout_Ip::STATUS_NORMAL;
778 $model->save();
779 }
780 } else {
781 WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
782 WP_CLI::runcommand( 'defender firewall --help' );
783
784 return;
785 }
786
787 WP_CLI::log( sprintf( 'Firewall %s %s unblocked', str_replace( '_', ' ', $field ), $type ) );
788 }
789
790 /**
791 * Lists details for the firewall based on the specified type and field.
792 * Example: wp defender firewall list user_agent all
793 *
794 * @param string $type The type of data to list.
795 * @param string $field The specific field to list.
796 *
797 * @since v2.6.4. Add the details for User Agent Banning.
798 */
799 private function list_firewall( $type, $field ) {
800 $type_default = array( 'user_agent' );
801 $field_default = array( 'all', 'allowlist', 'blocklist' );
802 if ( ! in_array( $type, $type_default, true ) ) {
803 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
804 WP_CLI::runcommand( 'defender firewall --help' );
805
806 return;
807 }
808 if ( ! in_array( $field, $field_default, true ) ) {
809 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
810 WP_CLI::runcommand( 'defender firewall --help' );
811
812 return;
813 }
814 $model = wd_di()->get( User_Agent_Lockout::class );
815 $data = $model->export();
816 if ( 'all' === $field && ! empty( $data['whitelist'] ) && ! empty( $data['blacklist'] ) ) {
817 WP_CLI::log( 'ALLOWLIST:' );
818 WP_CLI::log( $data['whitelist'] );
819 WP_CLI::log( 'BLOCKLIST:' );
820 WP_CLI::log( $data['blacklist'] );
821 } elseif ( 'allowlist' === $field && ! empty( $data['whitelist'] ) ) {
822 WP_CLI::log( $data['whitelist'] );
823 } elseif ( 'blocklist' === $field && ! empty( $data['blacklist'] ) ) {
824 WP_CLI::log( $data['blacklist'] );
825 } else {
826 WP_CLI::log( 'No data.' );
827 }
828 }
829
830 /**
831 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
832 * Example: wp defender firewall activate submodule user_agent
833 * Example: wp defender firewall deactivate submodule login_protection
834 *
835 * @param string $key_word The keyword to identify the action.
836 * @param string $submodule The submodule to toggle.
837 * @param string $action The action to perform (activate or deactivate).
838 */
839 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
840 if ( 'submodule' !== $key_word ) {
841 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
842 WP_CLI::runcommand( 'defender firewall --help' );
843
844 return;
845 }
846 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
847 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
848 WP_CLI::runcommand( 'defender firewall --help' );
849
850 return;
851 }
852 // Get submodule slug.
853 if ( 'login_protection' === $submodule ) {
854 $model = wd_di()->get( Login_Lockout::class );
855 $submodule = Login_Lockout::get_module_name();
856 } elseif ( '404_detection' === $submodule ) {
857 $model = wd_di()->get( Notfound_Lockout::class );
858 $submodule = Notfound_Lockout::get_module_name();
859 } else {
860 $model = wd_di()->get( User_Agent_Lockout::class );
861 $submodule = User_Agent_Lockout::get_module_name();
862 }
863 // Activate/deactivate submodule.
864 if ( 'activate' === $action ) {
865 $text = 'activated';
866 // Check if the submodule is not yet activated.
867 if ( true !== $model->enabled ) {
868 $model->enabled = true;
869 $model->save();
870 }
871 } else {
872 $text = 'deactivated';
873 // Check if the submodule is not yet deactivated.
874 if ( false !== $model->enabled ) {
875 $model->enabled = false;
876 $model->save();
877 }
878 }
879
880 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
881 }
882
883 /**
884 * Rename a field to its original model field name.
885 *
886 * @param string $field The field name to rename.
887 *
888 * @return string The renamed field name.
889 */
890 private function rename_field( $field ) {
891 if ( ! empty( $field ) ) {
892 return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
893 }
894
895 return '';
896 }
897
898 /**
899 * Check if the specified field is related to country settings.
900 *
901 * @param string $field The field to check.
902 *
903 * @return bool True if the field is related to country settings, false otherwise.
904 */
905 private function is_country( $field ) {
906 return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
907 }
908
909 /**
910 * Force Bulk Password Reset.
911 * <command>
912 * : Value can be force|undo
913 * Syntax: wp defender password_reset <command>
914 * Example: wp defender password_reset force
915 *
916 * @param mixed $args Command arguments.
917 */
918 public function password_reset( $args ) {
919 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
920 WP_CLI::log( 'Invalid command.' );
921
922 return;
923 }
924
925 [ $command ] = $args;
926 switch ( $command ) {
927 case 'force':
928 // Get the model instance.
929 $model = wd_di()->get( Password_Reset::class );
930 $model->expire_force = true;
931 $model->force_time = time();
932 $model->save();
933 $message = sprintf(
934 'Passwords created before %s are required to be reset upon next login.',
935 $this->format_date_time( $model->force_time )
936 );
937 WP_CLI::log( $message );
938 break;
939 case 'undo':
940 $model = wd_di()->get( Password_Reset::class );
941 $model->expire_force = false;
942 $model->save();
943 WP_CLI::log( 'Passwords reset is no longer required.' );
944 break;
945 default:
946 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
947 break;
948 }
949 }
950
951 /**
952 * Clear completed action scheduler logs.
953 */
954 private function scan_clear_logs() {
955 $scan_component = wd_di()->get( Scan::class );
956 $result = $scan_component::clear_logs();
957 $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
958
959 WP_CLI::log( $message );
960 }
961
962 /**
963 * Delete old logs.
964 * <command> delete
965 * This command must have this command
966 * Syntax: wp defender logs <command>
967 * Example: wp defender logs delete
968 *
969 * @param mixed $args Command arguments.
970 */
971 public function logs( $args ) {
972 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
973 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
974 WP_CLI::runcommand( 'defender logs --help' );
975
976 return;
977 }
978
979 [ $command ] = $args;
980
981 switch ( $command ) {
982 case 'delete':
983 $rotation_logger = wd_di()->get( Rotation_Logger::class );
984 $rotation_logger->purge_old_log();
985 WP_CLI::log( 'Logs older than a week have been deleted.' );
986 break;
987 default:
988 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
989 break;
990 }
991 }
992
993 /**
994 * This is a helper for Google Recaptcha actions.
995 * Syntax: wp defender google_recaptcha <command>
996 * <command> activate|deactivate|clear
997 * Example: wp defender google_recaptcha activate
998 *
999 * @param mixed $args Command arguments.
1000 */
1001 public function google_recaptcha( $args ) {
1002 if ( empty( $args ) ) {
1003 WP_CLI::error( 'Invalid command.' );
1004 WP_CLI::runcommand( 'defender google_recaptcha --help' );
1005
1006 return;
1007 }
1008 $model = wd_di()->get( \WP_Defender\Model\Setting\Recaptcha::class );
1009 [ $command ] = $args;
1010 switch ( $command ) {
1011 case 'activate':
1012 if ( true !== $model->enabled ) {
1013 $model->enabled = true;
1014 $model->save();
1015 }
1016 WP_CLI::log( 'Google reCAPTCHA is activated.' );
1017 break;
1018 case 'deactivate':
1019 if ( false !== $model->enabled ) {
1020 $model->enabled = false;
1021 $model->save();
1022 }
1023 $model->save();
1024 WP_CLI::log( 'Google reCAPTCHA is deactivated.' );
1025 break;
1026 case 'clear':
1027 $default_values = $model->get_default_values();
1028 $model->message = $default_values['message'];
1029 $model->language = 'automatic';
1030 $model->data_v2_checkbox = array(
1031 'key' => '',
1032 'secret' => '',
1033 'size' => 'normal',
1034 'style' => 'light',
1035 );
1036 $model->data_v2_invisible = array(
1037 'key' => '',
1038 'secret' => '',
1039 );
1040 $model->data_v3_recaptcha = array(
1041 'key' => '',
1042 'secret' => '',
1043 'threshold' => '0.5',
1044 );
1045 $model->locations = array();
1046 $model->detect_woo = false;
1047 $model->woo_checked_locations = array();
1048 $model->detect_buddypress = false;
1049 $model->buddypress_checked_locations = array();
1050 $model->disable_for_known_users = true;
1051 $model->save();
1052
1053 WP_CLI::log( 'Google reCAPTCHA is cleared.' );
1054 break;
1055 default:
1056 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1057 WP_CLI::runcommand( 'defender google_recaptcha --help' );
1058 break;
1059 }
1060 }
1061 }
1062