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

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

909 lines 26.8 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 // Finish detailed scan.
399 if ( $is_detailed ) {
400 $scan = Model_Scan::get_last();
401 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
402 $results = $scan->to_array();
403 if ( is_array( $results ) && ! empty( $results['issues_items'] ) ) {
404 format_items( 'table', $results['issues_items'], [ 'type', 'short_desc', 'full_path' ] );
405 \WP_CLI::log( sprintf( 'Saved %s items.', is_array($results['issues_items']) || $results['issues_items'] instanceof \Countable ? count( $results['issues_items'] ) : 0 ) );
406 }
407 }
408 $finish = microtime( true ) - $start;
409 \WP_CLI::log( 'Scan take ' . round( $finish, 2 ) . 's to process.' );
410 }
411 \WP_CLI::success( 'All done!' );
412 }
413
414 /**
415 *
416 * This is a helper for Security header actions.
417 * #Options
418 * <command>
419 * : Value can be run - Check headers, or activate|deactivate all headers
420 *
421 * [--type=<type>]
422 * : Default is all
423 *
424 * ## EXAMPLES
425 * wp defender security_headers check
426 *
427 * @param $args
428 * @param $options
429 *
430 * @throws \WP_CLI\ExitException
431 */
432 public function security_headers( $args, $options ) {
433 if ( empty( $args ) ) {
434 \WP_CLI::error( 'Invalid command.' );
435 }
436 $model = new \WP_Defender\Model\Setting\Security_Headers();
437 if ( ! is_object( $model ) ) {
438 \WP_CLI::error( 'Invalid model.' );
439
440 return;
441 }
442 [$command] = $args;
443 switch ( $command ) {
444 case 'check':
445 $i = 1;
446 foreach ( $model->get_headers() as $header ) {
447 $state = true === $header->check() ? 'enabled' : 'disabled';
448 \WP_CLI::log( sprintf( '#%s - %s is %s', $i, $header->get_title(), $state ) );
449 $i ++;
450 }
451 \WP_CLI::success( 'Checking is ready.' );
452 break;
453 case 'activate':
454 foreach ( $model->get_headers() as $header ) {
455 $model->{$header::$rule_slug} = true;
456 }
457 $model->save();
458 \WP_CLI::log( 'Activating is ready.' );
459 break;
460 case 'deactivate':
461 foreach ( $model->get_headers() as $header ) {
462 $model->{$header::$rule_slug} = false;
463 }
464 $model->save();
465 \WP_CLI::log( 'Deactivating is ready.' );
466 break;
467 default:
468 \WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
469 break;
470 }
471 }
472
473 /**
474 * This is a helper command to reset plugin settings.
475 * #Options
476 * <command>
477 * Only allowed value is reset.
478 *
479 * syntax: wp defender settings <command>
480 * example: wp defender settings reset
481 *
482 * @param $args
483 * @param $options
484 */
485 public function settings( $args, $options ) {
486 if ( empty( $args ) ) {
487 \WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
488 \WP_CLI::runcommand( 'defender settings --help' );
489
490 return;
491 }
492
493 [$command] = $args;
494 switch ( $command ) {
495 case 'reset':
496 \WP_CLI::confirm(
497 'This will completely reset the plugin settings, are you sure to continue?',
498 $options
499 );
500 // Analog Settings > Reset Settings.
501 wd_di()->get( \WP_Defender\Controller\Advanced_Tools::class )->remove_settings();
502 wd_di()->get( \WP_Defender\Controller\Audit_Logging::class )->remove_settings();
503 wd_di()->get( \WP_Defender\Controller\Dashboard::class )->remove_settings();
504 wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class )->remove_settings();
505 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
506 wd_di()->get( \WP_Defender\Controller\Firewall::class )->remove_settings();
507 wd_di()->get( \WP_Defender\Controller\Firewall_Logs::class )->remove_settings();
508 wd_di()->get( \WP_Defender\Controller\Login_Lockout::class )->remove_settings();
509 wd_di()->get( \WP_Defender\Controller\Nf_Lockout::class )->remove_settings();
510 wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings();
511 wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings();
512 wd_di()->get( \WP_Defender\Controller\Tutorial::class )->remove_settings();
513 wd_di()->get( \WP_Defender\Controller\Two_Factor::class )->remove_settings();
514 wd_di()->get( \WP_Defender\Controller\Blocklist_Monitor::class )->remove_settings();
515 wd_di()->get( \WP_Defender\Controller\Main_Setting::class )->remove_settings();
516 \WP_CLI::log( 'All cleared!' );
517 break;
518 default:
519 \WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
520 \WP_CLI::runcommand( 'defender settings --help' );
521 break;
522 }
523 }
524
525 /**
526 *
527 * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
528 *
529 * syntax: wp defender firewall <command> <args_1> <args_2>
530 * <command> clear|unblock|list|activate|deactivate
531 *
532 * <args_1> Allowed values are: ip, user_agent and files
533 * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist and lockout
534 *
535 * example: wp defender firewall clear ip allowlist
536 * example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
537 * example: wp defender firewall list user_agent <status>
538 * example: wp defender firewall activate submodule <submodule>
539 * example: wp defender firewall deactivate submodule login_protection
540 *
541 * <status> Allowed values are: all, allowlist, blocklist.
542 * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
543 *
544 * @param $args
545 * @param $options
546 */
547 public function firewall( $args, $options ) {
548 if ( (is_array($args) || $args instanceof \Countable ? count( $args ) : 0) <= 2 ) {
549 \WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
550 \WP_CLI::runcommand( 'defender firewall --help' );
551
552 return;
553 }
554
555 [$command, $type, $field] = $args;
556 if ( empty( $type ) || empty( $field ) ) {
557 \WP_CLI::log( 'Invalid option.' );
558 \WP_CLI::runcommand( 'defender firewall --help' );
559
560 return;
561 }
562 switch ( $command ) {
563 case 'clear':
564 $this->clear_firewall( $type, $field, $options );
565 break;
566 case 'unblock':
567 $this->unblock_firewall( $type, $field, $options );
568 break;
569 case 'list':
570 $this->list_firewall( $type, $field );
571 break;
572 case 'activate':
573 $this->toggle_firewall_submodule( $type, $field, 'activate' );
574 break;
575 case 'deactivate':
576 $this->toggle_firewall_submodule( $type, $field, 'deactivate' );
577 break;
578 default:
579 \WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
580 break;
581 }
582 }
583
584 /**
585 * This clears the mask login settings.
586 *
587 * <command> clear
588 * This command must have this command
589 *
590 * syntax: wp defender mask_login <command>
591 * example: wp defender mask_login clear
592 *
593 * @param $args
594 * @param $options
595 */
596 public function mask_login( $args, $options ) {
597 if ( (is_array($args) || $args instanceof \Countable ? count( $args ) : 0) < 1 ) {
598 \WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
599 \WP_CLI::runcommand( 'defender mask_login --help' );
600
601 return;
602 }
603
604 [$command] = $args;
605 switch ( $command ) {
606 case 'clear':
607 wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
608 \WP_CLI::log( 'Mask login settings cleared!' );
609 break;
610 default:
611 \WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
612 break;
613 }
614 }
615
616 /**
617 * Clear the firewall data with different options.
618 */
619 private function clear_firewall( $type, $field, $options ) {
620 $type_default = array( 'ip', 'files', 'user_agent' );
621 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist' );
622
623 if ( ! in_array( $type, $type_default, true ) ) {
624 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
625 \WP_CLI::runcommand( 'defender firewall --help' );
626
627 return;
628 }
629
630 if ( ! in_array( $field, $field_default, true ) ) {
631 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
632 \WP_CLI::runcommand( 'defender firewall --help' );
633
634 return;
635 }
636
637 // Rename the field's name to original model field name.
638 $original_field = $this->rename_field( $field );
639 if ( 'ip' === $type ) {
640 // Get the model instance.
641 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
642 $data = $model->export();
643 // Rename the field to match with the appropriate model field name.
644 $mod_field = $this->is_country( $original_field ) ? $original_field : 'ip_' . $original_field;
645 // Reset to default data with correct data type.
646 $default_data = $this->is_country( $original_field ) ? array() : '';
647 // Empty the $field option of field data.
648 $data[ $mod_field ] = $default_data;
649 $model->import( $data );
650 $model->save();
651 } elseif ( 'files' === $type ) {
652 // Get the model instance.
653 $model = wd_di()->get( \WP_Defender\Model\Setting\Notfound_Lockout::class );
654 $data = $model->export();
655 // Empty the $field option of field data.
656 $data[ $original_field ] = '';
657 $model->import( $data );
658 $model->save();
659 } elseif ( 'user_agent' === $type ) {
660 $model = wd_di()->get( \WP_Defender\Model\Setting\User_Agent_Lockout::class );
661 $data = $model->export();
662 $data[ $original_field ] = '';
663 $model->import( $data );
664 $model->save();
665 }
666
667 \WP_CLI::log( sprintf( 'Firewall %s %s cleared', str_replace( '_', ' ', $field ), $type ) );
668 }
669
670 /**
671 * Unblock the IP(s) from block list.
672 */
673 private function unblock_firewall( $type, $field, $options ) {
674 $type_default = array( 'ip' );
675 $field_default = array( 'lockout' );
676
677 if ( ! in_array( $type, $type_default, true ) ) {
678 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
679 \WP_CLI::runcommand( 'defender firewall --help' );
680
681 return;
682 }
683
684 if ( ! in_array( $field, $field_default, true ) ) {
685 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
686 \WP_CLI::runcommand( 'defender firewall --help' );
687
688 return;
689 }
690
691 if ( array_key_exists( 'ips', $options ) ) {
692 $ips = array_map( 'trim', explode( ',', $options['ips'] ) );
693 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
694
695 foreach ( $models as $model ) {
696 $model->status = Lockout_Ip::STATUS_NORMAL;
697 $model->save();
698 }
699 } else {
700 \WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
701 \WP_CLI::runcommand( 'defender firewall --help' );
702
703 return;
704 }
705
706 \WP_CLI::log( sprintf( 'Firewall %s %s unblocked', str_replace( '_', ' ', $field ), $type ) );
707 }
708
709 /**
710 * Get the details for Firewall submodules.
711 *
712 * example: wp defender firewall list user_agent all
713 *
714 * @since v2.6.4. Add the details for User Agent Banning.
715 * @param string $type
716 * @param string $field
717 */
718 private function list_firewall( $type, $field ) {
719 $type_default = array( 'user_agent' );
720 $field_default = array( 'all', 'allowlist', 'blocklist' );
721 if ( ! in_array( $type, $type_default, true ) ) {
722 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
723 \WP_CLI::runcommand( 'defender firewall --help' );
724
725 return;
726 }
727 if ( ! in_array( $field, $field_default, true ) ) {
728 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
729 \WP_CLI::runcommand( 'defender firewall --help' );
730
731 return;
732 }
733 $model = wd_di()->get( \WP_Defender\Model\Setting\User_Agent_Lockout::class );
734 $data = $model->export();
735 if ( 'all' === $field && ! empty( $data['whitelist'] ) && ! empty( $data['blacklist'] ) ) {
736 \WP_CLI::log( 'ALLOWLIST:' );
737 \WP_CLI::log( $data['whitelist'] );
738 \WP_CLI::log( 'BLOCKLIST:' );
739 \WP_CLI::log( $data['blacklist'] );
740 } elseif ( 'allowlist' === $field && ! empty( $data['whitelist'] ) ) {
741 \WP_CLI::log( $data['whitelist'] );
742 } elseif ( 'blocklist' === $field && ! empty( $data['blacklist'] ) ) {
743 \WP_CLI::log( $data['blacklist'] );
744 } else {
745 \WP_CLI::log( 'No data.' );
746 }
747 }
748
749 /**
750 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
751 *
752 * example: wp defender firewall activate submodule user_agent
753 * example: wp defender firewall deactivate submodule login_protection
754 *
755 * @param string $key_word
756 * @param string $submodule
757 * @param string $action
758 */
759 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
760 if ( 'submodule' !== $key_word ) {
761 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
762 \WP_CLI::runcommand( 'defender firewall --help' );
763
764 return;
765 }
766 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
767 \WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
768 \WP_CLI::runcommand( 'defender firewall --help' );
769
770 return;
771 }
772 // Get submodule slug.
773 if ( 'login_protection' === $submodule ) {
774 $model = wd_di()->get( \WP_Defender\Model\Setting\Login_Lockout::class );
775 $submodule = 'Login Protection';
776 } elseif ( '404_detection' === $submodule ) {
777 $model = wd_di()->get( \WP_Defender\Model\Setting\Notfound_Lockout::class );
778 $submodule = '404 Detection';
779 } else {
780 $model = wd_di()->get( \WP_Defender\Model\Setting\User_Agent_Lockout::class );
781 $submodule = 'User Agent Banning';
782 }
783 // Activate/deactivate submodule.
784 if ( 'activate' === $action ) {
785 $text = 'activated';
786 // Check if the submodule is not yet activated.
787 if ( true !== $model->enabled ) {
788 $model->enabled = true;
789 $model->save();
790 }
791 } else {
792 $text = 'deactivated';
793 // Check if the submodule is not yet deactivated.
794 if ( false !== $model->enabled ) {
795 $model->enabled = false;
796 $model->save();
797 }
798 }
799
800 \WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
801 }
802
803 /**
804 * Check if the field is country allowlist or country blocklist.
805 */
806 private function rename_field( $field ) {
807 if ( ! empty( $field ) ) {
808 return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
809 }
810 return '';
811 }
812
813 /**
814 * Check if the field is country allowlist or country blocklist.
815 */
816 private function is_country( $field ) {
817 return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
818 }
819
820 /**
821 *
822 * Force Bulk Password Reset.
823 *
824 * <command>
825 * : Value can be force|undo
826 *
827 * syntax: wp defender password_reset <command>
828 * example: wp defender password_reset force
829 *
830 * @param $args
831 * @param $options
832 */
833 public function password_reset( $args, $options ) {
834 if ( (is_array($args) || $args instanceof \Countable ? count( $args ) : 0) < 1 ) {
835 \WP_CLI::log( 'Invalid command.' );
836
837 return;
838 }
839
840 [$command] = $args;
841 switch ( $command ) {
842 case 'force':
843 // Get the model instance.
844 $model = wd_di()->get( \WP_Defender\Model\Setting\Password_Reset::class );
845 $model->expire_force = true;
846 $model->force_time = time();
847 $model->save();
848 $message = sprintf(
849 'Passwords created before %s are required to be reset upon next login.',
850 $this->format_date_time( $model->force_time )
851 );
852 \WP_CLI::log( $message );
853 break;
854 case 'undo':
855 $model = wd_di()->get( \WP_Defender\Model\Setting\Password_Reset::class );
856 $model->expire_force = false;
857 $model->save();
858 \WP_CLI::log( 'Passwords reset is no longer required.' );
859 break;
860 default:
861 \WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
862 break;
863 }
864 }
865
866 /**
867 * Clear completed action scheduler logs.
868 */
869 private function scan_clear_logs() {
870 $result = \WP_Defender\Component\Scan::clear_logs();
871 $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
872
873 \WP_CLI::log( $message );
874 }
875
876 /**
877 * Delete old logs.
878 *
879 * <command> delete
880 * This command must have this command
881 *
882 * syntax: wp defender logs <command>
883 * example: wp defender logs delete
884 *
885 * @param $args
886 */
887 public function logs( $args ) {
888 if ( (is_array($args) || $args instanceof \Countable ? count( $args ) : 0) < 1 ) {
889 \WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
890 \WP_CLI::runcommand( 'defender logs --help' );
891
892 return;
893 }
894
895 [$command] = $args;
896
897 switch ( $command ) {
898 case 'delete':
899 $rotation_logger = wd_di()->get( \WP_Defender\Component\Logger\Rotation_Logger::class );
900 $rotation_logger->purge_old_log();
901 \WP_CLI::log( 'Old logs are deleted.' );
902 break;
903 default:
904 \WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
905 break;
906 }
907 }
908 }
909