PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.9.0
Defender Security – Malware Scanner, Login Security & Firewall v5.9.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-cli.php

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

1,172 lines 36.3 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 // @phpstan-ignore-next-line
627 $model->{$header::$rule_slug} = true;
628 }
629 $model->save();
630 WP_CLI::log( 'Activating is ready.' );
631 break;
632 case 'deactivate':
633 foreach ( $model->get_headers() as $header ) {
634 // @phpstan-ignore-next-line
635 $model->{$header::$rule_slug} = false;
636 }
637 $model->save();
638 WP_CLI::log( 'Deactivating is ready.' );
639 break;
640 default:
641 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
642 break;
643 }
644 }
645
646 /**
647 * This is a helper command to reset plugin settings.
648 * #Options
649 * <command>
650 * Only allowed value is reset.
651 * Syntax: wp defender settings <command>
652 * Example: wp defender settings reset
653 *
654 * @param mixed $args Command arguments.
655 * @param mixed $options Command options.
656 */
657 public function settings( $args, $options ) {
658 if ( ! is_array( $args ) || array() === $args ) {
659 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
660 WP_CLI::runcommand( 'defender settings --help' );
661
662 return;
663 }
664
665 [ $command ] = $args;
666 switch ( $command ) {
667 case 'reset':
668 WP_CLI::confirm(
669 'This will completely reset the plugin settings, are you sure to continue?',
670 $options
671 );
672 // Analog Settings > Reset Settings.
673 wd_di()->get( Advanced_Tools::class )->remove_settings();
674 wd_di()->get( Audit_Logging::class )->remove_settings();
675 wd_di()->get( Dashboard::class )->remove_settings();
676 wd_di()->get( Security_Tweaks::class )->remove_settings();
677 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
678 // Parent and submodules.
679 wd_di()->get( \WP_Defender\Controller\Firewall::class )->remove_settings();
680
681 wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings();
682 wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings();
683 wd_di()->get( Two_Factor::class )->remove_settings();
684 wd_di()->get( Blocklist_Monitor::class )->remove_settings();
685 wd_di()->get( Main_Setting::class )->remove_settings();
686 WP_CLI::log( 'All cleared!' );
687 break;
688 default:
689 WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
690 WP_CLI::runcommand( 'defender settings --help' );
691 break;
692 }
693 }
694
695 /**
696 * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
697 * Syntax: wp defender firewall <command> <args_1> <args_2>
698 * <command> clear|unblock|list|activate|deactivate
699 *
700 * <args_1> Allowed values are: ip, user_agent, files and maxmind
701 * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist, lockout and license_key
702 * Example: wp defender firewall clear ip allowlist
703 * Example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
704 * Example: wp defender firewall list user_agent <status>
705 * Example: wp defender firewall activate submodule <submodule>
706 * Example: wp defender firewall deactivate submodule login_protection
707 * <status> Allowed values are: all, allowlist, blocklist.
708 * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
709 *
710 * @param mixed $args Command arguments.
711 * @param mixed $options Command options.
712 */
713 public function firewall( $args, $options ) {
714 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) <= 2 ) {
715 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
716 WP_CLI::runcommand( 'defender firewall --help' );
717
718 return;
719 }
720
721 [ $command, $type, $field ] = $args;
722 if ( ! is_string( $type ) || '' === $type || ! is_string( $field ) || '' === $field ) {
723 WP_CLI::log( 'Invalid option.' );
724 WP_CLI::runcommand( 'defender firewall --help' );
725
726 return;
727 }
728 switch ( $command ) {
729 case 'clear':
730 $this->clear_firewall( $type, $field );
731 break;
732 case 'unblock':
733 $this->unblock_firewall( $type, $field, $options );
734 break;
735 case 'list':
736 $this->list_firewall( $type, $field );
737 break;
738 case 'activate':
739 $this->toggle_firewall_submodule( $type, $field, 'activate' );
740 break;
741 case 'deactivate':
742 $this->toggle_firewall_submodule( $type, $field, 'deactivate' );
743 break;
744 default:
745 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
746 break;
747 }
748 }
749
750 /**
751 * This clears the mask login settings.
752 * <command> clear
753 * This command must have this command
754 * Syntax: wp defender mask_login <command>
755 * Example: wp defender mask_login clear
756 *
757 * @param mixed $args Command arguments.
758 */
759 public function mask_login( $args ) {
760 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
761 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
762 WP_CLI::runcommand( 'defender mask_login --help' );
763
764 return;
765 }
766
767 [ $command ] = $args;
768 switch ( $command ) {
769 case 'clear':
770 wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
771 WP_CLI::log( 'Mask login settings cleared!' );
772 break;
773 default:
774 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
775 break;
776 }
777 }
778
779 /**
780 * Clears the firewall data based on the specified type and field.
781 *
782 * @param string $type The type of data to clear.
783 * @param string $field The specific field to clear.
784 */
785 private function clear_firewall( $type, $field ) {
786 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
787 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
788
789 if ( ! in_array( $type, $type_default, true ) ) {
790 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
791 WP_CLI::runcommand( 'defender firewall --help' );
792
793 return;
794 }
795
796 if ( ! in_array( $field, $field_default, true ) ) {
797 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
798 WP_CLI::runcommand( 'defender firewall --help' );
799
800 return;
801 }
802
803 // Rename the field's name to original model field name.
804 $original_field = $this->rename_field( $field );
805 if ( 'ip' === $type ) {
806 // Get the model instance.
807 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
808 $data = $model->export();
809 // Rename the field to match with the appropriate model field name.
810 $mod_field = $this->is_country( $original_field ) ? $original_field : 'ip_' . $original_field;
811 // Reset to default data with correct data type.
812 $default_data = $this->is_country( $original_field ) ? array() : '';
813 // Empty the $field option of field data.
814 $data[ $mod_field ] = $default_data;
815 $model->import( $data );
816 $model->save();
817 } elseif ( 'files' === $type ) {
818 // Get the model instance.
819 $model = wd_di()->get( Notfound_Lockout::class );
820 $data = $model->export();
821 // Empty the $field option of field data.
822 $data[ $original_field ] = '';
823 $model->import( $data );
824 $model->save();
825 } elseif ( 'user_agent' === $type ) {
826 $model = wd_di()->get( User_Agent_Lockout::class );
827 $data = $model->export();
828 $data[ $original_field ] = '';
829 $model->import( $data );
830 $model->save();
831 } elseif ( 'maxmind' === $type ) {
832 try {
833 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
834 if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
835 wp_delete_file( $model->geodb_path );
836 }
837 $model->maxmind_license_key = '';
838 $model->geodb_path = '';
839 $model->save();
840 } catch ( Throwable $th ) {
841 WP_CLI::log( $th->getMessage() );
842 }
843 }
844
845 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
846 }
847
848 /**
849 * Unblocks the specified IPs from the firewall.
850 *
851 * @param string $type The type of data to unblock.
852 * @param string $field The specific field to unblock.
853 * @param array $options Command options including IPs to unblock.
854 */
855 private function unblock_firewall( $type, $field, $options ) {
856 $type_default = array( 'ip' );
857 $field_default = array( 'lockout' );
858
859 if ( ! in_array( $type, $type_default, true ) ) {
860 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
861 WP_CLI::runcommand( 'defender firewall --help' );
862
863 return;
864 }
865
866 if ( ! in_array( $field, $field_default, true ) ) {
867 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
868 WP_CLI::runcommand( 'defender firewall --help' );
869
870 return;
871 }
872
873 if ( array_key_exists( 'ips', $options ) ) {
874 $ips = array_map( 'trim', explode( ',', $options['ips'] ) );
875 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
876
877 foreach ( $models as $model ) {
878 $model->status = Lockout_Ip::STATUS_NORMAL;
879 $model->save();
880 }
881 } else {
882 WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
883 WP_CLI::runcommand( 'defender firewall --help' );
884
885 return;
886 }
887
888 WP_CLI::log( sprintf( 'Firewall %s %s unblocked', str_replace( '_', ' ', $field ), $type ) );
889 }
890
891 /**
892 * Lists details for the firewall based on the specified type and field.
893 * Example: wp defender firewall list user_agent all
894 *
895 * @param string $type The type of data to list.
896 * @param string $field The specific field to list.
897 *
898 * @since v2.6.4. Add the details for User Agent Banning.
899 */
900 private function list_firewall( $type, $field ) {
901 $type_default = array( 'user_agent' );
902 $field_default = array( 'all', 'allowlist', 'blocklist' );
903 if ( ! in_array( $type, $type_default, true ) ) {
904 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
905 WP_CLI::runcommand( 'defender firewall --help' );
906
907 return;
908 }
909 if ( ! in_array( $field, $field_default, true ) ) {
910 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
911 WP_CLI::runcommand( 'defender firewall --help' );
912
913 return;
914 }
915 $model = wd_di()->get( User_Agent_Lockout::class );
916 $data = $model->export();
917 if ( 'all' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
918 WP_CLI::log( 'ALLOWLIST:' );
919 WP_CLI::log( $data['whitelist'] );
920 WP_CLI::log( 'BLOCKLIST:' );
921 WP_CLI::log( $data['blacklist'] );
922 } elseif ( 'allowlist' === $field && isset( $data['whitelist'] ) && '' !== $data['whitelist'] ) {
923 WP_CLI::log( $data['whitelist'] );
924 } elseif ( 'blocklist' === $field && isset( $data['blacklist'] ) && '' !== $data['blacklist'] ) {
925 WP_CLI::log( $data['blacklist'] );
926 } else {
927 WP_CLI::log( 'No data.' );
928 }
929 }
930
931 /**
932 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
933 * Example: wp defender firewall activate submodule user_agent
934 * Example: wp defender firewall deactivate submodule login_protection
935 *
936 * @param string $key_word The keyword to identify the action.
937 * @param string $submodule The submodule to toggle.
938 * @param string $action The action to perform (activate or deactivate).
939 */
940 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
941 if ( 'submodule' !== $key_word ) {
942 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
943 WP_CLI::runcommand( 'defender firewall --help' );
944
945 return;
946 }
947 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
948 WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
949 WP_CLI::runcommand( 'defender firewall --help' );
950
951 return;
952 }
953 // Get submodule slug.
954 if ( 'login_protection' === $submodule ) {
955 $model = wd_di()->get( Login_Lockout::class );
956 $submodule = Login_Lockout::get_module_name();
957 } elseif ( '404_detection' === $submodule ) {
958 $model = wd_di()->get( Notfound_Lockout::class );
959 $submodule = Notfound_Lockout::get_module_name();
960 } else {
961 $model = wd_di()->get( User_Agent_Lockout::class );
962 $submodule = User_Agent_Lockout::get_module_name();
963 }
964 // Activate/deactivate submodule.
965 if ( 'activate' === $action ) {
966 $text = 'activated';
967 // Check if the submodule is not yet activated.
968 if ( true !== $model->enabled ) {
969 $model->enabled = true;
970 $model->save();
971 }
972 } else {
973 $text = 'deactivated';
974 // Check if the submodule is not yet deactivated.
975 if ( false !== $model->enabled ) {
976 $model->enabled = false;
977 $model->save();
978 }
979 }
980
981 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
982 }
983
984 /**
985 * Rename a field to its original model field name.
986 *
987 * @param string $field The field name to rename.
988 *
989 * @return string The renamed field name.
990 */
991 private function rename_field( $field ) {
992 if ( '' !== $field ) {
993 return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
994 }
995
996 return '';
997 }
998
999 /**
1000 * Check if the specified field is related to country settings.
1001 *
1002 * @param string $field The field to check.
1003 *
1004 * @return bool True if the field is related to country settings, false otherwise.
1005 */
1006 private function is_country( $field ) {
1007 return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
1008 }
1009
1010 /**
1011 * Force Bulk Password Reset.
1012 * <command>
1013 * : Value can be force|undo
1014 * Syntax: wp defender password_reset <command>
1015 * Example: wp defender password_reset force
1016 *
1017 * @param mixed $args Command arguments.
1018 */
1019 public function password_reset( $args ) {
1020 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1021 WP_CLI::log( 'Invalid command.' );
1022
1023 return;
1024 }
1025
1026 [ $command ] = $args;
1027 switch ( $command ) {
1028 case 'force':
1029 // Get the model instance.
1030 $model = wd_di()->get( Password_Reset::class );
1031 $model->expire_force = true;
1032 $model->force_time = time();
1033 $model->save();
1034 $message = sprintf(
1035 'Passwords created before %s are required to be reset upon next login.',
1036 $this->format_date_time( $model->force_time )
1037 );
1038 WP_CLI::log( $message );
1039 break;
1040 case 'undo':
1041 $model = wd_di()->get( Password_Reset::class );
1042 $model->expire_force = false;
1043 $model->save();
1044 WP_CLI::log( 'Passwords reset is no longer required.' );
1045 break;
1046 default:
1047 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1048 break;
1049 }
1050 }
1051
1052 /**
1053 * Clear completed action scheduler logs.
1054 */
1055 private function scan_clear_logs() {
1056 $scan_component = wd_di()->get( Scan_Component::class );
1057 $result = $scan_component::clear_logs();
1058 $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
1059
1060 WP_CLI::log( $message );
1061 }
1062
1063 /**
1064 * Delete old logs.
1065 * <command> delete
1066 * This command must have this command
1067 * Syntax: wp defender logs <command>
1068 * Example: wp defender logs delete
1069 *
1070 * @param mixed $args Command arguments.
1071 */
1072 public function logs( $args ) {
1073 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1074 WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
1075 WP_CLI::runcommand( 'defender logs --help' );
1076
1077 return;
1078 }
1079
1080 [ $command ] = $args;
1081
1082 switch ( $command ) {
1083 case 'delete':
1084 $rotation_logger = wd_di()->get( Rotation_Logger::class );
1085 $rotation_logger->purge_old_log();
1086 WP_CLI::log( 'Logs older than a week have been deleted.' );
1087 break;
1088 default:
1089 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1090 break;
1091 }
1092 }
1093
1094 /**
1095 * This is a helper for Captcha actions.
1096 * Syntax: wp defender captcha <command>
1097 * <command> activate|deactivate|clear
1098 * Example: wp defender captcha activate
1099 *
1100 * @param mixed $args Command arguments.
1101 */
1102 public function captcha( $args ) {
1103 if ( ! is_array( $args ) || array() === $args ) {
1104 WP_CLI::error( 'Invalid command.' );
1105 WP_CLI::runcommand( 'defender captcha --help' );
1106
1107 return;
1108 }
1109 $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1110 [ $command ] = $args;
1111 switch ( $command ) {
1112 case 'activate':
1113 if ( true !== $model->enabled ) {
1114 $model->enabled = true;
1115 $model->save();
1116 }
1117 WP_CLI::log( 'CAPTCHA is activated.' );
1118 break;
1119 case 'deactivate':
1120 if ( false !== $model->enabled ) {
1121 $model->enabled = false;
1122 $model->save();
1123 }
1124 $model->save();
1125 WP_CLI::log( 'CAPTCHA is deactivated.' );
1126 break;
1127 case 'clear':
1128 $default_values = $model->get_default_values();
1129 $model->message = $default_values['message'];
1130 $model->language = 'automatic';
1131 $model->provider = 'recaptcha';
1132 $model->data_v2_checkbox = array(
1133 'key' => '',
1134 'secret' => '',
1135 'size' => 'normal',
1136 'style' => 'light',
1137 );
1138 $model->data_v2_invisible = array(
1139 'key' => '',
1140 'secret' => '',
1141 );
1142 $model->data_v3_recaptcha = array(
1143 'key' => '',
1144 'secret' => '',
1145 'threshold' => '0.5',
1146 );
1147 $model->data_turnstile = array(
1148 'key' => '',
1149 'secret' => '',
1150 'size' => 'normal',
1151 'style' => 'auto',
1152 'message' => $default_values['turnstile_message'],
1153 'language' => 'auto',
1154 );
1155 $model->locations = array();
1156 $model->detect_woo = false;
1157 $model->woo_checked_locations = array();
1158 $model->detect_buddypress = false;
1159 $model->buddypress_checked_locations = array();
1160 $model->disable_for_known_users = true;
1161 $model->save();
1162
1163 WP_CLI::log( 'CAPTCHA is cleared.' );
1164 break;
1165 default:
1166 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1167 WP_CLI::runcommand( 'defender captcha --help' );
1168 break;
1169 }
1170 }
1171 }
1172