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

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