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
defender-security / src / component / audit / class-user-audit.php

class-user-audit.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/component/audit/class-user-audit.php

627 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 user-related audit events.
4 *
5 * @package WP_Defender\Component\Audit
6 */
7
8 namespace WP_Defender\Component\Audit;
9
10 use WP_User;
11 use WP_Error;
12 use stdClass;
13 use WP_Defender\Traits\User;
14 use WP_Defender\Model\Audit_Log;
15
16 /**
17 * Handle user-related audit events such as login, logout, registration, and profile updates.
18 */
19 class User_Audit extends Audit_Event {
20
21 use User;
22
23 public const ACTION_LOGIN = 'login', ACTION_LOGOUT = 'logout', ACTION_REGISTERED = 'registered',
24 ACTION_LOST_PASS = 'lost_password', ACTION_RESET_PASS = 'reset_password',
25 ACTION_CREATED = 'created', ACTION_ADDED = 'added',
26 ACTION_GRANTED = 'granted', ACTION_REVOKED = 'revoked';
27
28 public const CONTEXT_SESSION = 'session', CONTEXT_USERS = 'users', CONTEXT_PROFILE = 'profile',
29 CONTEXT_MULTISITE_USERS = 'multisite_users', CONTEXT_SUPER_ADMIN = 'super-admin';
30
31 /**
32 * Cached old user data used for tracking changes to user profiles.
33 *
34 * This array stores old user data when a user's profile is updated.
35 * The data is keyed by user ID and contains the user's metadata before the update.
36 *
37 * @var array
38 */
39 private static array $cached_old_user_data = array();
40
41 /**
42 * Returns an array of hooks associated with various user actions to capture and log them.
43 *
44 * @return array
45 */
46 public function get_hooks(): array {
47
48 $hooks = array(
49 'user_profile_update_errors' => array(
50 'args' => array( 'errors', 'update', 'user' ),
51 'callback' => array( $this, 'cache_old_user_data' ),
52 ),
53 'wp_login_failed' => array(
54 'args' => array( 'username' ),
55 'text' => sprintf(
56 /* translators: 1: Blog name, 2: Username */
57 esc_html__( '%1$s User login fail. Username: %2$s', 'defender-security' ),
58 '{{blog_name}}',
59 '{{username}}'
60 ),
61 'event_type' => Audit_Log::EVENT_TYPE_USER,
62 'context' => self::CONTEXT_SESSION,
63 'action_type' => self::ACTION_LOGIN,
64 ),
65 'wp_login' => array(
66 'args' => array( 'userlogin', 'user' ),
67 'text' => sprintf(
68 /* translators: 1: Blog name, 2: Username */
69 esc_html__( '%1$s User login success: %2$s', 'defender-security' ),
70 '{{blog_name}}',
71 '{{userlogin}}'
72 ),
73 'event_type' => Audit_Log::EVENT_TYPE_USER,
74 'context' => self::CONTEXT_SESSION,
75 'action_type' => self::ACTION_LOGIN,
76 ),
77 'wpmu_2fa_login' => array(
78 'args' => array( 'user_id', '2fa_slug' ),
79 'text' => sprintf(
80 /* translators: 1: Blog name, 2: 2fa method slug, 3: Username. */
81 esc_html__( '%1$s 2fa with %2$s method login success for user: %3$s', 'defender-security' ),
82 '{{blog_name}}',
83 '{{2fa_slug}}',
84 '{{username}}'
85 ),
86 'event_type' => Audit_Log::EVENT_TYPE_USER,
87 'context' => self::CONTEXT_SESSION,
88 'action_type' => self::ACTION_LOGIN,
89 'program_args' => array(
90 'username' => array(
91 'callable' => 'get_user_by',
92 'params' => array(
93 'id',
94 '{{user_id}}',
95 ),
96 'result_property' => 'data->user_login',
97 ),
98 ),
99 ),
100 'wp_logout' => array(
101 'args' => array( 'user_id' ),
102 'text' => sprintf(
103 /* translators: 1: Blog name, 2: Username */
104 esc_html__( '%1$s User logout success: %2$s', 'defender-security' ),
105 '{{blog_name}}',
106 '{{username}}'
107 ),
108 'event_type' => Audit_Log::EVENT_TYPE_USER,
109 'action_type' => self::ACTION_LOGOUT,
110 'context' => self::CONTEXT_SESSION,
111 'program_args' => array(
112 'username' => array(
113 'callable' => 'get_user_by',
114 'params' => array(
115 'id',
116 '{{user_id}}',
117 ),
118 'result_property' => 'data->user_login',
119 ),
120 ),
121 ),
122 'user_register' => array(
123 'args' => array( 'user_id' ),
124 'event_type' => Audit_Log::EVENT_TYPE_USER,
125 'context' => self::CONTEXT_USERS,
126 'action_type' => self::ACTION_REGISTERED,
127 'callback' => array( $this, 'created_new_user_callback' ),
128 ),
129 'delete_user' => array(
130 'args' => array( 'user_id' ),
131 'text' => sprintf(
132 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: User ID, 4: Username */
133 esc_html__( '%1$s %2$s deleted a user: ID: %3$s, username: %4$s', 'defender-security' ),
134 '{{blog_name}}',
135 '{{wp_user}}',
136 '{{user_id}}',
137 '{{username}}'
138 ),
139 'context' => self::CONTEXT_USERS,
140 'action_type' => self::ACTION_DELETED,
141 'event_type' => Audit_Log::EVENT_TYPE_USER,
142 'program_args' => array(
143 'username' => array(
144 'callable' => 'get_user_by',
145 'params' => array(
146 'id',
147 '{{user_id}}',
148 ),
149 'result_property' => 'data->user_login',
150 ),
151 ),
152 ),
153 'remove_user_from_blog' => array(
154 'args' => array( 'user_id', 'blog_id' ),
155 'context' => self::CONTEXT_USERS,
156 'action_type' => self::ACTION_DELETED,
157 'event_type' => Audit_Log::EVENT_TYPE_USER,
158 'callback' => array( self::class, 'remove_user_from_blog_callback' ),
159 ),
160 'wpmu_delete_user' => array(
161 'args' => array( 'user_id' ),
162 'text' => sprintf(
163 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: User ID, 4: Username */
164 esc_html__( '%1$s %2$s deleted a user: ID: %3$s, username: %4$s', 'defender-security' ),
165 '{{blog_name}}',
166 '{{wp_user}}',
167 '{{user_id}}',
168 '{{username}}'
169 ),
170 'context' => self::CONTEXT_USERS,
171 'action_type' => self::ACTION_DELETED,
172 'event_type' => Audit_Log::EVENT_TYPE_USER,
173 'program_args' => array(
174 'username' => array(
175 'callable' => 'get_user_by',
176 'params' => array(
177 'id',
178 '{{user_id}}',
179 ),
180 'result_property' => 'data->user_login',
181 ),
182 ),
183 ),
184 'profile_update' => array(
185 'args' => array( 'user_id', 'old_user_data', 'userdata' ),
186 'action_type' => self::ACTION_UPDATED,
187 'event_type' => Audit_Log::EVENT_TYPE_USER,
188 'context' => self::CONTEXT_PROFILE,
189 'callback' => array( self::class, 'profile_update_callback' ),
190 ),
191 'retrieve_password' => array(
192 'args' => array( 'username' ),
193 'text' => sprintf(
194 /* translators: 1: Blog name, 2: Username */
195 esc_html__( '%1$s Password requested to reset for user: %2$s', 'defender-security' ),
196 '{{blog_name}}',
197 '{{username}}'
198 ),
199 'action_type' => self::ACTION_LOST_PASS,
200 'event_type' => Audit_Log::EVENT_TYPE_USER,
201 'context' => self::CONTEXT_PROFILE,
202 'program_args' => array(
203 'user' => array(
204 'callable' => 'get_user_by',
205 'params' => array(
206 'login',
207 '{{username}}',
208 ),
209 ),
210 ),
211 ),
212 'after_password_reset' => array(
213 'args' => array( 'user' ),
214 'text' => sprintf(
215 /* translators: 1: Blog name, 2: Username. */
216 esc_html__( '%1$s Password reset for user: %2$s', 'defender-security' ),
217 '{{blog_name}}',
218 '{{user_login}}'
219 ),
220 'event_type' => Audit_Log::EVENT_TYPE_USER,
221 'action_type' => self::ACTION_RESET_PASS,
222 'context' => self::CONTEXT_PROFILE,
223 'custom_args' => array(
224 'user_login' => '{{user->data->user_login}}',
225 ),
226 ),
227 'set_user_role' => array(
228 'args' => array( 'user_ID', 'new_role', 'old_role' ),
229 'text' => sprintf(
230 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Username, 4: Old user role, 5: New user role */
231 esc_html__( "%1\$s %2\$s changed user %3\$s's role from %4\$s to %5\$s", 'defender-security' ),
232 '{{blog_name}}',
233 '{{wp_user}}',
234 '{{username}}',
235 '{{from_role}}',
236 '{{new_role}}'
237 ),
238 'action_type' => self::ACTION_UPDATED,
239 'event_type' => Audit_Log::EVENT_TYPE_USER,
240 'context' => self::CONTEXT_PROFILE,
241 'custom_args' => array(
242 'from_role' => '{{old_role->0}}',
243 ),
244 'program_args' => array(
245 'username' => array(
246 'callable' => 'get_user_by',
247 'params' => array(
248 'id',
249 '{{user_ID}}',
250 ),
251 'result_property' => 'data->user_login',
252 ),
253 ),
254 'false_when' => array(
255 array(
256 '{{old_role}}',
257 array(),
258 '==',
259 ),
260 ),
261 ),
262 'wpdef_session_lock' => array(
263 'args' => array( 'user_id', 'session_lock_type' ),
264 'text' => sprintf(
265 /* translators: 1: Blog name, 2: Username, 3: Session lock type. */
266 esc_html__( '%1$s User session ended for %2$s due to a change in %3$s', 'defender-security' ),
267 '{{blog_name}}',
268 '{{username}}',
269 '{{session_lock_type}}'
270 ),
271 'event_type' => Audit_Log::EVENT_TYPE_USER,
272 'context' => self::CONTEXT_SESSION,
273 'action_type' => self::ACTION_LOGIN,
274 'program_args' => array(
275 'username' => array(
276 'callable' => 'get_user_by',
277 'params' => array(
278 'id',
279 '{{user_id}}',
280 ),
281 'result_property' => 'data->user_login',
282 ),
283 ),
284 ),
285 'wpdef_session_timeout' => array(
286 'args' => array( 'user_id' ),
287 'text' => sprintf(
288 /* translators: 1: Blog name, 2: Username. */
289 esc_html__( '%1$s User session ended for %2$s due to an idle session', 'defender-security' ),
290 '{{blog_name}}',
291 '{{username}}'
292 ),
293 'event_type' => Audit_Log::EVENT_TYPE_USER,
294 'context' => self::CONTEXT_SESSION,
295 'action_type' => self::ACTION_LOGIN,
296 'program_args' => array(
297 'username' => array(
298 'callable' => 'get_user_by',
299 'params' => array(
300 'id',
301 '{{user_id}}',
302 ),
303 'result_property' => 'data->user_login',
304 ),
305 ),
306 ),
307 // Since 5.6.0.
308 'wpmudev_sso_set_current_user' => array(
309 'args' => array( 'user_id' ),
310 'text' => sprintf(
311 /* translators: 1: Blog name, 2: Username. */
312 esc_html__( '%1$s Hub SSO login success: %2$s', 'defender-security' ),
313 '{{blog_name}}',
314 '{{username}}'
315 ),
316 'event_type' => Audit_Log::EVENT_TYPE_USER,
317 'context' => self::CONTEXT_SESSION,
318 'action_type' => self::ACTION_LOGIN,
319 'program_args' => array(
320 'username' => array(
321 'callable' => 'get_user_by',
322 'params' => array(
323 'id',
324 '{{user_id}}',
325 ),
326 'result_property' => 'data->user_login',
327 ),
328 ),
329 ),
330 );
331
332 if ( is_multisite() ) {
333 $hooks['granted_super_admin'] = array(
334 'args' => array( 'user_id' ),
335 'text' => sprintf(
336 /* translators: 1: Blog name. 2: Actor username. 3: Affected username. */
337 esc_html__( '%1$s %2$s granted Super Admin privileges to %3$s', 'defender-security' ),
338 '{{blog_name}}',
339 '{{wp_user}}',
340 '{{username}}'
341 ),
342 'event_type' => Audit_Log::EVENT_TYPE_USER,
343 'context' => self::CONTEXT_SUPER_ADMIN,
344 'action_type' => self::ACTION_GRANTED,
345 'program_args' => array(
346 'username' => array(
347 'callable' => 'get_user_by',
348 'params' => array( 'id', '{{user_id}}' ),
349 'result_property' => 'data->user_login',
350 ),
351 ),
352 );
353 $hooks['revoked_super_admin'] = array(
354 'args' => array( 'user_id' ),
355 'text' => sprintf(
356 /* translators: 1: Blog name. 2: Actor username. 3: Affected username. */
357 esc_html__( '%1$s %2$s revoked Super Admin privileges from %3$s', 'defender-security' ),
358 '{{blog_name}}',
359 '{{wp_user}}',
360 '{{username}}'
361 ),
362 'event_type' => Audit_Log::EVENT_TYPE_USER,
363 'context' => self::CONTEXT_SUPER_ADMIN,
364 'action_type' => self::ACTION_REVOKED,
365 'program_args' => array(
366 'username' => array(
367 'callable' => 'get_user_by',
368 'params' => array( 'id', '{{user_id}}' ),
369 'result_property' => 'data->user_login',
370 ),
371 ),
372 );
373 $hooks['wpmu_new_user'] = array(
374 'args' => array( 'user_id' ),
375 'event_type' => Audit_Log::EVENT_TYPE_USER,
376 'context' => self::CONTEXT_MULTISITE_USERS,
377 'action_type' => self::ACTION_CREATED,
378 'callback' => array( $this, 'created_new_user_callback' ),
379 );
380 $hooks['add_user_to_blog'] = array(
381 'args' => array( 'user_id', 'role', 'blog_id' ),
382 'text' => sprintf(
383 /* translators: 1: Blog name, 2: Username, 3: User role. */
384 esc_html__( 'User added to %1$s, Username: %2$s, Role: %3$s', 'defender-security' ),
385 '{{site_name}}',
386 '{{username}}',
387 '{{role}}'
388 ),
389 'event_type' => Audit_Log::EVENT_TYPE_USER,
390 'context' => self::CONTEXT_MULTISITE_USERS,
391 'action_type' => self::ACTION_ADDED,
392 'program_args' => array(
393 'site_name' => array(
394 'callable' => 'get_blog_details',
395 'params' => array( '{{blog_id}}' ),
396 'result_property' => 'blogname',
397 ),
398 'username' => array(
399 'callable' => 'get_user_by',
400 'params' => array( 'id', '{{user_id}}' ),
401 'result_property' => 'data->user_login',
402 ),
403 ),
404 );
405 }
406
407 return $hooks;
408 }
409
410 /**
411 * Log when user is created on MU.
412 *
413 * @param int $user_id User ID.
414 *
415 * @return bool|array
416 */
417 public function created_new_user_callback( int $user_id ): bool|array {
418 $user = get_user_by( 'id', $user_id );
419 $username = $user->user_login ?? '';
420 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : 'the site';
421 $userdata_role = defender_get_data_from_request( 'role', 'p' );
422
423 if ( ! isset( $user->roles[0], $userdata_role ) || $userdata_role !== $user->roles[0] ) {
424 return false;
425 }
426 $role = $this->get_first_user_role( $user );
427 $action = defender_get_data_from_request( 'action', 'p' );
428 if ( is_admin() ) {
429 return array(
430 sprintf(
431 /* translators: 1: Blog name, 2: Username, 3: User role. */
432 esc_html__( 'User %1$s %2$s, Username: %3$s, Role: %4$s', 'defender-security' ),
433 'created' === $action ? 'created on' : 'added to',
434 $blog_name,
435 $username,
436 $role
437 ),
438 self::ACTION_CREATED,
439 );
440 } else {
441 return array(
442 sprintf(
443 /* translators: 1: Blog name, 2: Username, 3: User role */
444 esc_html__( '%1$s A new user registered: Username: %2$s, Role: %3$s', 'defender-security' ),
445 $blog_name,
446 $username,
447 $role
448 ),
449 );
450 }
451 }
452
453 /**
454 * Log when user is removed from a blog.
455 *
456 * @return bool|array
457 */
458 public function remove_user_from_blog_callback() {
459 $action = defender_get_data_from_request( 'action', 'p' );
460 if ( 'doremove' !== $action ) {
461 return false;
462 }
463
464 $args = func_get_args();
465 $user_id = $args[1]['user_id'];
466 $blog_id = $args[1]['blog_id'];
467 $user = get_user_by( 'id', $user_id );
468 $username = $user->user_login ?? '';
469 $current_user_display = $this->get_user_display( get_current_user_id() );
470 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
471
472 return array(
473 sprintf(
474 /* translators: 1: Blog name, 2: User's display name, 3: User ID, 4: Username, 5: Blog ID */
475 esc_html__( '%1$s %2$s removed a user: ID: %3$s, username: %4$s from blog %5$s', 'defender-security' ),
476 $blog_name,
477 $current_user_display,
478 $user_id,
479 $username,
480 $blog_id
481 ),
482 self::ACTION_DELETED,
483 );
484 }
485
486 /**
487 * Callback for logging when a user's profile is updated. It distinguishes between self-updates and updates made by
488 * other users.
489 *
490 * @param int $user_id User ID.
491 * @param WP_User $old_user_data Object containing user's data prior to update.
492 * @param array $userdata The raw array of data passed to wp_insert_user().
493 *
494 * @return bool|array
495 */
496 public function profile_update_callback( int $user_id, WP_User $old_user_data, array $userdata ): bool|array {
497 $from = defender_get_data_from_request( 'from', 'p' );
498 if ( 'profile' !== $from ) {
499 return false;
500 }
501 $diff_assoc = array_diff_assoc( $userdata, self::$cached_old_user_data[ $user_id ] ?? array() );
502 if ( array() === $diff_assoc ) {
503 return false;
504 }
505 $current_user = get_user_by( 'id', $user_id );
506 if ( array_key_exists( 'user_pass', $diff_assoc ) && $old_user_data->user_pass !== $userdata['user_pass'] ) {
507 $password_audit = new Password_Audit();
508 return $password_audit->build_log( $current_user );
509 }
510 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
511 $current_user_id = get_current_user_id();
512
513 if ( $current_user_id === $user_id ) {
514
515 return array(
516 sprintf(
517 /* translators: 1: Blog name, 2: User's nicename */
518 esc_html__( '%1$s User %2$s updated his/her profile', 'defender-security' ),
519 $blog_name,
520 $current_user->user_nicename
521 ),
522 self::ACTION_UPDATED,
523 );
524 } elseif ( 0 !== $current_user_id ) {
525
526 return array(
527 sprintf(
528 /* translators: 1: Blog name, 2: User's display name, 3: User's nicename */
529 esc_html__( "%1\$s %2\$s updated user %3\$s's profile information", 'defender-security' ),
530 $blog_name,
531 $this->get_user_display( $current_user_id ),
532 $current_user->user_nicename
533 ),
534 self::ACTION_UPDATED,
535 );
536 }
537
538 return false;
539 }
540
541 /**
542 * Provides a dictionary for translating action constants into human-readable strings.
543 *
544 * @return array
545 */
546 public function dictionary(): array {
547 return array(
548 self::ACTION_LOST_PASS => esc_html__( 'lost password', 'defender-security' ),
549 self::ACTION_REGISTERED => esc_html__( 'registered', 'defender-security' ),
550 self::ACTION_LOGIN => esc_html__( 'login', 'defender-security' ),
551 self::ACTION_LOGOUT => esc_html__( 'logout', 'defender-security' ),
552 self::ACTION_RESET_PASS => esc_html__( 'password reset', 'defender-security' ),
553 self::ACTION_CREATED => esc_html__( 'created', 'defender-security' ),
554 self::ACTION_ADDED => esc_html__( 'added', 'defender-security' ),
555 self::ACTION_GRANTED => esc_html__( 'granted', 'defender-security' ),
556 self::ACTION_REVOKED => esc_html__( 'revoked', 'defender-security' ),
557 );
558 }
559
560 /**
561 * Static method to retrieve the role of a user by their ID
562 *
563 * @param int $user_id The ID of the user whose role is to be fetched.
564 *
565 * @return string
566 */
567 public static function get_user_role( $user_id ): string {
568 $user = get_user_by( 'id', $user_id );
569 if ( $user instanceof WP_User ) {
570 $_this = new self();
571
572 return $_this->get_first_user_role( $user );
573 } else {
574 return '';
575 }
576 }
577
578 /**
579 * Check if it is a create new user request.
580 *
581 * @since 2.8.0
582 * @return bool
583 */
584 public static function is_create_user_action(): bool {
585 $action = defender_get_data_from_request( 'action', 'p' );
586 if ( 'createuser' === $action ) {
587 return true;
588 }
589
590 return false;
591 }
592
593 /**
594 * Cache old user data from when a user's profile is updated.
595 *
596 * @param WP_Error $errors WP_Error object (passed by reference).
597 * @param bool $update Whether this is a user update.
598 * @param stdClass $user User object (passed by reference).
599 *
600 * @return WP_Error The errors that occurred during the action.
601 */
602 public function cache_old_user_data( WP_Error $errors, bool $update, stdClass $user ): WP_Error {
603 if ( $update ) {
604 $id = $user->ID;
605 $current_user = get_userdata( $id );
606 $user_array = $current_user->to_array();
607 $meta = array(
608 'first_name' => get_user_meta( $current_user->ID, 'first_name', true ),
609 'last_name' => get_user_meta( $current_user->ID, 'last_name', true ),
610 'nickname' => get_user_meta( $current_user->ID, 'nickname', true ),
611 'description' => get_user_meta( $current_user->ID, 'description', true ),
612 'rich_editing' => get_user_meta( $current_user->ID, 'rich_editing', true ),
613 'syntax_highlighting' => get_user_meta( $current_user->ID, 'syntax_highlighting', true ),
614 'comment_shortcuts' => get_user_meta( $current_user->ID, 'comment_shortcuts', true ) === 'true' ? 'true' : '',
615 'locale' => get_user_meta( $current_user->ID, 'locale', true ),
616 'admin_color' => get_user_meta( $current_user->ID, 'admin_color', true ),
617 'use_ssl' => get_user_meta( $current_user->ID, 'use_ssl', true ),
618 'show_admin_bar_front' => get_user_meta( $current_user->ID, 'show_admin_bar_front', true ),
619 );
620
621 self::$cached_old_user_data[ $id ] = array_merge_recursive( $user_array, $meta );
622 }
623
624 return $errors;
625 }
626 }
627