PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/component/class-cli.php +744 -390 5.8.1 → 6.3.0 View file →
@@ -8,17 +8,20 @@
8 8
9 9 namespace WP_Defender\Component;
10 10
11 11 use WP_CLI;
12 +use Countable;
12 13 use Exception;
13 -use Countable;
14 14 use Throwable;
15 15 use Faker\Factory;
16 +use WP_Filesystem_Base;
16 17 use WP_CLI\ExitException;
17 18 use WP_Defender\Traits\IO;
18 19 use WP_Defender\Traits\Theme;
19 20 use WP_Defender\Traits\Plugin;
20 21 use WP_Defender\Traits\Formats;
22 +use WP_Defender\Behavior\WPMUDEV;
23 +use WP_Defender\Component\Audit;
21 24 use WP_Defender\Model\Audit_Log;
22 25 use WP_Defender\Model\Scan_Item;
23 26 use WP_Defender\Model\Lockout_Ip;
24 27 use WP_Defender\Model\Lockout_Log;
@@ -23,22 +26,22 @@
23 26 use WP_Defender\Model\Lockout_Ip;
24 27 use WP_Defender\Model\Lockout_Log;
25 28 use WP_Defender\Controller\Dashboard;
26 29 use WP_Defender\Controller\Two_Factor;
27 -use WP_Defender\Model\Scan as Model_Scan;
30 +use WP_Defender\Controller\Login_Access;
28 31 use WP_Defender\Controller\Main_Setting;
29 32 use WP_Defender\Controller\Audit_Logging;
30 -use WP_Defender\Controller\Advanced_Tools;
33 +use WP_Defender\Model\Scan as Model_Scan;
31 34 use WP_Defender\Controller\Security_Tweaks;
32 35 use WP_Defender\Model\Setting\Login_Lockout;
36 +use WP_Defender\Behavior\Scan\Core_Integrity;
37 +use WP_Defender\Controller\Blocklist_Monitor;
33 38 use WP_Defender\Model\Setting\Password_Reset;
34 -use WP_Defender\Controller\Blocklist_Monitor;
35 39 use WP_Defender\Model\Setting\Notfound_Lockout;
36 40 use WP_Defender\Model\Setting\Security_Headers;
41 +use WP_Defender\Component\Scan as Scan_Component;
42 +use WP_Defender\Component\Logger\Rotation_Logger;
37 43 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 44 use function WP_CLI\Utils\format_items;
42 45
43 46 if ( ! defined( 'WPINC' ) ) {
44 47 die;
@@ -60,26 +63,100 @@
60 63 local_to_utc as protected;
61 64 moment_datetime_format_from as protected;
62 65 persistent_hub_datetime_format as protected;
63 66 time_since as protected;
67 + get_local_human_date as protected;
68 + get_time_diff as protected;
64 69 }
65 - use IO;
66 - use Theme;
67 - use Plugin;
70 + use IO {
71 + try_create_lock as protected;
72 + release_cron_lock as protected;
73 + remove_lock as protected;
74 + acquire_cron_lock as protected;
75 + compare_hashes as protected;
76 + delete_dir as protected;
77 + detect_line_ending as protected;
78 + get_log_path as protected;
79 + }
80 + use Theme {
81 + get_path_of_themes_dir as protected;
82 + get_theme as protected;
83 + get_theme_slugs as protected;
84 + get_themes as protected;
85 + is_active_theme as protected;
86 + }
87 + use Plugin {
88 + check_plugin_on_wp_org as protected;
89 + check_by_readme_file as protected;
90 + get_abs_plugin_path_by_slug as protected;
91 + get_active_plugin_names as protected;
92 + get_plugin_details_by as protected;
93 + get_plugin_directory_name as protected;
94 + get_plugin_headers as protected;
95 + get_plugin_relative_path as protected;
96 + get_plugin_slugs as protected;
97 + get_plugins as protected;
98 + get_plugin_slug_by as protected;
99 + handle_wp_org_response_by as protected;
100 + is_active_plugin as protected;
101 + is_likely_wporg_slug as protected;
102 + ping_wp_org_by_plugin_slug as protected;
103 + }
68 104
69 105 /**
70 - * This is a helper for scan module.
71 - * #Options
106 + * Run scans and manage scan results via WP-CLI.
107 + *
108 + * ## OPTIONS
109 + *
72 110 * <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.
111 + * : Action to perform.
112 + * ---
113 + * options:
114 + * - run
115 + * - ignore
116 + * - unignore
117 + * - resolve
118 + * - delete
119 + * - clear_logs
120 + * ---
121 + *
76 122 * [--type=<type>]
77 - * : Default, without values, is for all items, or core_integrity|plugin_integrity|vulnerability|suspicious_code|plugin_outdated|plugin_closed.
123 + * : Filter by issue type. Omit to target all types.
124 + * ---
125 + * options:
126 + * - detailed
127 + * - core_integrity
128 + * - plugin_integrity
129 + * - vulnerability
130 + * - suspicious_code
131 + * - plugin_outdated
132 + * - plugin_closed
133 + * ---
78 134 *
79 - * @param mixed $args Command arguments.
80 - * @param mixed $options Command options.
135 + * ## EXAMPLES
81 136 *
137 + * # Run a full scan.
138 + * $ wp defender scan run
139 + * Success: All done!
140 + *
141 + * # Run a detailed scan with table output.
142 + * $ wp defender scan run --type=detailed
143 + *
144 + * # Ignore all active core integrity issues.
145 + * $ wp defender scan ignore --type=core_integrity
146 + *
147 + * # Resolve all active vulnerability issues.
148 + * $ wp defender scan resolve --type=vulnerability
149 + *
150 + * # Delete all suspicious code files.
151 + * $ wp defender scan delete --type=suspicious_code
152 + *
153 + * # Clear completed scan logs.
154 + * $ wp defender scan clear_logs
155 + *
156 + * @param mixed $args Command arguments.
157 + * @param mixed $options Command options.
158 + *
82 159 * @throws ExitException If an invalid command is provided.
83 160 */
84 161 public function scan( $args, $options ) {
85 162 if ( ! is_array( $args ) || array() === $args ) {
@@ -86,9 +163,9 @@
86 163 WP_CLI::error( 'Invalid command' );
87 164
88 165 return;
89 166 }
90 - [ $command ] = $args;
167 + [$command] = $args;
91 168 switch ( $command ) {
92 169 case 'run':
93 170 $this->scan_all( $options );
94 171 break;
@@ -95,19 +172,11 @@
95 172 case 'clear_logs':
96 173 $this->scan_clear_logs();
97 174 break;
98 175 default:
99 - $commands = array(
100 - 'ignore',
101 - 'unignore',
102 - 'resolve',
103 - 'delete',
104 - );
176 + $commands = array( 'ignore', 'unignore', 'resolve', 'delete' );
105 177 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 - );
178 + WP_CLI::confirm( 'This can cause your site get fatal error and can\'t restore back unless you have a backup, are you sure to continue?', $options );
110 179 $this->scan_task( $command, $options );
111 180 } else {
112 181 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
113 182 }
@@ -115,69 +184,106 @@
115 184 }
116 185 }
117 186
118 187 /**
119 - * Split scan issue into file and dir.
188 + * Starts a full scan based on the provided options.
120 189 *
121 - * @param string|null $type Scan type.
122 - * @param array $raw_data Array of raw scan data.
123 - *
124 - * @return array
190 + * @param array $options Command options.
125 191 */
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 - );
192 + private function scan_all( $options ) {
193 + $type = $options['type'] ?? null;
194 + $is_detailed = false;
195 + switch ( $type ) {
196 + case null:
197 + // All items.
198 + $type = null;
199 + break;
200 + case 'detailed':
201 + $is_detailed = true;
202 + break;
203 + default:
204 + WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
205 + break;
206 + }
207 + $scan_component = wd_di()->get( Scan_Component::class );
208 + if ( ! $scan_component->is_any_scan_type_active() ) {
209 + WP_CLI::error( Scan_Component::get_emergency_scan_stop_text() );
210 + }
211 + WP_CLI::log( 'Check if there is a scan ongoing...' );
212 + $scan = Model_Scan::get_active();
213 + if ( ! is_object( $scan ) ) {
214 + WP_CLI::log( 'No active scan, creating...' );
215 + // Match the web-triggered flow: clear stale idle scans first so they don't skew "last scan" lookups.
216 + wd_di()->get( Model_Scan::class )->delete_idle();
217 + delete_site_option( Core_Integrity::ISSUE_CHECKSUMS );
218 + $scan = Model_Scan::create();
219 + if ( is_wp_error( $scan ) ) {
220 + WP_CLI::error( $scan->get_error_message() );
144 221 }
222 + $scan_component->gather_actioned_plugin_details();
223 + } else {
224 + WP_CLI::log( 'Continue from last scan' );
145 225 }
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 - );
226 + // Start detailed scan.
227 + if ( $is_detailed ) {
228 + $start = microtime( true );
169 229 }
230 + $handler = wd_di()->get( Scan_Component::class );
231 + while ( $handler->process() === false ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedWhile
232 + }
233 + $scan = Model_Scan::get_last();
234 + if ( ! is_object( $scan ) || is_wp_error( $scan ) ) {
235 + return;
236 + }
237 + $results = $scan->to_array();
238 + if ( is_array( $results ) && isset( $results['issues_items'] ) && array() !== $results['issues_items'] ) {
239 + $count = is_array( $results['issues_items'] ) || $results['issues_items'] instanceof Countable ? count( $results['issues_items'] ) : 0;
240 + // Finish detailed scan.
241 + if ( $is_detailed ) {
242 + format_items( 'table', $results['issues_items'], array( 'type', 'short_desc', 'full_path' ) );
243 + WP_CLI::log( sprintf( 'Saved %d items.', $count ) );
244 + $finish = microtime( true ) - $start;
245 + WP_CLI::log( 'Scan takes ' . round( $finish, 2 ) . 's to process.' );
246 + } else {
247 + WP_CLI::log( sprintf( 'Found %d issues.', $count ) );
248 + }
249 + }
250 + WP_CLI::success( 'All done!' );
170 251 }
171 252
172 253 /**
254 + * Clear completed action scheduler logs.
255 + */
256 + private function scan_clear_logs() {
257 + $scan_component = wd_di()->get( Scan_Component::class );
258 + $result = $scan_component::clear_logs();
259 + $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
260 +
261 + WP_CLI::log( $message );
262 + }
263 +
264 + /**
173 265 * Executes tasks based on the type of scan.
174 266 *
175 - * @param string $command The task to perform.
176 - * @param mixed $options Command options.
267 + * @param string $command The task to perform.
268 + * @param mixed $options Command options.
177 269 */
178 270 private function scan_task( $command, $options ) {
179 - $type = $options['type'] ?? null;
271 + $option_type = is_array( $options ) ? ( $options['type'] ?? null ) : null;
272 + $type = is_string( $option_type ) && '' !== $option_type ? strtolower( $option_type ) : null;
273 + if ( defender_is_wp_org_version() && in_array(
274 + $type,
275 + array(
276 + Scan_Item::TYPE_VULNERABILITY, // TYPE_SUSPICIOUS const is not suitable for use.
277 + 'suspicious_code',
278 +
279 + ),
280 + true
281 + ) ) {
282 + WP_CLI::warning( 'A WPMU DEV subscription is required to use this command.' );
283 + return;
284 + }
285 +
180 286 switch ( $type ) {
181 287 case null:
182 288 // All items.
183 289 $type = null;
@@ -205,9 +311,9 @@
205 311 break;
206 312 }
207 313 $active = Model_Scan::get_active();
208 314 if ( is_object( $active ) ) {
209 - return WP_CLI::error( 'A scan is running, you need to wait till it complete to continue' );
315 + WP_CLI::error( 'A scan is running, you need to wait till it complete to continue' );
210 316 }
211 317 $model = Model_Scan::get_last();
212 318 if ( ! is_object( $model ) ) {
213 319 return;
@@ -236,38 +342,28 @@
236 342 case 'resolve':
237 343 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
238 344 $resolved = array();
239 345 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 - ) {
346 + if ( in_array( $item->type, array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ), true ) ) {
247 347 WP_CLI::log( sprintf( 'Reverting %s to original', $item->raw_data['file'] ) );
248 348 $ret = $item->resolve();
249 349 if ( ! is_wp_error( $ret ) ) {
250 350 $resolved[] = $item;
251 351 } else {
252 - return WP_CLI::error( $ret->get_error_message() );
352 + WP_CLI::error( $ret->get_error_message() );
253 353 }
254 354 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $item->type ) {
255 355 // 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.
356 + $whitelist = array(// wordfence waf.
357 + ABSPATH . '/wordfence-waf.php', // Any files inside plugins, if removed, can cause fatal error.
358 + WP_CONTENT_DIR . '/plugins/', // Any files inside themes.
262 359 $this->get_path_of_themes_dir(),
263 360 );
264 361 $path = $item->raw_data['file'];
265 362 $can_delete = true;
266 - $current = '';
267 363 foreach ( $whitelist as $value ) {
268 364 $current = $value;
269 - if ( strpos( $value, $path ) > 0 ) {
365 + if ( str_contains( $path, $value ) ) {
270 366 // Ignore this.
271 367 $can_delete = false;
272 368 break;
273 369 }
@@ -273,17 +369,26 @@
273 369 }
274 370 }
275 371 if ( false === $can_delete ) {
276 372 WP_CLI::log( sprintf( 'Ignore file %s as it is in %s', $path, $current ) );
277 - } elseif ( wp_delete_file( $path ) ) {
373 + } elseif ( ! is_dir( $path ) && wp_delete_file( $path ) ) {
278 374 WP_CLI::log( sprintf( 'Delete file %s', $path ) );
279 375 $model->remove_issue( $item->id );
280 376 $resolved[] = $item;
281 377 } else {
282 - return WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
378 + WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
283 379 }
380 + } elseif ( Scan_Item::TYPE_VULNERABILITY === $item->type ) {
381 + $ret = $item->resolve();
382 + if ( is_wp_error( $ret ) ) {
383 + WP_CLI::error( $ret->get_error_message() );
384 + } elseif ( is_array( $ret ) && isset( $ret['type_notice'] ) && 'error' === $ret['type_notice'] ) {
385 + WP_CLI::error( $ret['message'] ?? esc_html__( 'Unable to resolve vulnerability.', 'defender-security' ) );
386 + } else {
387 + $model->remove_issue( $item->id );
388 + $resolved[] = $item;
389 + }
284 390 }
285 - // No result for Vulnerability, Outdated or Closed plugin types.
286 391 }
287 392 WP_CLI::log( sprintf( 'Resolved %s items', count( $resolved ) ) );
288 393 break;
289 394 case 'delete':
@@ -314,9 +419,9 @@
314 419 $model->remove_issue( $item->id );
315 420 $deleted[] = $item;
316 421 } else {
317 422
318 - return WP_CLI::error( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
423 + WP_CLI::error( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
319 424 }
320 425 } elseif ( 'file' === $issue_type ) {
321 426 // Work with core_integrity, plugin_integrity or suspicious_code types.
322 427 if ( wp_delete_file( $path ) ) {
@@ -323,9 +428,9 @@
323 428 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
324 429 $model->remove_issue( $item->id );
325 430 $deleted[] = $item;
326 431 } else {
327 - return WP_CLI::error( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
432 + WP_CLI::warning( sprintf( "Can't delete %s: %s", $issue_type, $path ) );
328 433 }
329 434 }
330 435 }
331 436 WP_CLI::log( sprintf( 'Deleted %s items', count( $deleted ) ) );
@@ -335,12 +440,59 @@
335 440 }
336 441 }
337 442
338 443 /**
444 + * Split scan issue into file and dir.
445 + *
446 + * @param string|null $type Scan type.
447 + * @param array $raw_data Array of raw scan data.
448 + *
449 + * @return array
450 + */
451 + private function split_scan_issue_into_file_and_dir( $type, $raw_data ): array {
452 + // General case without type-param.
453 + if ( null === $type ) {
454 + if ( isset( $raw_data['file'] ) ) {
455 + return array(
456 + 'type' => 'file',
457 + 'path' => $raw_data['file'],
458 + );
459 + } elseif ( isset( $raw_data['base_slug'] ) ) {
460 + return array(
461 + 'type' => 'folder',
462 + 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
463 + );
464 + } elseif ( isset( $raw_data['slug'] ) ) {
465 + return array(
466 + 'type' => 'folder',
467 + 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
468 + );
469 + }
470 + }
471 +
472 + if ( in_array( $type, array( Scan_Item::TYPE_PLUGIN_OUTDATED, Scan_Item::TYPE_PLUGIN_CLOSED ), true ) ) {
473 + return array(
474 + 'type' => 'folder',
475 + 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
476 + );
477 + } elseif ( Scan_Item::TYPE_VULNERABILITY === $type ) {
478 + return array(
479 + 'type' => 'folder',
480 + 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
481 + );
482 + } else {
483 + return array(
484 + 'type' => 'file',
485 + 'path' => $raw_data['file'],
486 + );
487 + }
488 + }
489 +
490 + /**
339 491 * Generate dummy data, use in unit tests.
340 492 * DO NOT USE IN PRODUCTION.
341 493 *
342 - * @param mixed $args Command arguments.
494 + * @param mixed $args Command arguments.
343 495 */
344 496 public function seed( $args ) {
345 497 global $wp_filesystem;
346 498 // Initialize the WP filesystem, no more using 'file-put-contents' function.
@@ -352,59 +504,41 @@
352 504 WP_CLI::error( 'Invalid command' );
353 505
354 506 return;
355 507 }
508 + if ( ! $this->is_testing_mode() ) {
509 + return;
510 + }
511 +
356 512 [ $command ] = $args;
357 513 switch ( $command ) {
358 514 case 'scan:core':
515 + WP_CLI::confirm( 'This will modify a WordPress core file (wp-load.php). Are you sure?', array() );
516 +
359 517 $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;
518 + if ( ! $wp_filesystem->exists( $file_path ) ) {
519 + WP_CLI::error( sprintf( 'File does not exist: %s', $file_path ) );
520 +
521 + return;
363 522 }
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();
523 + $content = $wp_filesystem->get_contents( $file_path );
524 + if ( false === $content ) {
525 + WP_CLI::error( sprintf( 'Could not read file: %s', $file_path ) );
526 +
527 + return;
391 528 }
529 + if ( str_contains( $content, '//this make different' ) ) {
530 + WP_CLI::warning( 'File already seeded, skipping.' );
531 +
532 + return;
533 + }
534 + $wp_filesystem->put_contents( $file_path, $content . '//this make different' );
392 535 break;
393 536 case 'ip:logs':
537 + WP_CLI::confirm( 'This will insert fake firewall lockout log entries into the database. Are you sure?', array() );
394 538 // 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 - );
539 + $types = array( Lockout_Log::AUTH_FAIL, Lockout_Log::AUTH_LOCK, Lockout_Log::ERROR_404, Lockout_Log::LOCKOUT_404, Lockout_Log::LOCKOUT_UA );
540 + $is_lock = array( Lockout_Log::AUTH_LOCK, Lockout_Log::LOCKOUT_404, Lockout_Log::LOCKOUT_UA );
407 541 $faker = Factory::create();
408 542 WP_CLI::log( $faker->ipv4 );
409 543 $range = array(
410 544 'today midnight' => array( 'now', 100 ),
@@ -419,9 +553,9 @@
419 553 'ua_lockout' => 0,
420 554 );
421 555 $last_lockout = 0;
422 556 foreach ( $range as $date => $to ) {
423 - [ $to, $count ] = $to;
557 + [$to, $count] = $to;
424 558 for ( $i = 0; $i < $count; $i++ ) {
425 559 $model = new Lockout_Log();
426 560 $model->ip = $faker->ipv4;
427 561 $model->type = $types[ array_rand( $types ) ];
@@ -461,10 +595,11 @@
461 595 }
462 596
463 597 /**
464 598 * Clean up dummy data.
599 + * DO NOT USE IN PRODUCTION.
465 600 *
466 - * @param mixed $args Command arguments.
601 + * @param mixed $args Command arguments.
467 602 */
468 603 public function unseed( $args ) {
469 604 global $wp_filesystem;
470 605 // Initialize the WP filesystem, no more using 'file-put-contents' function.
@@ -476,15 +611,38 @@
476 611 WP_CLI::error( 'Invalid command' );
477 612
478 613 return;
479 614 }
615 + if ( ! $this->is_testing_mode() ) {
616 + return;
617 + }
618 +
480 619 [ $command ] = $args;
481 620 switch ( $command ) {
482 621 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 ) );
622 + WP_CLI::confirm( 'This will revert the modification to wp-load.php. Are you sure?', array() );
623 +
624 + $file_path = ABSPATH . 'wp-load.php';
625 + if ( ! $wp_filesystem->exists( $file_path ) ) {
626 + WP_CLI::error( sprintf( 'File does not exist: %s', $file_path ) );
627 +
628 + return;
629 + }
630 + $content = $wp_filesystem->get_contents( $file_path );
631 + if ( false === $content ) {
632 + WP_CLI::error( sprintf( 'Could not read file: %s', $file_path ) );
633 +
634 + return;
635 + }
636 + if ( ! str_contains( $content, '//this make different' ) ) {
637 + WP_CLI::warning( 'Marker not found in file, nothing to revert.' );
638 +
639 + return;
640 + }
641 + $wp_filesystem->put_contents( $file_path, str_replace( '//this make different', '', $content ) );
485 642 break;
486 643 case 'scan:suspicious':
644 + WP_CLI::confirm( 'This will delete the false-positive test file. Are you sure?', array() );
487 645 wp_delete_file( WP_CONTENT_DIR . '/false-positive.php' );
488 646 break;
489 647 default:
490 648 break;
@@ -491,112 +649,96 @@
491 649 }
492 650 }
493 651
494 652 /**
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
653 + * Manage audit logs via WP-CLI.
500 654 *
501 - * @param mixed $args Command arguments.
655 + * ## OPTIONS
656 + *
657 + * <command>
658 + * : Action to perform.
659 + * ---
660 + * options:
661 + * - reset
662 + * - sync
663 + * ---
664 + *
665 + * ## EXAMPLES
666 + *
667 + * # Delete all audit log entries from the database.
668 + * $ wp defender audit reset
669 + * All clear
670 + *
671 + * # Synchronize local audit logs with cloud history (Pro only).
672 + * $ wp defender audit sync
673 + * Sync completed.
674 + *
675 + * @param mixed $args Command arguments.
502 676 */
503 677 public function audit( $args ) {
504 678 if ( ! is_array( $args ) || array() === $args ) {
505 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
506 - WP_CLI::runcommand( 'defender audit --help' );
679 + WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
680 + WP_CLI::runcommand(
681 + 'defender audit --help',
682 + array(
683 + 'launch' => false,
684 + 'exit_error' => false,
685 + )
686 + );
507 687
508 688 return;
509 689 }
510 - [ $command ] = $args;
690 +
691 + [$command] = $args;
511 692 switch ( $command ) {
512 693 case 'reset':
513 - Audit_Log::truncate();
514 - delete_site_option( 'wd_audit_fetch_checkpoint' );
694 + wd_di()->get( Audit::class )->reset();
515 695
516 696 WP_CLI::log( 'All clear' );
517 697 break;
518 698 default:
519 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
520 - WP_CLI::runcommand( 'defender audit --help' );
699 + WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
700 + WP_CLI::runcommand(
701 + 'defender audit --help',
702 + array(
703 + 'launch' => false,
704 + 'exit_error' => false,
705 + )
706 + );
521 707 break;
522 708 }
523 709 }
524 710
525 711 /**
526 - * Starts a full scan based on the provided options.
712 + * Manage security headers via WP-CLI.
527 713 *
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
714 + * ## OPTIONS
715 + *
590 716 * <command>
591 - * : Value can be run - Check headers, or activate|deactivate all headers
592 - * [--type=<type>]
593 - * : Default is all
717 + * : Action to perform.
718 + * ---
719 + * options:
720 + * - check
721 + * - activate
722 + * - deactivate
723 + * ---
724 + *
594 725 * ## EXAMPLES
595 - * wp defender security_headers check
596 726 *
597 - * @param mixed $args Command arguments.
727 + * # Check the current status of all security headers.
728 + * $ wp defender security_headers check
729 + * Success: Checking is ready.
598 730 *
731 + * # Activate all security headers.
732 + * $ wp defender security_headers activate
733 + * Activating is ready.
734 + *
735 + * # Deactivate all security headers.
736 + * $ wp defender security_headers deactivate
737 + * Deactivating is ready.
738 + *
739 + * @param mixed $args Command arguments.
740 + *
599 741 * @throws ExitException|Exception If an invalid command is provided.
600 742 */
601 743 public function security_headers( $args ) {
602 744 if ( ! is_array( $args ) || array() === $args ) {
@@ -603,15 +745,10 @@
603 745 WP_CLI::error( 'Invalid command.' );
604 746
605 747 return;
606 748 }
607 - $model = new Security_Headers();
608 - if ( ! is_object( $model ) ) {
609 - WP_CLI::error( 'Invalid model.' );
610 -
611 - return;
612 - }
613 - [ $command ] = $args;
749 + $model = new Security_Headers();
750 + [$command] = $args;
614 751 switch ( $command ) {
615 752 case 'check':
616 753 $i = 1;
617 754 foreach ( $model->get_headers() as $header ) {
@@ -621,17 +758,17 @@
621 758 }
622 759 WP_CLI::success( 'Checking is ready.' );
623 760 break;
624 761 case 'activate':
625 - foreach ( $model->get_headers() as $header ) {
626 - $model->{$header::$rule_slug} = true;
762 + foreach ( $model->get_headers() as $rule_slug => $header ) {
763 + $this->set_security_header_state( $model, $rule_slug, true );
627 764 }
628 765 $model->save();
629 766 WP_CLI::log( 'Activating is ready.' );
630 767 break;
631 768 case 'deactivate':
632 - foreach ( $model->get_headers() as $header ) {
633 - $model->{$header::$rule_slug} = false;
769 + foreach ( $model->get_headers() as $rule_slug => $header ) {
770 + $this->set_security_header_state( $model, $rule_slug, false );
634 771 }
635 772 $model->save();
636 773 WP_CLI::log( 'Deactivating is ready.' );
637 774 break;
@@ -641,35 +778,78 @@
641 778 }
642 779 }
643 780
644 781 /**
645 - * This is a helper command to reset plugin settings.
646 - * #Options
782 + * Set a security header setting without using dynamic model properties.
783 + *
784 + * @param Security_Headers $model The security headers settings model.
785 + * @param string $rule_slug The header rule slug.
786 + * @param bool $enabled Whether the rule is enabled.
787 + */
788 + private function set_security_header_state( Security_Headers $model, string $rule_slug, bool $enabled ): void {
789 + switch ( $rule_slug ) {
790 + case 'sh_xframe':
791 + $model->sh_xframe = $enabled;
792 + break;
793 + case 'sh_xss_protection':
794 + $model->sh_xss_protection = $enabled;
795 + break;
796 + case 'sh_content_type_options':
797 + $model->sh_content_type_options = $enabled;
798 + break;
799 + case 'sh_strict_transport':
800 + $model->sh_strict_transport = $enabled;
801 + break;
802 + case 'sh_referrer_policy':
803 + $model->sh_referrer_policy = $enabled;
804 + break;
805 + case 'sh_feature_policy':
806 + $model->sh_feature_policy = $enabled;
807 + break;
808 + }
809 + }
810 +
811 + /**
812 + * Manage plugin settings via WP-CLI.
813 + *
814 + * ## OPTIONS
815 + *
647 816 * <command>
648 - * Only allowed value is reset.
649 - * Syntax: wp defender settings <command>
650 - * Example: wp defender settings reset
817 + * : Action to perform.
818 + * ---
819 + * options:
820 + * - reset
821 + * ---
651 822 *
652 - * @param mixed $args Command arguments.
653 - * @param mixed $options Command options.
823 + * ## EXAMPLES
824 + *
825 + * # Reset all plugin settings to defaults.
826 + * $ wp defender settings reset
827 + * All cleared!
828 + *
829 + * @param mixed $args Command arguments.
830 + * @param mixed $options Command options.
654 831 */
655 832 public function settings( $args, $options ) {
656 833 if ( ! is_array( $args ) || array() === $args ) {
657 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
658 - WP_CLI::runcommand( 'defender settings --help' );
834 + WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
835 + WP_CLI::runcommand(
836 + 'defender settings --help',
837 + array(
838 + 'launch' => false,
839 + 'exit_error' => false,
840 + )
841 + );
659 842
660 843 return;
661 844 }
662 845
663 - [ $command ] = $args;
846 + [$command] = $args;
664 847 switch ( $command ) {
665 848 case 'reset':
666 - WP_CLI::confirm(
667 - 'This will completely reset the plugin settings, are you sure to continue?',
668 - $options
669 - );
849 + WP_CLI::confirm( 'This will completely reset the plugin settings, are you sure to continue?', $options );
670 850 // Analog Settings > Reset Settings.
671 - wd_di()->get( Advanced_Tools::class )->remove_settings();
851 + wd_di()->get( Login_Access::class )->remove_settings();
672 852 wd_di()->get( Audit_Logging::class )->remove_settings();
673 853 wd_di()->get( Dashboard::class )->remove_settings();
674 854 wd_di()->get( Security_Tweaks::class )->remove_settings();
675 855 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
@@ -678,50 +858,103 @@
678 858
679 859 wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings();
680 860 wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings();
681 861 wd_di()->get( Two_Factor::class )->remove_settings();
682 - wd_di()->get( Blocklist_Monitor::class )->remove_settings();
683 862 wd_di()->get( Main_Setting::class )->remove_settings();
684 863 WP_CLI::log( 'All cleared!' );
685 864 break;
686 865 default:
687 - WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
688 - WP_CLI::runcommand( 'defender settings --help' );
866 + WP_CLI::error( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ), false );
867 + WP_CLI::runcommand(
868 + 'defender settings --help',
869 + array(
870 + 'launch' => false,
871 + 'exit_error' => false,
872 + )
873 + );
689 874 break;
690 875 }
691 876 }
692 877
693 878 /**
694 - * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
695 - * Syntax: wp defender firewall <command> <args_1> <args_2>
696 - * <command> clear|unblock|list|activate|deactivate
879 + * Manage firewall submodules, data, and lockouts via WP-CLI.
697 880 *
698 - * <args_1> Allowed values are: ip, user_agent, files and maxmind
699 - * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist, lockout and license_key
700 - * Example: wp defender firewall clear ip allowlist
701 - * Example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
702 - * Example: wp defender firewall list user_agent <status>
703 - * Example: wp defender firewall activate submodule <submodule>
704 - * Example: wp defender firewall deactivate submodule login_protection
705 - * <status> Allowed values are: all, allowlist, blocklist.
706 - * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
881 + * ## OPTIONS
707 882 *
708 - * @param mixed $args Command arguments.
709 - * @param mixed $options Command options.
883 + * <command>
884 + * : Action to perform.
885 + * ---
886 + * options:
887 + * - clear
888 + * - unblock
889 + * - list
890 + * - activate
891 + * - deactivate
892 + * ---
893 + *
894 + * <type>
895 + * : The firewall data type to target (e.g. ip, user_agent, files, maxmind, submodule).
896 + *
897 + * [<field>]
898 + * : The specific field or submodule to target. Defaults to 'all' for the list command.
899 + *
900 + * [--ips=<ips>]
901 + * : Comma-separated list of IP addresses to unblock. Required for the unblock command.
902 + *
903 + * ## EXAMPLES
904 + *
905 + * # Clear the IP allowlist.
906 + * $ wp defender firewall clear ip allowlist
907 + * Firewall allowlist ip is cleared.
908 + *
909 + * # Unblock specific IPs from lockout.
910 + * $ wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
911 + * Firewall lockout ip unblocked
912 + *
913 + * # List all user agent entries.
914 + * $ wp defender firewall list user_agent all
915 + *
916 + * # Activate login protection submodule.
917 + * $ wp defender firewall activate submodule login_protection
918 + * Success: Firewall "Login Protection" has been activated.
919 + *
920 + * # Deactivate 404 detection submodule.
921 + * $ wp defender firewall deactivate submodule 404_detection
922 + * Success: Firewall "404 Detection" has been deactivated.
923 + *
924 + * @param mixed $args Command arguments.
925 + * @param mixed $options Command options.
710 926 */
711 927 public function firewall( $args, $options ) {
712 - if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) <= 2 ) {
713 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
714 - WP_CLI::runcommand( 'defender firewall --help' );
928 + $arg_count = is_array( $args ) || $args instanceof Countable ? count( $args ) : 0;
929 + if ( $arg_count < 2 ) {
930 + WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
931 + WP_CLI::runcommand(
932 + 'defender firewall --help',
933 + array(
934 + 'launch' => false,
935 + 'exit_error' => false,
936 + )
937 + );
715 938
716 939 return;
717 940 }
718 941
719 - [ $command, $type, $field ] = $args;
720 - if ( ! is_string( $type ) || '' === $type || ! is_string( $field ) || '' === $field ) {
721 - WP_CLI::log( 'Invalid option.' );
722 - WP_CLI::runcommand( 'defender firewall --help' );
942 + $command = $args[0];
943 + $type = $args[1];
944 + // Field is optional for the 'list' command — defaults to 'all'.
945 + $field = $args[2] ?? ( 'list' === $command ? 'all' : '' );
723 946
947 + if ( ! is_string( $type ) || '' === $type ) {
948 + WP_CLI::error( 'Invalid option.', false );
949 + WP_CLI::runcommand(
950 + 'defender firewall --help',
951 + array(
952 + 'launch' => false,
953 + 'exit_error' => false,
954 + )
955 + );
956 +
724 957 return;
725 958 }
726 959 switch ( $command ) {
727 960 case 'clear':
@@ -745,41 +978,12 @@
745 978 }
746 979 }
747 980
748 981 /**
749 - * This clears the mask login settings.
750 - * <command> clear
751 - * This command must have this command
752 - * Syntax: wp defender mask_login <command>
753 - * Example: wp defender mask_login clear
754 - *
755 - * @param mixed $args Command arguments.
756 - */
757 - public function mask_login( $args ) {
758 - if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
759 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
760 - WP_CLI::runcommand( 'defender mask_login --help' );
761 -
762 - return;
763 - }
764 -
765 - [ $command ] = $args;
766 - switch ( $command ) {
767 - case 'clear':
768 - wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
769 - WP_CLI::log( 'Mask login settings cleared!' );
770 - break;
771 - default:
772 - WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
773 - break;
774 - }
775 - }
776 -
777 - /**
778 982 * Clears the firewall data based on the specified type and field.
779 983 *
780 - * @param string $type The type of data to clear.
781 - * @param string $field The specific field to clear.
984 + * @param string $type The type of data to clear.
985 + * @param string $field The specific field to clear.
782 986 */
783 987 private function clear_firewall( $type, $field ) {
784 988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
785 989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
@@ -784,17 +988,29 @@
784 988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
785 989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
786 990
787 991 if ( ! in_array( $type, $type_default, true ) ) {
788 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
789 - WP_CLI::runcommand( 'defender firewall --help' );
992 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $type ), false );
993 + WP_CLI::runcommand(
994 + 'defender firewall --help',
995 + array(
996 + 'launch' => false,
997 + 'exit_error' => false,
998 + )
999 + );
790 1000
791 1001 return;
792 1002 }
793 1003
794 1004 if ( ! in_array( $field, $field_default, true ) ) {
795 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
796 - WP_CLI::runcommand( 'defender firewall --help' );
1005 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $field ), false );
1006 + WP_CLI::runcommand(
1007 + 'defender firewall --help',
1008 + array(
1009 + 'launch' => false,
1010 + 'exit_error' => false,
1011 + )
1012 + );
797 1013
798 1014 return;
799 1015 }
800 1016
@@ -828,9 +1044,9 @@
828 1044 $model->save();
829 1045 } elseif ( 'maxmind' === $type ) {
830 1046 try {
831 1047 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
832 - if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
1048 + if ( is_string( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
833 1049 wp_delete_file( $model->geodb_path );
834 1050 }
835 1051 $model->maxmind_license_key = '';
836 1052 $model->geodb_path = '';
@@ -843,13 +1059,39 @@
843 1059 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
844 1060 }
845 1061
846 1062 /**
1063 + * Rename a field to its original model field name.
1064 + *
1065 + * @param string $field The field name to rename.
1066 + *
1067 + * @return string The renamed field name.
1068 + */
1069 + private function rename_field( $field ) {
1070 + if ( '' !== $field ) {
1071 + return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
1072 + }
1073 +
1074 + return '';
1075 + }
1076 +
1077 + /**
1078 + * Check if the specified field is related to country settings.
1079 + *
1080 + * @param string $field The field to check.
1081 + *
1082 + * @return bool True if the field is related to country settings, false otherwise.
1083 + */
1084 + private function is_country( $field ) {
1085 + return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
1086 + }
1087 +
1088 + /**
847 1089 * Unblocks the specified IPs from the firewall.
848 1090 *
849 - * @param string $type The type of data to unblock.
850 - * @param string $field The specific field to unblock.
851 - * @param array $options Command options including IPs to unblock.
1091 + * @param string $type The type of data to unblock.
1092 + * @param string $field The specific field to unblock.
1093 + * @param array $options Command options including IPs to unblock.
852 1094 */
853 1095 private function unblock_firewall( $type, $field, $options ) {
854 1096 $type_default = array( 'ip' );
855 1097 $field_default = array( 'lockout' );
@@ -854,17 +1096,29 @@
854 1096 $type_default = array( 'ip' );
855 1097 $field_default = array( 'lockout' );
856 1098
857 1099 if ( ! in_array( $type, $type_default, true ) ) {
858 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
859 - WP_CLI::runcommand( 'defender firewall --help' );
1100 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $type ), false );
1101 + WP_CLI::runcommand(
1102 + 'defender firewall --help',
1103 + array(
1104 + 'launch' => false,
1105 + 'exit_error' => false,
1106 + )
1107 + );
860 1108
861 1109 return;
862 1110 }
863 1111
864 1112 if ( ! in_array( $field, $field_default, true ) ) {
865 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
866 - WP_CLI::runcommand( 'defender firewall --help' );
1113 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $field ), false );
1114 + WP_CLI::runcommand(
1115 + 'defender firewall --help',
1116 + array(
1117 + 'launch' => false,
1118 + 'exit_error' => false,
1119 + )
1120 + );
867 1121
868 1122 return;
869 1123 }
870 1124
@@ -876,10 +1130,16 @@
876 1130 $model->status = Lockout_Ip::STATUS_NORMAL;
877 1131 $model->save();
878 1132 }
879 1133 } else {
880 - WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
881 - WP_CLI::runcommand( 'defender firewall --help' );
1134 + WP_CLI::error( 'Option \'ips\' is not provided. See below...', false );
1135 + WP_CLI::runcommand(
1136 + 'defender firewall --help',
1137 + array(
1138 + 'launch' => false,
1139 + 'exit_error' => false,
1140 + )
1141 + );
882 1142
883 1143 return;
884 1144 }
885 1145
@@ -889,10 +1149,10 @@
889 1149 /**
890 1150 * Lists details for the firewall based on the specified type and field.
891 1151 * Example: wp defender firewall list user_agent all
892 1152 *
893 - * @param string $type The type of data to list.
894 - * @param string $field The specific field to list.
1153 + * @param string $type The type of data to list.
1154 + * @param string $field The specific field to list.
895 1155 *
896 1156 * @since v2.6.4. Add the details for User Agent Banning.
897 1157 */
898 1158 private function list_firewall( $type, $field ) {
@@ -898,16 +1158,28 @@
898 1158 private function list_firewall( $type, $field ) {
899 1159 $type_default = array( 'user_agent' );
900 1160 $field_default = array( 'all', 'allowlist', 'blocklist' );
901 1161 if ( ! in_array( $type, $type_default, true ) ) {
902 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
903 - WP_CLI::runcommand( 'defender firewall --help' );
1162 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $type ), false );
1163 + WP_CLI::runcommand(
1164 + 'defender firewall --help',
1165 + array(
1166 + 'launch' => false,
1167 + 'exit_error' => false,
1168 + )
1169 + );
904 1170
905 1171 return;
906 1172 }
907 1173 if ( ! in_array( $field, $field_default, true ) ) {
908 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
909 - WP_CLI::runcommand( 'defender firewall --help' );
1174 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $field ), false );
1175 + WP_CLI::runcommand(
1176 + 'defender firewall --help',
1177 + array(
1178 + 'launch' => false,
1179 + 'exit_error' => false,
1180 + )
1181 + );
910 1182
911 1183 return;
912 1184 }
913 1185 $model = wd_di()->get( User_Agent_Lockout::class );
@@ -930,22 +1202,34 @@
930 1202 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
931 1203 * Example: wp defender firewall activate submodule user_agent
932 1204 * Example: wp defender firewall deactivate submodule login_protection
933 1205 *
934 - * @param string $key_word The keyword to identify the action.
935 - * @param string $submodule The submodule to toggle.
936 - * @param string $action The action to perform (activate or deactivate).
1206 + * @param string $key_word The keyword to identify the action.
1207 + * @param string $submodule The submodule to toggle.
1208 + * @param string $action The action to perform (activate or deactivate).
937 1209 */
938 1210 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
939 1211 if ( 'submodule' !== $key_word ) {
940 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
941 - WP_CLI::runcommand( 'defender firewall --help' );
1212 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $key_word ), false );
1213 + WP_CLI::runcommand(
1214 + 'defender firewall --help',
1215 + array(
1216 + 'launch' => false,
1217 + 'exit_error' => false,
1218 + )
1219 + );
942 1220
943 1221 return;
944 1222 }
945 1223 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
946 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
947 - WP_CLI::runcommand( 'defender firewall --help' );
1224 + WP_CLI::error( sprintf( 'Invalid option %s. See below...', $submodule ), false );
1225 + WP_CLI::runcommand(
1226 + 'defender firewall --help',
1227 + array(
1228 + 'launch' => false,
1229 + 'exit_error' => false,
1230 + )
1231 + );
948 1232
949 1233 return;
950 1234 }
951 1235 // Get submodule slug.
@@ -979,50 +1263,98 @@
979 1263 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
980 1264 }
981 1265
982 1266 /**
983 - * Rename a field to its original model field name.
1267 + * Check if the testing mode is enabled.
1268 + * Outputs an error and returns false if WP_DEFENDER_TESTING is not defined and true.
984 1269 *
985 - * @param string $field The field name to rename.
986 - *
987 - * @return string The renamed field name.
1270 + * @return bool
988 1271 */
989 - private function rename_field( $field ) {
990 - if ( '' !== $field ) {
991 - return str_replace( array( 'allow', 'block' ), array( 'white', 'black' ), $field );
1272 + private function is_testing_mode(): bool {
1273 + if ( ! defined( 'WP_DEFENDER_TESTING' ) || ! WP_DEFENDER_TESTING ) {
1274 + WP_CLI::error( 'This command is intended for testing only. Define WP_DEFENDER_TESTING as true to proceed.' );
1275 +
1276 + return false;
992 1277 }
993 1278
994 - return '';
1279 + return true;
995 1280 }
996 1281
997 1282 /**
998 - * Check if the specified field is related to country settings.
1283 + * Force Bulk Password Reset.
1284 + * <command>
1285 + * : Action to perform.
1286 + * ---
1287 + * options:
1288 + * - clear
1289 + * ---
999 1290 *
1000 - * @param string $field The field to check.
1291 + * ## EXAMPLES
1001 1292 *
1002 - * @return bool True if the field is related to country settings, false otherwise.
1293 + * # Reset all mask login settings to defaults.
1294 + * $ wp defender mask_login clear
1295 + * Mask login settings cleared!
1296 + *
1297 + * @param mixed $args Command arguments.
1003 1298 */
1004 - private function is_country( $field ) {
1005 - return ( 'country_whitelist' === $field || 'country_blacklist' === $field );
1299 + public function mask_login( $args ) {
1300 + if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1301 + WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
1302 + WP_CLI::runcommand(
1303 + 'defender mask_login --help',
1304 + array(
1305 + 'launch' => false,
1306 + 'exit_error' => false,
1307 + )
1308 + );
1309 +
1310 + return;
1311 + }
1312 +
1313 + [$command] = $args;
1314 + switch ( $command ) {
1315 + case 'clear':
1316 + wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
1317 + WP_CLI::log( 'Mask login settings cleared!' );
1318 + break;
1319 + default:
1320 + WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
1321 + break;
1322 + }
1006 1323 }
1007 1324
1008 1325 /**
1009 - * Force Bulk Password Reset.
1326 + * Manage bulk password reset via WP-CLI.
1327 + *
1328 + * ## OPTIONS
1329 + *
1010 1330 * <command>
1011 - * : Value can be force|undo
1012 - * Syntax: wp defender password_reset <command>
1013 - * Example: wp defender password_reset force
1331 + * : Action to perform.
1332 + * ---
1333 + * options:
1334 + * - force
1335 + * - undo
1336 + * ---
1014 1337 *
1015 - * @param mixed $args Command arguments.
1338 + * ## EXAMPLES
1339 + *
1340 + * # Force all users to reset their password on next login.
1341 + * $ wp defender password_reset force
1342 + *
1343 + * # Cancel a previously forced password reset.
1344 + * $ wp defender password_reset undo
1345 + * Passwords reset is no longer required.
1346 + *
1347 + * @param mixed $args Command arguments.
1016 1348 */
1017 1349 public function password_reset( $args ) {
1018 1350 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1019 - WP_CLI::log( 'Invalid command.' );
1351 + WP_CLI::error( 'Invalid command.' );
1020 1352
1021 1353 return;
1022 1354 }
1023 1355
1024 - [ $command ] = $args;
1356 + [$command] = $args;
1025 1357 switch ( $command ) {
1026 1358 case 'force':
1027 1359 // Get the model instance.
1028 1360 $model = wd_di()->get( Password_Reset::class );
@@ -1028,12 +1360,9 @@
1028 1360 $model = wd_di()->get( Password_Reset::class );
1029 1361 $model->expire_force = true;
1030 1362 $model->force_time = time();
1031 1363 $model->save();
1032 - $message = sprintf(
1033 - 'Passwords created before %s are required to be reset upon next login.',
1034 - $this->format_date_time( $model->force_time )
1035 - );
1364 + $message = sprintf( 'Passwords created before %s are required to be reset upon next login.', $this->format_date_time( $model->force_time ) );
1036 1365 WP_CLI::log( $message );
1037 1366 break;
1038 1367 case 'undo':
1039 1368 $model = wd_di()->get( Password_Reset::class );
@@ -1047,36 +1376,42 @@
1047 1376 }
1048 1377 }
1049 1378
1050 1379 /**
1051 - * Clear completed action scheduler logs.
1052 - */
1053 - private function scan_clear_logs() {
1054 - $scan_component = wd_di()->get( Scan_Component::class );
1055 - $result = $scan_component::clear_logs();
1056 - $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
1057 -
1058 - WP_CLI::log( $message );
1059 - }
1060 -
1061 - /**
1062 - * Delete old logs.
1063 - * <command> delete
1064 - * This command must have this command
1065 - * Syntax: wp defender logs <command>
1066 - * Example: wp defender logs delete
1380 + * Manage Defender's internal log files.
1067 1381 *
1068 - * @param mixed $args Command arguments.
1382 + * ## OPTIONS
1383 + *
1384 + * <command>
1385 + * : Action to perform.
1386 + * ---
1387 + * options:
1388 + * - delete
1389 + * ---
1390 + *
1391 + * ## EXAMPLES
1392 + *
1393 + * # Delete log files older than one week.
1394 + * $ wp defender logs delete
1395 + * Logs older than a week have been deleted.
1396 + *
1397 + * @param mixed $args Command arguments.
1069 1398 */
1070 1399 public function logs( $args ) {
1071 1400 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1072 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
1073 - WP_CLI::runcommand( 'defender logs --help' );
1401 + WP_CLI::error( 'Invalid command, add necessary arguments. See below...', false );
1402 + WP_CLI::runcommand(
1403 + 'defender logs --help',
1404 + array(
1405 + 'launch' => false,
1406 + 'exit_error' => false,
1407 + )
1408 + );
1074 1409
1075 1410 return;
1076 1411 }
1077 1412
1078 - [ $command ] = $args;
1413 + [$command] = $args;
1079 1414
1080 1415 switch ( $command ) {
1081 1416 case 'delete':
1082 1417 $rotation_logger = wd_di()->get( Rotation_Logger::class );
@@ -1089,24 +1424,45 @@
1089 1424 }
1090 1425 }
1091 1426
1092 1427 /**
1093 - * This is a helper for Captcha actions.
1094 - * Syntax: wp defender captcha <command>
1095 - * <command> activate|deactivate|clear
1096 - * Example: wp defender captcha activate
1428 + * Manage CAPTCHA settings via WP-CLI.
1097 1429 *
1098 - * @param mixed $args Command arguments.
1430 + * ## OPTIONS
1431 + *
1432 + * <command>
1433 + * : Action to perform.
1434 + * ---
1435 + * options:
1436 + * - activate
1437 + * - deactivate
1438 + * - clear
1439 + * ---
1440 + *
1441 + * ## EXAMPLES
1442 + *
1443 + * # Enable CAPTCHA.
1444 + * $ wp defender captcha activate
1445 + * CAPTCHA is activated.
1446 + *
1447 + * # Disable CAPTCHA.
1448 + * $ wp defender captcha deactivate
1449 + * CAPTCHA is deactivated.
1450 + *
1451 + * # Reset all CAPTCHA settings to defaults.
1452 + * $ wp defender captcha clear
1453 + * CAPTCHA is cleared.
1454 + *
1455 + * @param mixed $args Command arguments.
1099 1456 */
1100 1457 public function captcha( $args ) {
1101 1458 if ( ! is_array( $args ) || array() === $args ) {
1102 1459 WP_CLI::error( 'Invalid command.' );
1103 - WP_CLI::runcommand( 'defender captcha --help' );
1104 1460
1105 1461 return;
1106 1462 }
1107 - $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1108 - [ $command ] = $args;
1463 + $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1464 + [$command] = $args;
1109 1465 switch ( $command ) {
1110 1466 case 'activate':
1111 1467 if ( true !== $model->enabled ) {
1112 1468 $model->enabled = true;
@@ -1118,9 +1474,8 @@
1118 1474 if ( false !== $model->enabled ) {
1119 1475 $model->enabled = false;
1120 1476 $model->save();
1121 1477 }
1122 - $model->save();
1123 1478 WP_CLI::log( 'CAPTCHA is deactivated.' );
1124 1479 break;
1125 1480 case 'clear':
1126 1481 $default_values = $model->get_default_values();
@@ -1161,9 +1516,8 @@
1161 1516 WP_CLI::log( 'CAPTCHA is cleared.' );
1162 1517 break;
1163 1518 default:
1164 1519 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1165 - WP_CLI::runcommand( 'defender captcha --help' );
1166 1520 break;
1167 1521 }
1168 1522 }
1169 1523 }