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

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

582 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare( strict_types=1 );
4
5 namespace WP_Defender\Model;
6
7 use Calotes\Base\Model;
8 use WP_Defender\DB;
9 use WP_Defender\Model\Setting\User_Agent_Lockout;
10 use WP_Defender\Traits\Formats;
11 use WP_Defender\Component\Table_Lockout;
12
13 class Lockout_Log extends DB {
14 use Formats;
15
16 public const AUTH_FAIL = 'auth_fail';
17 public const AUTH_LOCK = 'auth_lock';
18
19 public const ERROR_404 = '404_error';
20 public const LOCKOUT_404 = '404_lockout';
21 public const ERROR_404_IGNORE = '404_error_ignore';
22
23 public const LOCKOUT_UA = 'ua_lockout';
24
25 public const INFINITE_SCROLL_SIZE = 50;
26
27 protected $table = 'defender_lockout_log';
28
29 /**
30 * @var int
31 * @defender_property
32 */
33 public $id;
34 /**
35 * @var string
36 * @defender_property
37 */
38 public $log;
39 /**
40 * @var string
41 * @defender_property
42 */
43 public $ip;
44 /**
45 * @var int
46 * @defender_property
47 */
48 public $date;
49 /**
50 * @var string
51 * @defender_property
52 */
53 public $user_agent;
54 /**
55 * @var string
56 * @defender_property
57 */
58 public $type;
59 /**
60 * @var int
61 * @defender_property
62 */
63 public $blog_id;
64 /**
65 * @var string
66 * @defender_property
67 */
68 public $tried;
69 /**
70 * @var string
71 * @defender_property
72 */
73 public $country_iso_code;
74
75 /**
76 * Pulling the logs as data, use in Logs tab
77 * $filters will have those params
78 * -date_from
79 * -date_to
80 * == Defaults is 7 days and always require
81 * -type: optional
82 * -ip: optional.
83 *
84 * @param array $filters
85 * @param int $paged
86 * @param string $order_by
87 * @param string $order
88 * @param int $page_size
89 *
90 * @return Lockout_Log[]
91 */
92 public static function query_logs(
93 $filters = [],
94 $paged = 1,
95 $order_by = 'id',
96 $order = 'desc',
97 $page_size = 50
98 ): array {
99 $orm = self::get_orm();
100 $orm->get_repository( self::class )
101 ->where(
102 'date',
103 'between',
104 [
105 $filters['from'],
106 $filters['to'],
107 ]
108 );
109
110 if ( isset( $filters['ip'] ) && ! empty( $filters['ip'] ) ) {
111 $orm->where( 'ip', 'like', '%' . $filters['ip'] . '%' );
112 }
113 if ( isset( $filters['type'] ) && ! empty( $filters['type'] ) ) {
114 $orm->where( 'type', $filters['type'] );
115 }
116
117 if ( ! empty( $filters['ban_status'] ) ) {
118 $ban_status_where = self::ban_status_where( $filters['ban_status'] );
119
120 if ( 3 === count( $ban_status_where ) ) {
121 $orm->where( ... $ban_status_where );
122 }
123 }
124
125 if ( ! empty( $order_by ) && ! empty( $order ) ) {
126 $orm->order_by( $order_by, $order );
127 }
128
129 if ( -1 === (int) $page_size ) {
130 $page_size = self::INFINITE_SCROLL_SIZE;
131 }
132
133 if ( false !== $page_size ) {
134 $offset = ( $paged - 1 ) * $page_size;
135 $orm->limit( "$offset,$page_size" );
136 }
137
138 return $orm->get();
139 }
140
141 /**
142 * This similar to @query_logs, but we count the total row.
143 *
144 * @param $date_from
145 * @param $date_to
146 * @param string $type
147 * @param string $ip
148 * @param array $filters Array consists key value pair to pass in SQL where condition.
149 *
150 * @return string|null
151 */
152 public static function count( $date_from, $date_to, $type = '', $ip = '', $filters = [] ): ?string {
153 $orm = self::get_orm();
154 $orm->get_repository( self::class )
155 ->where(
156 'date',
157 'between',
158 [
159 $date_from,
160 $date_to,
161 ]
162 );
163
164 if ( ! empty( $type ) ) {
165 if ( is_array( $type ) ) {
166 $orm->where( 'type', 'in', $type );
167 } else {
168 $orm->where( 'type', $type );
169 }
170 }
171
172 if ( ! empty( $ip ) ) {
173 $orm->where( 'ip', 'like', "%$ip%" );
174 }
175
176 if ( ! empty( $filters['ban_status'] ) ) {
177 $ban_status_where = self::ban_status_where( $filters['ban_status'] );
178
179 if ( 3 === count( $ban_status_where ) ) {
180 $orm->where( ... $ban_status_where );
181 }
182 }
183
184 return $orm->count();
185 }
186
187 /**
188 * Count login lockout in the last 7 days.
189 *
190 * @return string|null
191 */
192 public static function count_login_lockout_last_7_days(): ?string {
193 $start = strtotime( '-7 days' );
194 $end = time();
195
196 return self::count( $start, $end, self::AUTH_LOCK );
197 }
198
199 /**
200 * Count 404 lockout in the last 7 days.
201 *
202 * @return string|null
203 */
204 public static function count_404_lockout_last_7_days(): ?string {
205 $start = strtotime( '-7 days' );
206 $end = time();
207
208 return self::count( $start, $end, self::LOCKOUT_404 );
209 }
210
211 /**
212 * Count UA lockout in the last 7 days.
213 *
214 * @return string|null
215 */
216 public static function count_ua_lockout_last_7_days(): ?string {
217 $start = strtotime( '-7 days' );
218 $end = time();
219
220 return self::count( $start, $end, self::LOCKOUT_UA );
221 }
222
223 /**
224 * A shortcut for quickly count lockout in last 24 hours.
225 *
226 * @return string|null
227 */
228 public static function count_lockout_in_24_hours(): ?string {
229 $start = strtotime( '-24 hours' );
230 $end = time();
231
232 return self::count(
233 $start,
234 $end,
235 [
236 self::AUTH_LOCK,
237 self::LOCKOUT_404,
238 self::LOCKOUT_UA,
239 ]
240 );
241 }
242
243 /**
244 * A shortcut for quickly count lockout in last 24 hours.
245 *
246 * @return string|null
247 */
248 public static function count_lockout_in_7_days(): ?string {
249 $start = strtotime( '-7 days' );
250 $end = time();
251
252 return self::count(
253 $start,
254 $end,
255 [
256 self::AUTH_LOCK,
257 self::LOCKOUT_404,
258 self::LOCKOUT_UA,
259 ]
260 );
261 }
262
263 /**
264 * A shortcut for count lockout in 30 days.
265 *
266 * @return string|null
267 */
268 public static function count_lockout_in_30_days(): ?string {
269 $start = strtotime( '-30 days' );
270 $end = time();
271
272 return self::count(
273 $start,
274 $end,
275 [
276 self::AUTH_LOCK,
277 self::LOCKOUT_404,
278 self::LOCKOUT_UA,
279 ]
280 );
281 }
282
283 /**
284 * Get the last time a lockout happened.
285 *
286 * @param false $for_hub
287 *
288 * @return false|string
289 */
290 public static function get_last_lockout_date( $for_hub = false ) {
291 $data = self::query_logs(
292 [
293 'from' => strtotime( '-30 days' ),
294 'to' => time(),
295 ],
296 1,
297 'id',
298 'desc',
299 1
300 );
301 $last = array_shift( $data );
302 if ( ! is_object( $last ) ) {
303 return 'n/a';
304 }
305
306 return $for_hub
307 ? $last->persistent_hub_datetime_format( $last->date )
308 : $last->format_date_time( $last->date );
309 }
310
311 /**
312 * Remove all data.
313 *
314 * @return bool|int
315 */
316 public static function truncate() {
317 $orm = self::get_orm();
318
319 return $orm->get_repository( self::class )
320 ->truncate();
321 }
322
323 /**
324 * Remove data by time period.
325 *
326 * @param int $timestamp
327 * @param int $limit
328 *
329 * @return void
330 */
331 public static function remove_logs( $timestamp, $limit ) {
332 $orm = self::get_orm();
333 $orm->get_repository( self::class )
334 ->where( 'date', '<=', $timestamp )
335 ->order_by( 'id' )
336 ->limit( $limit )
337 ->delete_by_limit();
338 }
339
340 /**
341 * Get log summary.
342 *
343 * @return array
344 */
345 public static function get_summary(): array {
346 // Time.
347 $current_time = current_time( 'timestamp' );
348 $today_midnight = strtotime( '-24 hours', $current_time );
349 $first_this_week = strtotime( '-7 days', $current_time );
350
351 // Prepare columns
352 $select = [
353 'MAX(date) as lockout_last',
354 'COUNT(*) as lockout_this_month',
355 // 24 hours
356 "COUNT(IF(date > {$today_midnight}, 1, NULL)) as lockout_today",
357 "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_404 . "', 1, NULL)) as lockout_404_today",
358 "COUNT(IF(date > {$today_midnight} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_today",
359 "COUNT(IF(date > {$today_midnight} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_today",
360 // 7 days
361 "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_404 . "', 1, NULL)) as lockout_404_this_week",
362 "COUNT(IF(date > {$first_this_week} AND type = '" . self::AUTH_LOCK . "', 1, NULL)) as lockout_login_this_week",
363 "COUNT(IF(date > {$first_this_week} AND type = '" . self::LOCKOUT_UA . "', 1, NULL)) as lockout_ua_this_week",
364 ];
365 $select = implode( ',', $select );
366
367 $orm = self::get_orm();
368 $result = $orm->get_repository( self::class )
369 ->select( $select )
370 ->where( 'type', 'in', [ self::LOCKOUT_404, self::AUTH_LOCK, self::LOCKOUT_UA ] )
371 ->where( 'date', '>=', strtotime( '-30 days', $current_time ) )
372 ->get_results();
373
374 return $result[0] ?? [];
375 }
376
377 /**
378 * @param string $type
379 *
380 * @return string
381 */
382 protected static function get_log_tag( $type ): string {
383 switch ( $type ) {
384 case self::LOCKOUT_404:
385 case self::ERROR_404:
386 case self::ERROR_404_IGNORE:
387 $tag = '404';
388 break;
389 case self::AUTH_FAIL:
390 case self::AUTH_LOCK:
391 $tag = 'login';
392 break;
393 case self::LOCKOUT_UA:
394 default:
395 $tag = 'bots';
396 break;
397 }
398
399 return $tag;
400 }
401
402 /**
403 * @param string $type
404 *
405 * @return string
406 */
407 protected static function get_log_tag_class( $type ): string {
408 switch ( $type ) {
409 case self::AUTH_LOCK:
410 case self::LOCKOUT_404:
411 case self::LOCKOUT_UA:
412 $badge_bg = 'bg-badge-red';
413 break;
414 case self::AUTH_FAIL:
415 case self::ERROR_404:
416 case self::ERROR_404_IGNORE:
417 default:
418 $badge_bg = 'bg-badge-green';
419 break;
420 }
421
422 return $badge_bg;
423 }
424
425 /**
426 * @param string $type
427 *
428 * @return string
429 */
430 protected static function get_log_container_class( $type ): string {
431 switch ( $type ) {
432 case self::AUTH_LOCK:
433 case self::LOCKOUT_404:
434 case self::LOCKOUT_UA:
435 $class = 'sui-error';
436 break;
437 case self::AUTH_FAIL:
438 case self::ERROR_404:
439 case self::ERROR_404_IGNORE:
440 default:
441 $class = 'sui-warning';
442 break;
443 }
444
445 return $class;
446 }
447
448 /**
449 * Get data from db and format it for ready to use on frontend.
450 *
451 * @param array $filters
452 * @param int $paged
453 * @param string $order_by
454 * @param string $order
455 * @param int $page_size
456 *
457 * @return array
458 */
459 public static function get_logs_and_format(
460 $filters = [],
461 $paged = 1,
462 $order_by = 'id',
463 $order = 'desc',
464 $page_size = 50
465 ): array {
466 $logs = self::query_logs( $filters, $paged, $order_by, $order, $page_size );
467 $data = [];
468 $ua_model = wd_di()->get( User_Agent_Lockout::class );
469 foreach ( $logs as $item ) {
470 $ip_model = Lockout_Ip::get( $item->ip );
471
472 // Escape object properties received from end user.
473 $item->log = sanitize_textarea_field( $item->log );
474 $item->tried = sanitize_textarea_field( $item->tried );
475
476 $log = $item->export();
477
478 // Escape array keys received from end user.
479 $log['log'] = sanitize_textarea_field( $log['log'] );
480 $log['tried'] = sanitize_textarea_field( $log['tried'] );
481
482 $log['date'] = $item->format_date_time( $item->date );
483 $log['format_date'] = $item->get_date( $item->date );
484 $log['tag'] = self::get_log_tag( $item->type );
485 $log['tag_class'] = self::get_log_tag_class( $item->type );
486 $log['container_class'] = self::get_log_container_class( $item->type );
487 if ( self::LOCKOUT_UA === $item->type ) {
488 if ( \WP_Defender\Component\User_Agent::REASON_BAD_POST === $item->tried ) {
489 $log['description'] = __( '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.', 'wpdef' );
490 $log['type_label'] = __( 'Type', 'wpdef' );
491 $log['type_value'] = __( 'Empty Headers', 'wpdef' );
492 $arr_statuses = $ip_model->get_access_status();
493 } else {
494 $log['description'] = sprintf(
495 /* translators: %s: log, %d: user_agent */
496 __( '%1$s: <strong>%2$s</strong>. This user agent is considered bad bots and may harm your site.', 'wpdef' ),
497 sanitize_textarea_field( $item->log ),
498 sanitize_textarea_field( $item->user_agent )
499 );
500 $log['type_label'] = __( 'User Agent name', 'wpdef' );
501 $log['type_value'] = sanitize_textarea_field( $item->user_agent );
502 $arr_statuses = $ua_model->get_access_status( $item->user_agent );
503 }
504 } else {
505 $log['description'] = sanitize_textarea_field( $item->log );
506 $log['type_label'] = __( 'Type', 'wpdef' );
507 $log['type_value'] = str_replace( '_', ' ', $item->type );
508 $arr_statuses = $ip_model->get_access_status();
509 }
510 // There may be several statuses.
511 $log['access_status'] = $arr_statuses;
512 $log['access_status_text'] = $ip_model->get_access_status_text( $arr_statuses[0] );
513 $data[] = $log;
514 }
515
516 return $data;
517 }
518
519 /**
520 * Get the first log by ID.
521 *
522 * @param int $id
523 *
524 * @return null|Model
525 */
526 public static function find_by_id( $id ): ?Model {
527 $orm = self::get_orm();
528
529 return $orm->get_repository( self::class )
530 ->where( 'id', $id )
531 ->first();
532 }
533
534 /**
535 * Delete current log.
536 */
537 public function delete() {
538 $orm = self::get_orm();
539 $orm->get_repository( self::class )->delete(
540 [
541 'id' => $this->id,
542 ]
543 );
544 }
545
546 /**
547 * Prepare user-agent where condition based on the ban status variant.
548 *
549 * @param string $ban_status_type Ban status type.
550 *
551 * @return array Where condition arguments or empty array.
552 */
553 private static function ban_status_where( $ban_status_type ): array {
554 $table_lockout = wd_di()->get( Table_Lockout::class );
555
556 if ( $table_lockout::STATUS_NOT_BAN === $ban_status_type ) {
557 $ua_model = wd_di()->get( User_Agent_Lockout::class );
558
559 $blocklist = $ua_model->get_lockout_list( 'blocklist' );
560 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
561
562 $all = array_merge( $blocklist, $allowlist );
563
564 return [ 'user_agent', 'not regexp', implode( '|', $all ) ];
565 } elseif ( $table_lockout::STATUS_BAN === $ban_status_type ) {
566 $ua_model = wd_di()->get( User_Agent_Lockout::class );
567
568 $blocklist = $ua_model->get_lockout_list( 'blocklist' );
569
570 return [ 'user_agent', 'regexp', implode( '|', $blocklist ) ];
571 } elseif ( $table_lockout::STATUS_ALLOWLIST === $ban_status_type ) {
572 $ua_model = wd_di()->get( User_Agent_Lockout::class );
573
574 $allowlist = $ua_model->get_lockout_list( 'allowlist' );
575
576 return [ 'user_agent', 'regexp', implode( '|', $allowlist ) ];
577 }
578
579 return [];
580 }
581 }
582