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 +727 -371 6.0.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,20 +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;
30 +use WP_Defender\Controller\Login_Access;
31 +use WP_Defender\Controller\Main_Setting;
32 +use WP_Defender\Controller\Audit_Logging;
27 33 use WP_Defender\Model\Scan as Model_Scan;
28 -use WP_Defender\Controller\Main_Setting;
29 -use WP_Defender\Controller\Login_Access;
30 34 use WP_Defender\Controller\Security_Tweaks;
31 35 use WP_Defender\Model\Setting\Login_Lockout;
36 +use WP_Defender\Behavior\Scan\Core_Integrity;
37 +use WP_Defender\Controller\Blocklist_Monitor;
32 38 use WP_Defender\Model\Setting\Password_Reset;
33 39 use WP_Defender\Model\Setting\Notfound_Lockout;
34 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;
35 43 use WP_Defender\Model\Setting\User_Agent_Lockout;
36 -use WP_Defender\Component\Logger\Rotation_Logger;
37 -use WP_Defender\Component\Scan as Scan_Component;
38 -use WP_Filesystem_Base;
39 44 use function WP_CLI\Utils\format_items;
40 45
41 46 if ( ! defined( 'WPINC' ) ) {
42 47 die;
@@ -59,22 +64,32 @@
59 64 moment_datetime_format_from as protected;
60 65 persistent_hub_datetime_format as protected;
61 66 time_since as protected;
62 67 get_local_human_date as protected;
68 + get_time_diff as protected;
69 + }
70 + use IO {
71 + try_create_lock as protected;
63 72 release_cron_lock as protected;
64 - create_lock as protected;
65 - has_lock as protected;
66 73 remove_lock as protected;
67 - check_plugin_on_wp_org as protected;
68 - check_by_readme_file as protected;
69 74 acquire_cron_lock as protected;
70 75 compare_hashes as protected;
71 76 delete_dir as protected;
72 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;
73 90 get_abs_plugin_path_by_slug as protected;
74 91 get_active_plugin_names as protected;
75 - get_path_of_themes_dir as protected;
76 - get_log_path as protected;
77 92 get_plugin_details_by as protected;
78 93 get_plugin_directory_name as protected;
79 94 get_plugin_headers as protected;
80 95 get_plugin_relative_path as protected;
@@ -80,35 +95,68 @@
80 95 get_plugin_relative_path as protected;
81 96 get_plugin_slugs as protected;
82 97 get_plugins as protected;
83 98 get_plugin_slug_by as protected;
84 - get_theme as protected;
85 - get_theme_slugs as protected;
86 - get_themes as protected;
87 - get_time_diff as protected;
88 99 handle_wp_org_response_by as protected;
89 100 is_active_plugin as protected;
90 - is_active_theme as protected;
91 101 is_likely_wporg_slug as protected;
92 102 ping_wp_org_by_plugin_slug as protected;
93 103 }
94 - use IO;
95 - use Theme;
96 - use Plugin;
97 104
98 105 /**
99 - * This is a helper for scan module.
100 - * #Options
106 + * Run scans and manage scan results via WP-CLI.
107 + *
108 + * ## OPTIONS
109 + *
101 110 * <command>
102 - * : Value can be run - Perform a scan, e.g. 'run'-command or 'run ----type=detailed' for detailed result,
103 - * or (un)ignore|delete|resolve to do the relevant task,
104 - * 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 + *
105 122 * [--type=<type>]
106 - * : 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 + * ---
107 134 *
108 - * @param mixed $args Command arguments.
109 - * @param mixed $options Command options.
135 + * ## EXAMPLES
110 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 + *
111 159 * @throws ExitException If an invalid command is provided.
112 160 */
113 161 public function scan( $args, $options ) {
114 162 if ( ! is_array( $args ) || array() === $args ) {
@@ -115,9 +163,9 @@
115 163 WP_CLI::error( 'Invalid command' );
116 164
117 165 return;
118 166 }
119 - [ $command ] = $args;
167 + [$command] = $args;
120 168 switch ( $command ) {
121 169 case 'run':
122 170 $this->scan_all( $options );
123 171 break;
@@ -124,19 +172,11 @@
124 172 case 'clear_logs':
125 173 $this->scan_clear_logs();
126 174 break;
127 175 default:
128 - $commands = array(
129 - 'ignore',
130 - 'unignore',
131 - 'resolve',
132 - 'delete',
133 - );
176 + $commands = array( 'ignore', 'unignore', 'resolve', 'delete' );
134 177 if ( in_array( $command, $commands, true ) ) {
135 - WP_CLI::confirm(
136 - 'This can cause your site get fatal error and can\'t restore back unless you have a backup, are you sure to continue?',
137 - $options
138 - );
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 );
139 179 $this->scan_task( $command, $options );
140 180 } else {
141 181 WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
142 182 }
@@ -144,80 +184,102 @@
144 184 }
145 185 }
146 186
147 187 /**
148 - * Split scan issue into file and dir.
188 + * Starts a full scan based on the provided options.
149 189 *
150 - * @param string|null $type Scan type.
151 - * @param array $raw_data Array of raw scan data.
152 - *
153 - * @return array
190 + * @param array $options Command options.
154 191 */
155 - private function split_scan_issue_into_file_and_dir( $type, $raw_data ): array {
156 - // General case without type-param.
157 - if ( null === $type ) {
158 - if ( isset( $raw_data['file'] ) ) {
159 - return array(
160 - 'type' => 'file',
161 - 'path' => $raw_data['file'],
162 - );
163 - } elseif ( isset( $raw_data['base_slug'] ) ) {
164 - return array(
165 - 'type' => 'folder',
166 - 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
167 - );
168 - } elseif ( isset( $raw_data['slug'] ) ) {
169 - return array(
170 - 'type' => 'folder',
171 - 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
172 - );
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() );
173 221 }
222 + $scan_component->gather_actioned_plugin_details();
223 + } else {
224 + WP_CLI::log( 'Continue from last scan' );
174 225 }
226 + // Start detailed scan.
227 + if ( $is_detailed ) {
228 + $start = microtime( true );
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!' );
251 + }
175 252
176 - if ( in_array(
177 - $type,
178 - array(
179 - Scan_Item::TYPE_PLUGIN_OUTDATED,
180 - Scan_Item::TYPE_PLUGIN_CLOSED,
181 - ),
182 - true
183 - ) ) {
184 - return array(
185 - 'type' => 'folder',
186 - 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['slug'] ),
187 - );
188 - } elseif ( Scan_Item::TYPE_VULNERABILITY === $type ) {
189 - return array(
190 - 'type' => 'folder',
191 - 'path' => $this->get_abs_plugin_path_by_slug( $raw_data['base_slug'] ),
192 - );
193 - } else {
194 - return array(
195 - 'type' => 'file',
196 - 'path' => $raw_data['file'],
197 - );
198 - }
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 );
199 262 }
200 263
201 264 /**
202 265 * Executes tasks based on the type of scan.
203 266 *
204 - * @param string $command The task to perform.
205 - * @param mixed $options Command options.
267 + * @param string $command The task to perform.
268 + * @param mixed $options Command options.
206 269 */
207 270 private function scan_task( $command, $options ) {
208 - $type = $options['type'] ? strtolower( $options['type'] ) : null;
209 - if ( defender_is_wp_org_version()
210 - && in_array(
211 - $type,
212 - array(
213 - Scan_Item::TYPE_VULNERABILITY,
214 - // TYPE_SUSPICIOUS const is not suitable for use.
215 - 'suspicious_code',
216 - ),
217 - true
218 - )
219 - ) {
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 + ) ) {
220 282 WP_CLI::warning( 'A WPMU DEV subscription is required to use this command.' );
221 283 return;
222 284 }
223 285
@@ -249,9 +311,9 @@
249 311 break;
250 312 }
251 313 $active = Model_Scan::get_active();
252 314 if ( is_object( $active ) ) {
253 - 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' );
254 316 }
255 317 $model = Model_Scan::get_last();
256 318 if ( ! is_object( $model ) ) {
257 319 return;
@@ -280,38 +342,28 @@
280 342 case 'resolve':
281 343 $items = $model->get_issues( $type, Scan_Item::STATUS_ACTIVE );
282 344 $resolved = array();
283 345 foreach ( $items as $item ) {
284 - if (
285 - in_array(
286 - $item->type,
287 - array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ),
288 - true
289 - )
290 - ) {
346 + if ( in_array( $item->type, array( Scan_Item::TYPE_INTEGRITY, Scan_Item::TYPE_PLUGIN_CHECK ), true ) ) {
291 347 WP_CLI::log( sprintf( 'Reverting %s to original', $item->raw_data['file'] ) );
292 348 $ret = $item->resolve();
293 349 if ( ! is_wp_error( $ret ) ) {
294 350 $resolved[] = $item;
295 351 } else {
296 - return WP_CLI::error( $ret->get_error_message() );
352 + WP_CLI::error( $ret->get_error_message() );
297 353 }
298 354 } elseif ( Scan_Item::TYPE_SUSPICIOUS === $item->type ) {
299 355 // If this is content, we will try to delete them.
300 - $whitelist = array(
301 - // wordfence waf.
302 - ABSPATH . '/wordfence-waf.php',
303 - // Any files inside plugins, if removed, can cause fatal error.
304 - WP_CONTENT_DIR . '/plugins/',
305 - // 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.
306 359 $this->get_path_of_themes_dir(),
307 360 );
308 361 $path = $item->raw_data['file'];
309 362 $can_delete = true;
310 - $current = '';
311 363 foreach ( $whitelist as $value ) {
312 364 $current = $value;
313 - if ( strpos( $value, $path ) > 0 ) {
365 + if ( str_contains( $path, $value ) ) {
314 366 // Ignore this.
315 367 $can_delete = false;
316 368 break;
317 369 }
@@ -317,17 +369,26 @@
317 369 }
318 370 }
319 371 if ( false === $can_delete ) {
320 372 WP_CLI::log( sprintf( 'Ignore file %s as it is in %s', $path, $current ) );
321 - } elseif ( wp_delete_file( $path ) ) {
373 + } elseif ( ! is_dir( $path ) && wp_delete_file( $path ) ) {
322 374 WP_CLI::log( sprintf( 'Delete file %s', $path ) );
323 375 $model->remove_issue( $item->id );
324 376 $resolved[] = $item;
325 377 } else {
326 - return WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
378 + WP_CLI::error( sprintf( "Can't delete file %s", $path ) );
327 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 + }
328 390 }
329 - // No result for Vulnerability, Outdated or Closed plugin types.
330 391 }
331 392 WP_CLI::log( sprintf( 'Resolved %s items', count( $resolved ) ) );
332 393 break;
333 394 case 'delete':
@@ -358,9 +419,9 @@
358 419 $model->remove_issue( $item->id );
359 420 $deleted[] = $item;
360 421 } else {
361 422
362 - 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 ) );
363 424 }
364 425 } elseif ( 'file' === $issue_type ) {
365 426 // Work with core_integrity, plugin_integrity or suspicious_code types.
366 427 if ( wp_delete_file( $path ) ) {
@@ -367,9 +428,9 @@
367 428 WP_CLI::log( sprintf( 'Delete %s: %s', $issue_type, $path ) );
368 429 $model->remove_issue( $item->id );
369 430 $deleted[] = $item;
370 431 } else {
371 - 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 ) );
372 433 }
373 434 }
374 435 }
375 436 WP_CLI::log( sprintf( 'Deleted %s items', count( $deleted ) ) );
@@ -379,12 +440,59 @@
379 440 }
380 441 }
381 442
382 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 + /**
383 491 * Generate dummy data, use in unit tests.
384 492 * DO NOT USE IN PRODUCTION.
385 493 *
386 - * @param mixed $args Command arguments.
494 + * @param mixed $args Command arguments.
387 495 */
388 496 public function seed( $args ) {
389 497 global $wp_filesystem;
390 498 // Initialize the WP filesystem, no more using 'file-put-contents' function.
@@ -396,32 +504,41 @@
396 504 WP_CLI::error( 'Invalid command' );
397 505
398 506 return;
399 507 }
508 + if ( ! $this->is_testing_mode() ) {
509 + return;
510 + }
511 +
400 512 [ $command ] = $args;
401 513 switch ( $command ) {
402 514 case 'scan:core':
515 + WP_CLI::confirm( 'This will modify a WordPress core file (wp-load.php). Are you sure?', array() );
516 +
403 517 $file_path = ABSPATH . 'wp-load.php';
404 - $content = '//this make different';
405 - if ( $wp_filesystem->exists( $file_path ) ) {
406 - $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;
407 522 }
408 - $wp_filesystem->put_contents( $file_path, $content );
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;
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' );
409 535 break;
410 536 case 'ip:logs':
537 + WP_CLI::confirm( 'This will insert fake firewall lockout log entries into the database. Are you sure?', array() );
411 538 // We will generate randomly 10k logs in 3 months.
412 - $types = array(
413 - Lockout_Log::AUTH_FAIL,
414 - Lockout_Log::AUTH_LOCK,
415 - Lockout_Log::ERROR_404,
416 - Lockout_Log::LOCKOUT_404,
417 - Lockout_Log::LOCKOUT_UA,
418 - );
419 - $is_lock = array(
420 - Lockout_Log::AUTH_LOCK,
421 - Lockout_Log::LOCKOUT_404,
422 - Lockout_Log::LOCKOUT_UA,
423 - );
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 );
424 541 $faker = Factory::create();
425 542 WP_CLI::log( $faker->ipv4 );
426 543 $range = array(
427 544 'today midnight' => array( 'now', 100 ),
@@ -436,9 +553,9 @@
436 553 'ua_lockout' => 0,
437 554 );
438 555 $last_lockout = 0;
439 556 foreach ( $range as $date => $to ) {
440 - [ $to, $count ] = $to;
557 + [$to, $count] = $to;
441 558 for ( $i = 0; $i < $count; $i++ ) {
442 559 $model = new Lockout_Log();
443 560 $model->ip = $faker->ipv4;
444 561 $model->type = $types[ array_rand( $types ) ];
@@ -480,9 +597,9 @@
480 597 /**
481 598 * Clean up dummy data.
482 599 * DO NOT USE IN PRODUCTION.
483 600 *
484 - * @param mixed $args Command arguments.
601 + * @param mixed $args Command arguments.
485 602 */
486 603 public function unseed( $args ) {
487 604 global $wp_filesystem;
488 605 // Initialize the WP filesystem, no more using 'file-put-contents' function.
@@ -494,15 +611,38 @@
494 611 WP_CLI::error( 'Invalid command' );
495 612
496 613 return;
497 614 }
615 + if ( ! $this->is_testing_mode() ) {
616 + return;
617 + }
618 +
498 619 [ $command ] = $args;
499 620 switch ( $command ) {
500 621 case 'scan:core':
501 - $content = file_get_contents( ABSPATH . 'wp-load.php' );
502 - $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 ) );
503 642 break;
504 643 case 'scan:suspicious':
644 + WP_CLI::confirm( 'This will delete the false-positive test file. Are you sure?', array() );
505 645 wp_delete_file( WP_CONTENT_DIR . '/false-positive.php' );
506 646 break;
507 647 default:
508 648 break;
@@ -509,94 +649,96 @@
509 649 }
510 650 }
511 651
512 652 /**
513 - * Clears the audit log from Database.
514 - * <command> reset
653 + * Manage audit logs via WP-CLI.
515 654 *
516 - * @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.
517 676 */
518 677 public function audit( $args ) {
519 - if ( defender_is_wp_org_version() ) {
520 - WP_CLI::warning( 'A WPMU DEV subscription is required to use this command.' );
678 + if ( ! is_array( $args ) || array() === $args ) {
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 + );
687 +
521 688 return;
522 689 }
523 - }
524 690
525 - /**
526 - * Starts a full scan based on the provided options.
527 - *
528 - * @param array $options Command options.
529 - */
530 - private function scan_all( $options ) {
531 - $type = $options['type'] ?? null;
532 - $is_detailed = false;
533 - switch ( $type ) {
534 - case null:
535 - // All items.
536 - $type = null;
691 + [$command] = $args;
692 + switch ( $command ) {
693 + case 'reset':
694 + wd_di()->get( Audit::class )->reset();
695 +
696 + WP_CLI::log( 'All clear' );
537 697 break;
538 - case 'detailed':
539 - $is_detailed = true;
540 - break;
541 698 default:
542 - WP_CLI::error( sprintf( 'Unknown scan type %s', $type ) );
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 + );
543 707 break;
544 708 }
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 709 }
586 710
587 711 /**
588 - * This is a helper for Security header actions.
589 - * #Options
712 + * Manage security headers via WP-CLI.
713 + *
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,79 @@
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 851 wd_di()->get( Login_Access::class )->remove_settings();
852 + wd_di()->get( Audit_Logging::class )->remove_settings();
672 853 wd_di()->get( Dashboard::class )->remove_settings();
673 854 wd_di()->get( Security_Tweaks::class )->remove_settings();
674 855 wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings();
675 856 // Parent and submodules.
@@ -681,45 +862,99 @@
681 862 wd_di()->get( Main_Setting::class )->remove_settings();
682 863 WP_CLI::log( 'All cleared!' );
683 864 break;
684 865 default:
685 - WP_CLI::log( sprintf( 'Unknown command %s, use correct arguments. See below...', $command ) );
686 - 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 + );
687 874 break;
688 875 }
689 876 }
690 877
691 878 /**
692 - * This toggle the firewall submodules, clears the data, show details or unlocks the IP from block list.
693 - * Syntax: wp defender firewall <command> <args_1> <args_2>
694 - * <command> clear|unblock|list|activate|deactivate
879 + * Manage firewall submodules, data, and lockouts via WP-CLI.
695 880 *
696 - * <args_1> Allowed values are: ip, user_agent, files and maxmind
697 - * <args_2> Allowed values are: allowlist, blocklist, country_allowlist, country_blocklist, lockout and license_key
698 - * Example: wp defender firewall clear ip allowlist
699 - * Example: wp defender firewall unblock ip lockout --ips=127.0.0.1,236.211.38.221
700 - * Example: wp defender firewall list user_agent <status>
701 - * Example: wp defender firewall activate submodule <submodule>
702 - * Example: wp defender firewall deactivate submodule login_protection
703 - * <status> Allowed values are: all, allowlist, blocklist.
704 - * <submodule> Allowed values are: login_protection, 404_detection or user_agent.
881 + * ## OPTIONS
705 882 *
706 - * @param mixed $args Command arguments.
707 - * @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.
708 926 */
709 927 public function firewall( $args, $options ) {
710 - if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) <= 2 ) {
711 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
712 - 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 + );
713 938
714 939 return;
715 940 }
716 941
717 - [ $command, $type, $field ] = $args;
718 - if ( ! is_string( $type ) || '' === $type || ! is_string( $field ) || '' === $field ) {
719 - WP_CLI::log( 'Invalid option.' );
720 - 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' : '' );
721 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 +
722 957 return;
723 958 }
724 959 switch ( $command ) {
725 960 case 'clear':
@@ -743,41 +978,12 @@
743 978 }
744 979 }
745 980
746 981 /**
747 - * This clears the mask login settings.
748 - * <command> clear
749 - * This command must have this command
750 - * Syntax: wp defender mask_login <command>
751 - * Example: wp defender mask_login clear
752 - *
753 - * @param mixed $args Command arguments.
754 - */
755 - public function mask_login( $args ) {
756 - if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
757 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
758 - WP_CLI::runcommand( 'defender mask_login --help' );
759 -
760 - return;
761 - }
762 -
763 - [ $command ] = $args;
764 - switch ( $command ) {
765 - case 'clear':
766 - wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->delete();
767 - WP_CLI::log( 'Mask login settings cleared!' );
768 - break;
769 - default:
770 - WP_CLI::error( sprintf( 'Unknown command %s', $command ) );
771 - break;
772 - }
773 - }
774 -
775 - /**
776 982 * Clears the firewall data based on the specified type and field.
777 983 *
778 - * @param string $type The type of data to clear.
779 - * @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.
780 986 */
781 987 private function clear_firewall( $type, $field ) {
782 988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
783 989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
@@ -782,17 +988,29 @@
782 988 $type_default = array( 'ip', 'files', 'user_agent', 'maxmind' );
783 989 $field_default = array( 'blocklist', 'allowlist', 'country_allowlist', 'country_blocklist', 'license_key' );
784 990
785 991 if ( ! in_array( $type, $type_default, true ) ) {
786 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
787 - 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 + );
788 1000
789 1001 return;
790 1002 }
791 1003
792 1004 if ( ! in_array( $field, $field_default, true ) ) {
793 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
794 - 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 + );
795 1013
796 1014 return;
797 1015 }
798 1016
@@ -826,9 +1044,9 @@
826 1044 $model->save();
827 1045 } elseif ( 'maxmind' === $type ) {
828 1046 try {
829 1047 $model = wd_di()->get( \WP_Defender\Model\Setting\Blacklist_Lockout::class );
830 - if ( ! is_null( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
1048 + if ( is_string( $model->geodb_path ) && is_file( $model->geodb_path ) ) {
831 1049 wp_delete_file( $model->geodb_path );
832 1050 }
833 1051 $model->maxmind_license_key = '';
834 1052 $model->geodb_path = '';
@@ -841,13 +1059,39 @@
841 1059 WP_CLI::log( sprintf( 'Firewall %s %s is cleared.', str_replace( '_', ' ', $field ), $type ) );
842 1060 }
843 1061
844 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 + /**
845 1089 * Unblocks the specified IPs from the firewall.
846 1090 *
847 - * @param string $type The type of data to unblock.
848 - * @param string $field The specific field to unblock.
849 - * @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.
850 1094 */
851 1095 private function unblock_firewall( $type, $field, $options ) {
852 1096 $type_default = array( 'ip' );
853 1097 $field_default = array( 'lockout' );
@@ -852,17 +1096,29 @@
852 1096 $type_default = array( 'ip' );
853 1097 $field_default = array( 'lockout' );
854 1098
855 1099 if ( ! in_array( $type, $type_default, true ) ) {
856 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
857 - 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 + );
858 1108
859 1109 return;
860 1110 }
861 1111
862 1112 if ( ! in_array( $field, $field_default, true ) ) {
863 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
864 - 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 + );
865 1121
866 1122 return;
867 1123 }
868 1124
@@ -874,10 +1130,16 @@
874 1130 $model->status = Lockout_Ip::STATUS_NORMAL;
875 1131 $model->save();
876 1132 }
877 1133 } else {
878 - WP_CLI::log( 'Option \'ips\' is not provided. See below...' );
879 - 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 + );
880 1142
881 1143 return;
882 1144 }
883 1145
@@ -887,10 +1149,10 @@
887 1149 /**
888 1150 * Lists details for the firewall based on the specified type and field.
889 1151 * Example: wp defender firewall list user_agent all
890 1152 *
891 - * @param string $type The type of data to list.
892 - * @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.
893 1155 *
894 1156 * @since v2.6.4. Add the details for User Agent Banning.
895 1157 */
896 1158 private function list_firewall( $type, $field ) {
@@ -896,16 +1158,28 @@
896 1158 private function list_firewall( $type, $field ) {
897 1159 $type_default = array( 'user_agent' );
898 1160 $field_default = array( 'all', 'allowlist', 'blocklist' );
899 1161 if ( ! in_array( $type, $type_default, true ) ) {
900 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $type ) );
901 - 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 + );
902 1170
903 1171 return;
904 1172 }
905 1173 if ( ! in_array( $field, $field_default, true ) ) {
906 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $field ) );
907 - 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 + );
908 1182
909 1183 return;
910 1184 }
911 1185 $model = wd_di()->get( User_Agent_Lockout::class );
@@ -928,22 +1202,34 @@
928 1202 * Change status of Firewall submodules: login_protection, 404_detection or user_agent.
929 1203 * Example: wp defender firewall activate submodule user_agent
930 1204 * Example: wp defender firewall deactivate submodule login_protection
931 1205 *
932 - * @param string $key_word The keyword to identify the action.
933 - * @param string $submodule The submodule to toggle.
934 - * @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).
935 1209 */
936 1210 private function toggle_firewall_submodule( $key_word, $submodule, $action ) {
937 1211 if ( 'submodule' !== $key_word ) {
938 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $key_word ) );
939 - 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 + );
940 1220
941 1221 return;
942 1222 }
943 1223 if ( ! in_array( $submodule, array( 'login_protection', '404_detection', 'user_agent' ), true ) ) {
944 - WP_CLI::log( sprintf( 'Invalid option %s. See below...', $submodule ) );
945 - 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 + );
946 1232
947 1233 return;
948 1234 }
949 1235 // Get submodule slug.
@@ -977,50 +1263,98 @@
977 1263 WP_CLI::success( sprintf( 'Firewall "%s" has been %s.', $submodule, $text ) );
978 1264 }
979 1265
980 1266 /**
981 - * 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.
982 1269 *
983 - * @param string $field The field name to rename.
984 - *
985 - * @return string The renamed field name.
1270 + * @return bool
986 1271 */
987 - private function rename_field( $field ) {
988 - if ( '' !== $field ) {
989 - 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;
990 1277 }
991 1278
992 - return '';
1279 + return true;
993 1280 }
994 1281
995 1282 /**
996 - * 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 + * ---
997 1290 *
998 - * @param string $field The field to check.
1291 + * ## EXAMPLES
999 1292 *
1000 - * @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.
1001 1298 */
1002 - private function is_country( $field ) {
1003 - 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 + }
1004 1323 }
1005 1324
1006 1325 /**
1007 - * Force Bulk Password Reset.
1326 + * Manage bulk password reset via WP-CLI.
1327 + *
1328 + * ## OPTIONS
1329 + *
1008 1330 * <command>
1009 - * : Value can be force|undo
1010 - * Syntax: wp defender password_reset <command>
1011 - * Example: wp defender password_reset force
1331 + * : Action to perform.
1332 + * ---
1333 + * options:
1334 + * - force
1335 + * - undo
1336 + * ---
1012 1337 *
1013 - * @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.
1014 1348 */
1015 1349 public function password_reset( $args ) {
1016 1350 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1017 - WP_CLI::log( 'Invalid command.' );
1351 + WP_CLI::error( 'Invalid command.' );
1018 1352
1019 1353 return;
1020 1354 }
1021 1355
1022 - [ $command ] = $args;
1356 + [$command] = $args;
1023 1357 switch ( $command ) {
1024 1358 case 'force':
1025 1359 // Get the model instance.
1026 1360 $model = wd_di()->get( Password_Reset::class );
@@ -1026,12 +1360,9 @@
1026 1360 $model = wd_di()->get( Password_Reset::class );
1027 1361 $model->expire_force = true;
1028 1362 $model->force_time = time();
1029 1363 $model->save();
1030 - $message = sprintf(
1031 - 'Passwords created before %s are required to be reset upon next login.',
1032 - $this->format_date_time( $model->force_time )
1033 - );
1364 + $message = sprintf( 'Passwords created before %s are required to be reset upon next login.', $this->format_date_time( $model->force_time ) );
1034 1365 WP_CLI::log( $message );
1035 1366 break;
1036 1367 case 'undo':
1037 1368 $model = wd_di()->get( Password_Reset::class );
@@ -1045,36 +1376,42 @@
1045 1376 }
1046 1377 }
1047 1378
1048 1379 /**
1049 - * Clear completed action scheduler logs.
1050 - */
1051 - private function scan_clear_logs() {
1052 - $scan_component = wd_di()->get( Scan_Component::class );
1053 - $result = $scan_component::clear_logs();
1054 - $message = $result['success'] ?? $result['error'] ?? 'Malware scan logs are cleared';
1055 -
1056 - WP_CLI::log( $message );
1057 - }
1058 -
1059 - /**
1060 - * Delete old logs.
1061 - * <command> delete
1062 - * This command must have this command
1063 - * Syntax: wp defender logs <command>
1064 - * Example: wp defender logs delete
1380 + * Manage Defender's internal log files.
1065 1381 *
1066 - * @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.
1067 1398 */
1068 1399 public function logs( $args ) {
1069 1400 if ( ( is_array( $args ) || $args instanceof Countable ? count( $args ) : 0 ) < 1 ) {
1070 - WP_CLI::log( 'Invalid command, add necessary arguments. See below...' );
1071 - 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 + );
1072 1409
1073 1410 return;
1074 1411 }
1075 1412
1076 - [ $command ] = $args;
1413 + [$command] = $args;
1077 1414
1078 1415 switch ( $command ) {
1079 1416 case 'delete':
1080 1417 $rotation_logger = wd_di()->get( Rotation_Logger::class );
@@ -1087,24 +1424,45 @@
1087 1424 }
1088 1425 }
1089 1426
1090 1427 /**
1091 - * This is a helper for Captcha actions.
1092 - * Syntax: wp defender captcha <command>
1093 - * <command> activate|deactivate|clear
1094 - * Example: wp defender captcha activate
1428 + * Manage CAPTCHA settings via WP-CLI.
1095 1429 *
1096 - * @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.
1097 1456 */
1098 1457 public function captcha( $args ) {
1099 1458 if ( ! is_array( $args ) || array() === $args ) {
1100 1459 WP_CLI::error( 'Invalid command.' );
1101 - WP_CLI::runcommand( 'defender captcha --help' );
1102 1460
1103 1461 return;
1104 1462 }
1105 - $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1106 - [ $command ] = $args;
1463 + $model = wd_di()->get( \WP_Defender\Model\Setting\Captcha::class );
1464 + [$command] = $args;
1107 1465 switch ( $command ) {
1108 1466 case 'activate':
1109 1467 if ( true !== $model->enabled ) {
1110 1468 $model->enabled = true;
@@ -1116,9 +1474,8 @@
1116 1474 if ( false !== $model->enabled ) {
1117 1475 $model->enabled = false;
1118 1476 $model->save();
1119 1477 }
1120 - $model->save();
1121 1478 WP_CLI::log( 'CAPTCHA is deactivated.' );
1122 1479 break;
1123 1480 case 'clear':
1124 1481 $default_values = $model->get_default_values();
@@ -1159,9 +1516,8 @@
1159 1516 WP_CLI::log( 'CAPTCHA is cleared.' );
1160 1517 break;
1161 1518 default:
1162 1519 WP_CLI::error( sprintf( 'Unknown command %s.', $command ) );
1163 - WP_CLI::runcommand( 'defender captcha --help' );
1164 1520 break;
1165 1521 }
1166 1522 }
1167 1523 }