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

class-lockout-log.php in Defender Security – Malware Scanner, Login Security & Firewall 5.6.0, at src/model/class-lockout-log.php

717 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles interactions with the database table for lockout logs.
4 *
5 * @package WP_Defender\Model
6 */
7
8 namespace WP_Defender\Model;
9
10 use WP_Defender\DB;
11 use Calotes\Base\Model;
12 use WP_Defender\Traits\Formats;
13 use WP_Defender\Component\User_Agent;
14 use WP_Defender\Component\Table_Lockout;
15 use WP_Defender\Model\Setting\User_Agent_Lockout;
16
17 /**
18 * Model for the lockout log table.
19 */
20 class Lockout_Log extends DB {
21
22 use Formats;
23
24 public const AUTH_FAIL = 'auth_fail';
25 public const AUTH_LOCK = 'auth_lock';
26 public const IP_UNLOCK = 'ip_unlock';
27
28 public const ERROR_404 = '404_error';
29 public const LOCKOUT_404 = '404_lockout';
30 public const ERROR_404_IGNORE = '404_error_ignore';
31 public const LOCKOUT_MALICIOUS_BOT = 'malicious_bot';
32 public const LOCKOUT_FAKE_BOT = 'fake_bot';
33
34 public const LOCKOUT_UA = 'ua_lockout';
35 // Different IP Lockout types.
36 public const LOCKOUT_IP_CUSTOM = 'custom_lockout';
37
38 public const INFINITE_SCROLL_SIZE = 50;
39
40 /**
41 * Table name.
42 *
43 * @var string
44 */
45 protected $table = 'defender_lockout_log';
46
47 /**
48 * Primary key column.
49 *
50 * @var int
51 * @defender_property
52 */
53 public $id;
54 /**
55 * Table column for log.
56 *
57 * @var string
58 * @defender_property
59 */
60 public $log;
61 /**
62 * Table column for IP address.
63 *
64 * @var string
65 * @defender_property
66 */
67 public $ip;
68 /**
69 * Table column for date.
70 *
71 * @var int
72 * @defender_property
73 */
74 public $date;
75 /**
76 * Table column for user agent.
77 *
78 * @var string
79 * @defender_property
80 */
81 public $user_agent;
82 /**
83 * Table column for type.
84 *
85 * @var string
86 * @defender_property
87 */
88 public $type;
89 /**
90 * Table column for blog id.
91 *
92 * @var int
93 * @defender_property
94 */
95 public $blog_id;
96 /**
97 * Table column for tried.
98 *
99 * @var string
100 * @defender_property
101 */
102 public $tried;
103 /**
104 * Table column for country iso code.
105 *
106 * @var string
107 * @defender_property
108 */
109 public $country_iso_code;
110
111 /**
112 * Query the logs based on the provided filters and pagination settings.
113 *
114 * @param array $filters An array of filters to apply to the query. Default is an empty array.
115 * The following filters are supported:
116 * - from: The start date for the date range filter.
117 * - to: The end date for the date range filter.
118 * - ip: The IP address to filter by.
119 * - type: The type of log to filter by.
120 * - ban_status: The ban status to filter by.
121 * @param int $paged The page number of the results to retrieve. Default is 1.
122 * @param string $order_by The field to order the results by. Default is 'id'.
123 * @param string $order The order direction of the results. Default is 'desc'.
124 * @param int $page_size The number of results to retrieve per page. Default is 50.
125 * Set to -1 to retrieve all results.
126 *
127 * @return array An array of Lockout_Log objects representing the queried logs.
128 */
129 public static function query_logs(
130 $filters = array(),
131 $paged = 1,
132 $order_by = 'id',
133 $order = 'desc',
134 $page_size = 50
135 ): array {
136 $orm = self::get_orm();
137 $orm->get_repository( self::class )
138 ->where(
139 'date',
140 'between',
141 array(
142 $filters['from'],
143 $filters['to'],
144 )
145 );
146
147 if ( isset( $filters['ip'] ) && '' !== $filters['ip'] ) {
148 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
149 }
150 if ( isset( $filters['type'] ) && '' !== $filters['type'] ) {
151 $orm->where( 'type', $filters['type'] );
152 }
153
154 if ( isset( $filters['ban_status'] ) && '' !== $filters['ban_status'] ) {
155 self::apply_ban_status_filter( $orm, $filters );
156 }
157
158 if ( '' !== $order_by && '' !== $order ) {
159 $orm->order_by( $order_by, $order );
160 }
161 if ( $page_size > 0 ) {
162 $offset = ( $paged - 1 ) * $page_size;
163 $orm->limit( $page_size, $offset );
164 }
165
166 return $orm->get();
167 }
168
169 /**
170 * Count the number of records in the database table based on the provided filters.
171 *
172 * @param mixed $date_from The start date for the date range filter.
173 * @param mixed $date_to The end date for the date range filter.
174 * @param mixed $type The type of log to filter by.
175 * @param string $ip The IP address to filter by. Default is an empty string.
176 * @param array $filters An array of additional filters to apply to the query. Default is an empty array.
177 * The following filters are supported:
178 * - ban_status: The ban status to filter by.
179 *
180 * @return string|null The number of records matching the provided filters, or null if an error occurred.
181 */
182 public static function count( $date_from, $date_to, $type, $ip = '', $filters = array() ): ?string {
183 $orm = self::get_orm();
184 $orm->get_repository( self::class )
185 ->where(
186 'date',
187 'between',
188 array(
189 $date_from,
190 $date_to,
191 )
192 );
193
194 if ( is_array( $type ) && array() !== $type ) {
195 $orm->where( 'type', 'in', $type );
196 } elseif ( is_string( $type ) && '' !== trim( $type ) ) {
197 $orm->where( 'type', $type );
198 }
199
200 if ( is_string( $ip ) && '' !== trim( $ip ) ) {
201 $orm->where( 'ip', 'like', "%$ip%" );
202 }
203
204 if ( isset( $filters['ban_status'] ) && '' !== trim( $filters['ban_status'] ) ) {
205 $ban_status_where = self::ban_status_where( $filters['ban_status'] );
206
207 if ( 3 === count( $ban_status_where ) ) {
208 $orm->where( ...$ban_status_where );
209 }
210 }
211
212 return $orm->count();
213 }
214
215 /**
216 * Count login lockout in the last 7 days.
217 *
218 * @return string|null
219 */
220 public static function count_login_lockout_last_7_days(): ?string {
221 $start = strtotime( '-7 days' );
222 $end = time();
223
224 return self::count( $start, $end, self::AUTH_LOCK );
225 }
226
227 /**
228 * Count 404 lockout in the last 7 days.
229 *
230 * @return string|null
231 */
232 public static function count_404_lockout_last_7_days(): ?string {
233 $start = strtotime( '-7 days' );
234 $end = time();
235
236 return self::count( $start, $end, self::LOCKOUT_404 );
237 }
238
239 /**
240 * Count UA lockout in the last 7 days.
241 *
242 * @return string|null
243 */
244 public static function count_ua_lockout_last_7_days(): ?string {
245 $start = strtotime( '-7 days' );
246 $end = time();
247
248 return self::count( $start, $end, self::LOCKOUT_UA );
249 }
250
251 /**
252 * A shortcut for quickly count lockout in last 24 hours.
253 *
254 * @return string|null
255 */
256 public static function count_lockout_in_24_hours(): ?string {
257 $start = strtotime( '-24 hours' );
258 $end = time();
259
260 return self::count(
261 $start,
262 $end,
263 array(
264 self::AUTH_LOCK,
265 self::LOCKOUT_404,
266 self::LOCKOUT_UA,
267 )
268 );
269 }
270
271 /**
272 * A shortcut for quickly count lockout in last 7 days.
273 *
274 * @return string|null
275 */
276 public static function count_lockout_in_7_days(): ?string {
277 $start = strtotime( '-7 days' );
278 $end = time();
279
280 return self::count(
281 $start,
282 $end,
283 array(
284 self::AUTH_LOCK,
285 self::LOCKOUT_404,
286 self::LOCKOUT_UA,
287 )
288 );
289 }
290
291 /**
292 * A shortcut for count lockout in 30 days.
293 *
294 * @return string|null
295 */
296 public static function count_lockout_in_30_days(): ?string {
297 $start = strtotime( '-30 days' );
298 $end = time();
299
300 return self::count(
301 $start,
302 $end,
303 array(
304 self::AUTH_LOCK,
305 self::LOCKOUT_404,
306 self::LOCKOUT_UA,
307 // LOCKOUT_IP_CUSTOM is not taken into account.
308 )
309 );
310 }
311
312 /**
313 * Retrieves the date of the last lockout that occurred within the last 30 days.
314 *
315 * @param bool $for_hub (optional) Whether to format the date for the persistent hub. Default is false.
316 *
317 * @return string The formatted date of the last lockout, or 'n/a' if no lockouts were found.
318 */
319 public static function get_last_lockout_date( $for_hub = false ) {
320 $data = self::query_logs(
321 array(
322 'from' => strtotime( '-30 days' ),
323 'to' => time(),
324 ),
325 1,
326 'id',
327 'desc',
328 1
329 );
330 $last = array_shift( $data );
331 if ( ! is_object( $last ) ) {
332 return 'n/a';
333 }
334
335 return $for_hub
336 ? $last->persistent_hub_datetime_format( $last->date )
337 : $last->format_date_time( $last->date );
338 }
339
340 /**
341 * Remove all data.
342 *
343 * @return bool|int
344 */
345 public static function truncate() {
346 $orm = self::get_orm();
347
348 return $orm->get_repository( self::class )
349 ->truncate();
350 }
351
352 /**
353 * Remove logs based on timestamp and limit.
354 *
355 * @param int $timestamp The timestamp to filter logs by.
356 * @param int $limit The maximum number of logs to delete.
357 *
358 * @return void
359 */
360 public static function remove_logs( $timestamp, $limit ) {
361 $orm = self::get_orm();
362 $orm->get_repository( self::class )
363 ->where( 'date', '<=', $timestamp )
364 ->order_by( 'id' )
365 ->limit( $limit )
366 ->delete_by_limit();
367 }
368
369 /**
370 * Get log summary.
371 *
372 * @return array
373 */
374 public static function get_summary(): array {
375 // Time.
376 $current_time = time();
377 $today_midnight = strtotime( '-24 hours', $current_time );
378 $first_this_week = strtotime( '-7 days', $current_time );
379 $first_this_month = strtotime( '-30 days', $current_time );
380
381 // Prepare columns.
382 $select = array(
383 'MAX(date) as lockout_last',
384 'COUNT(*) as lockout_this_month',
385 // 24 hours
386 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
387 "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_404 . "', 1, NULL)) as lockout_404_today",
388 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
389 "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_today",
390 // 7 days
391 "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_404 . "', 1, NULL)) as lockout_404_this_week",
392 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
393 "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_week",
394 // 30 days
395 "COUNT(IF(date > {$first_this_month} AND type = '" . self::LOCKOUT_404 . "', 1, NULL)) as lockout_404_this_month",
396 "COUNT(IF(date > {$first_this_month} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_month",
397 "COUNT(IF(date > {$first_this_month} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_month",
398 );
399 $select = implode( ',', $select );
400
401 $orm = self::get_orm();
402 $result = $orm->get_repository( self::class )
403 ->select( $select )
404 // LOCKOUT_IP_CUSTOM is not taken into account.
405 ->where( 'type', 'in', array( self::LOCKOUT_404, self::AUTH_LOCK, self::LOCKOUT_UA ) )
406 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
407 ->get_results();
408
409 return $result[0] ?? array();
410 }
411
412 /**
413 * Returns the log tag based on the given type.
414 *
415 * @param string $type The type of the log.
416 *
417 * @return string The log tag.
418 */
419 protected static function get_log_tag( $type ): string {
420 switch ( $type ) {
421 case self::LOCKOUT_404:
422 case self::ERROR_404:
423 case self::ERROR_404_IGNORE:
424 $tag = '404';
425 break;
426 case self::AUTH_FAIL:
427 case self::AUTH_LOCK:
428 $tag = 'login';
429 break;
430 case self::LOCKOUT_IP_CUSTOM:
431 $tag = 'Custom';
432 break;
433 case self::IP_UNLOCK:
434 $tag = 'Unlock';
435 break;
436 case self::LOCKOUT_UA:
437 case self::LOCKOUT_MALICIOUS_BOT:
438 case self::LOCKOUT_FAKE_BOT:
439 default:
440 $tag = 'bots';
441 break;
442 }
443
444 return $tag;
445 }
446
447 /**
448 * Returns the CSS class for the log container based on the given type.
449 *
450 * @param string $type The type of the log.
451 *
452 * @return string The CSS class for the log container.
453 */
454 protected static function get_log_container_class( $type ): string {
455 switch ( $type ) {
456 case self::AUTH_LOCK:
457 case self::LOCKOUT_404:
458 case self::LOCKOUT_UA:
459 case self::LOCKOUT_MALICIOUS_BOT:
460 case self::LOCKOUT_FAKE_BOT:
461 $class = 'sui-error';
462 break;
463 case self::AUTH_FAIL:
464 case self::ERROR_404:
465 case self::ERROR_404_IGNORE:
466 default:
467 $class = 'sui-warning';
468 break;
469 }
470
471 return $class;
472 }
473
474 /**
475 * Retrieves logs from the database and formats them for display on the frontend.
476 *
477 * @param array $filters An array of filters to apply to the query. Default is an empty array.
478 * The following filters are supported:
479 * - from: The start date for the date range filter.
480 * - to: The end date for the date range filter.
481 * - ip: The IP address to filter by.
482 * - type: The type of log to filter by.
483 * - ban_status: The ban status to filter by.
484 * @param int $paged The page number of the results to retrieve. Default is 1.
485 * @param string $order_by The field to order the results by. Default is 'id'.
486 * @param string $order The order direction of the results. Default is 'desc'.
487 * @param int $page_size The number of results to retrieve per page. Default is 50.
488 * Set to -1 to retrieve all results.
489 *
490 * @return array An array of formatted log entries.
491 */
492 public static function get_logs_and_format(
493 $filters = array(),
494 $paged = 1,
495 $order_by = 'id',
496 $order = 'desc',
497 $page_size = 50
498 ): array {
499 $logs = self::query_logs( $filters, $paged, $order_by, $order, $page_size );
500
501 return self::format_logs( $logs );
502 }
503
504 /**
505 * Get the first log by ID.
506 *
507 * @param int $id The ID of the log.
508 *
509 * @return null|Model
510 */
511 public static function find_by_id( $id ): ?Model {
512 $orm = self::get_orm();
513
514 return $orm->get_repository( self::class )
515 ->where( 'id', $id )
516 ->first();
517 }
518
519 /**
520 * Delete current log.
521 */
522 public function delete() {
523 $orm = self::get_orm();
524 $orm->get_repository( self::class )->delete(
525 array(
526 'id' => $this->id,
527 )
528 );
529 }
530
531 /**
532 * Apply ban status filtering based on lockout type.
533 *
534 * @param object $orm The ORM query builder.
535 * @param array $filters The filters array.
536 */
537 private static function apply_ban_status_filter( $orm, $filters ) {
538 $ban_status = $filters['ban_status'];
539 $type = $filters['type'] ?? 'all';
540
541 // Define lockout type categories.
542 $ua_types = array( self::get_ua_lockout_types() );
543
544 if ( 'all' === $type || '' === $type ) {
545 // For 'all' type, only show UA types with ban_status filtering.
546 $ban_status_where = self::ban_status_where( $ban_status );
547 if ( 3 === count( $ban_status_where ) ) {
548 $orm->where( 'type', 'in', $ua_types );
549 $orm->where( ...$ban_status_where );
550 }
551 } elseif ( in_array( $type, $ua_types, true ) ) {
552 // For UA-specific types, apply UA filtering.
553 $ban_status_where = self::ban_status_where( $ban_status );
554 if ( 3 === count( $ban_status_where ) ) {
555 $orm->where( ...$ban_status_where );
556 }
557 }
558 }
559
560 /**
561 * Prepare user-agent where condition based on the ban status variant.
562 *
563 * @param string $ban_status_type Ban status type.
564 *
565 * @return array Where condition arguments or empty array.
566 */
567 private static function ban_status_where( $ban_status_type ): array {
568 $table_lockout = wd_di()->get( Table_Lockout::class );
569 $ua_model = wd_di()->get( User_Agent_Lockout::class );
570
571 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
572 $blocklist = $ua_model->get_all_selected_blocklist_ua();
573 if ( array() === $blocklist ) {
574 return array();
575 }
576 $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
577 return array( 'user_agent', 'not regexp', '(?i)' . implode( '|', $escaped ) );
578 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
579 $blocklist = $ua_model->get_all_selected_blocklist_ua();
580 if ( array() === $blocklist ) {
581 return array();
582 }
583 $escaped = array_map( 'preg_quote', $blocklist, array_fill( 0, count( $blocklist ), '' ) );
584 return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
585 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
586 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
587 if ( array() === $allowlist ) {
588 return array();
589 }
590 $escaped = array_map( 'preg_quote', $allowlist, array_fill( 0, count( $allowlist ), '' ) );
591 return array( 'user_agent', 'regexp', '(?i)' . implode( '|', $escaped ) );
592 }
593
594 return array();
595 }
596
597 /**
598 * Format logs for ready to use on frontend.
599 *
600 * @param array $logs An array of log entries.
601 *
602 * @return array
603 * @since 3.11.0
604 */
605 public static function format_logs( array $logs ): array {
606 $data = array();
607 $ua_model = wd_di()->get( User_Agent_Lockout::class );
608 foreach ( $logs as $item ) {
609 $ip_model = Lockout_Ip::get( $item->ip );
610
611 // Escape object properties received from end user.
612 $item->log = sanitize_textarea_field( $item->log );
613 $item->tried = sanitize_textarea_field( $item->tried );
614
615 $arr_ip_statuses = $ip_model->get_access_status();
616
617 $log = $item->export();
618
619 // Escape array keys received from end user.
620 $log['log'] = sanitize_textarea_field( $log['log'] );
621 $log['tried'] = sanitize_textarea_field( $log['tried'] );
622
623 $log['date'] = $item->format_date_time( $item->date );
624 $log['format_date'] = $item->get_date( $item->date );
625 $log['tag'] = self::get_log_tag( $item->type );
626 $log['container_class'] = self::get_log_container_class( $item->type );
627 if ( self::LOCKOUT_UA === $item->type ) {
628 if ( User_Agent::REASON_BAD_POST === $item->tried ) {
629 $log['description'] = esc_html__(
630 'Lockout occurred due to attempted access with empty User-Agent and Referer headers. By default, IP addresses that send POST requests with empty User-Agent and Referer headers will be automatically banned. You can disable this option in the User Agent Banning settings, or you can unban the locked out IP address below.',
631 'defender-security'
632 );
633 $log['type_label'] = esc_html__( 'Type', 'defender-security' );
634 $log['type_value'] = esc_html__( 'Empty Headers', 'defender-security' );
635 $arr_statuses = $arr_ip_statuses;
636 } else {
637 $log['description'] = sprintf(
638 /* translators: 1. Log. 2. User agent. */
639 esc_html__(
640 '%1$s: %2$s. This user agent is considered bad bots and may harm your site.',
641 'defender-security'
642 ),
643 sanitize_textarea_field( $item->log ),
644 '<strong>' . sanitize_textarea_field( $item->user_agent ) . '</strong>'
645 );
646 $log['type_label'] = esc_html__( 'User Agent name', 'defender-security' );
647 $log['type_value'] = sanitize_textarea_field( $item->user_agent );
648 $log['access_status_ip'] = $arr_ip_statuses;
649 $arr_statuses = $ua_model->get_access_status( $item->user_agent );
650 }
651 } else {
652 $log['description'] = sanitize_textarea_field( $item->log );
653 $log['type_label'] = esc_html__( 'Type', 'defender-security' );
654 $log['type_value'] = str_replace( '_', ' ', $item->type );
655 $arr_statuses = $arr_ip_statuses;
656
657 if ( 'malicious_bot' === $item->type ) {
658 $log['access_status_ua'] = $ua_model->get_access_status( $item->user_agent );
659 }
660 }
661 // There may be several statuses.
662 $log['access_status'] = $arr_statuses;
663
664 // For UA lockout types, show UA status; for others show IP status.
665 if ( in_array( $item->type, array( self::get_ua_lockout_types() ), true ) ) {
666 $ua_statuses = $ua_model->get_access_status( $item->user_agent );
667 $log['access_status_text'] = $ip_model->get_access_status_text( $ua_statuses[0] ?? 'na' );
668 } else {
669 $log['access_status_text'] = $ip_model->get_access_status_text( $arr_statuses[0] );
670 }
671 $data[] = $log;
672 }
673
674 return $data;
675 }
676
677 /**
678 * Determine if the IP should be added to the database based on the timeframe.
679 *
680 * @return bool True if the IP should be added to the database, false otherwise.
681 */
682 public function has_recent_ip_log(): bool {
683 // Ensure IP is set before proceeding.
684 if ( ! is_string( $this->ip ) || '' === trim( $this->ip ) ) {
685 return false;
686 }
687
688 $orm = self::get_orm();
689 // Query the latest log for the current IP.
690 $latest_log = $orm->get_repository( self::class )
691 ->select( 'date' )
692 ->where( 'ip', $this->ip )
693 ->order_by( 'date', 'desc' )
694 ->first();
695
696 if ( null !== $latest_log ) {
697 // Return true if the log is within the 5-minute timeframe.
698 return ( time() - $latest_log->date ) <= 300;
699 }
700
701 return false;
702 }
703
704 /**
705 * Get UA lockout types.
706 *
707 * @return array
708 */
709 public static function get_ua_lockout_types(): array {
710 return array(
711 self::LOCKOUT_UA,
712 self::LOCKOUT_MALICIOUS_BOT,
713 self::LOCKOUT_FAKE_BOT,
714 );
715 }
716 }
717