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

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