PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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 / cli.php

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

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