Changeset 3492106
- Timestamp:
- 03/26/2026 07:29:16 PM (2 days ago)
- Location:
- vigilante
- Files:
-
- 49 added
- 7 edited
-
tags/1.8.0 (added)
-
tags/1.8.0/admin (added)
-
tags/1.8.0/admin/class-admin-ajax.php (added)
-
tags/1.8.0/admin/class-admin.php (added)
-
tags/1.8.0/assets (added)
-
tags/1.8.0/assets/css (added)
-
tags/1.8.0/assets/css/admin.css (added)
-
tags/1.8.0/assets/css/two-factor-admin.css (added)
-
tags/1.8.0/assets/css/two-factor-login.css (added)
-
tags/1.8.0/assets/css/under-attack-challenge.css (added)
-
tags/1.8.0/assets/images (added)
-
tags/1.8.0/assets/images/icon.png (added)
-
tags/1.8.0/assets/js (added)
-
tags/1.8.0/assets/js/admin.js (added)
-
tags/1.8.0/assets/js/qrcode.min.js (added)
-
tags/1.8.0/assets/js/two-factor-admin.js (added)
-
tags/1.8.0/assets/js/under-attack-challenge.js (added)
-
tags/1.8.0/includes (added)
-
tags/1.8.0/includes/class-activator.php (added)
-
tags/1.8.0/includes/class-activity-log.php (added)
-
tags/1.8.0/includes/class-ayudawp-promo-banner.php (added)
-
tags/1.8.0/includes/class-backup-manager.php (added)
-
tags/1.8.0/includes/class-comment-security.php (added)
-
tags/1.8.0/includes/class-database-backup.php (added)
-
tags/1.8.0/includes/class-database-prefix.php (added)
-
tags/1.8.0/includes/class-database.php (added)
-
tags/1.8.0/includes/class-deactivator.php (added)
-
tags/1.8.0/includes/class-email-template.php (added)
-
tags/1.8.0/includes/class-feed-manager.php (added)
-
tags/1.8.0/includes/class-file-integrity.php (added)
-
tags/1.8.0/includes/class-firewall.php (added)
-
tags/1.8.0/includes/class-head-cleaner.php (added)
-
tags/1.8.0/includes/class-htaccess-manager.php (added)
-
tags/1.8.0/includes/class-htaccess-protection.php (added)
-
tags/1.8.0/includes/class-https-enforcer.php (added)
-
tags/1.8.0/includes/class-login-security.php (added)
-
tags/1.8.0/includes/class-qr-svg.php (added)
-
tags/1.8.0/includes/class-rest-api-security.php (added)
-
tags/1.8.0/includes/class-security-headers.php (added)
-
tags/1.8.0/includes/class-settings.php (added)
-
tags/1.8.0/includes/class-two-factor-email.php (added)
-
tags/1.8.0/includes/class-two-factor-totp.php (added)
-
tags/1.8.0/includes/class-under-attack.php (added)
-
tags/1.8.0/includes/class-user-security.php (added)
-
tags/1.8.0/includes/class-wpconfig-security.php (added)
-
tags/1.8.0/includes/scan-patterns.json (added)
-
tags/1.8.0/readme.txt (added)
-
tags/1.8.0/uninstall.php (added)
-
tags/1.8.0/vigilante.php (added)
-
trunk/admin/class-admin-ajax.php (modified) (1 diff)
-
trunk/admin/class-admin.php (modified) (3 diffs)
-
trunk/assets/js/admin.js (modified) (2 diffs)
-
trunk/includes/class-login-security.php (modified) (1 diff)
-
trunk/includes/class-user-security.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/vigilante.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
vigilante/trunk/admin/class-admin-ajax.php
r3489456 r3492106 1229 1229 1230 1230 /** 1231 * AJAX: Force password reset by role 1232 * Resets passwords for all users with the selected roles 1233 */ 1234 public function ajax_force_password_reset_by_role() { 1235 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); 1236 1237 if ( ! current_user_can( 'manage_options' ) ) { 1238 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); 1239 } 1240 1241 $roles = isset( $_POST['roles'] ) ? array_map( 'sanitize_key', (array) $_POST['roles'] ) : array(); 1242 1243 if ( empty( $roles ) ) { 1244 wp_send_json_error( __( 'No roles selected.', 'vigilante' ) ); 1245 } 1246 1247 // Validate that submitted roles actually exist. 1248 $wp_roles = wp_roles(); 1249 foreach ( $roles as $role ) { 1250 if ( ! isset( $wp_roles->roles[ $role ] ) ) { 1251 wp_send_json_error( 1252 sprintf( 1253 /* translators: %s: Role slug */ 1254 __( 'Invalid role: %s', 'vigilante' ), 1255 $role 1256 ) 1257 ); 1258 } 1259 } 1260 1261 $include_self = ! empty( $_POST['include_self'] ); 1262 $current_user_id = get_current_user_id(); 1263 1264 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log ); 1265 1266 $results = $user_security->force_password_reset_by_roles( 1267 $roles, 1268 $current_user_id, 1269 ! $include_self 1270 ); 1271 1272 // Log the action. 1273 if ( $this->activity_log ) { 1274 $reset_by_user = get_userdata( $current_user_id ); 1275 $role_names = array(); 1276 1277 foreach ( $roles as $role ) { 1278 $role_names[] = isset( $wp_roles->roles[ $role ] ) 1279 ? translate_user_role( $wp_roles->roles[ $role ]['name'] ) 1280 : $role; 1281 } 1282 1283 $this->activity_log->log( 1284 'user', 1285 'force_password_reset_by_role', 1286 sprintf( 1287 /* translators: 1: Number of users, 2: Role names, 3: Admin username */ 1288 __( 'Password reset forced for %1$d users (roles: %2$s) by %3$s', 'vigilante' ), 1289 $results['success'], 1290 implode( ', ', $role_names ), 1291 $reset_by_user ? $reset_by_user->user_login : __( 'System', 'vigilante' ) 1292 ), 1293 array( 1294 'count' => $results['success'], 1295 'roles' => $roles, 1296 'reset_by' => $current_user_id, 1297 'include_self' => $include_self, 1298 ), 1299 'warning' 1300 ); 1301 } 1302 1303 $message = sprintf( 1304 /* translators: %d: Number of users */ 1305 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ), 1306 $results['success'] 1307 ); 1308 1309 if ( $results['failed'] > 0 ) { 1310 $message .= ' ' . sprintf( 1311 /* translators: %d: Number of failures */ 1312 __( '%d failed.', 'vigilante' ), 1313 $results['failed'] 1314 ); 1315 } 1316 1317 // Check if current user was included via role membership. 1318 $resetting_self = false; 1319 if ( $include_self ) { 1320 $current_user = wp_get_current_user(); 1321 $resetting_self = ! empty( array_intersect( $roles, $current_user->roles ) ); 1322 } 1323 1324 wp_send_json_success( array( 1325 'message' => $message, 1326 'results' => $results, 1327 'resetting_self' => $resetting_self, 1328 ) ); 1329 } 1330 1331 /** 1231 1332 * AJAX: Approve pending user 1232 1333 */ -
vigilante/trunk/admin/class-admin.php
r3490838 r3492106 142 142 add_action( 'wp_ajax_vigilante_force_password_reset', array( $this, 'ajax_force_password_reset' ) ); 143 143 add_action( 'wp_ajax_vigilante_force_password_reset_all', array( $this, 'ajax_force_password_reset_all' ) ); 144 add_action( 'wp_ajax_vigilante_force_password_reset_by_role', array( $this, 'ajax_force_password_reset_by_role' ) ); 144 145 145 146 // User approval AJAX handlers … … 1036 1037 'warningResettingSelfAll' => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ), 1037 1038 'forceResetAll' => __( 'Force Reset for ALL Users', 'vigilante' ), 1039 // Role-based password reset strings 1040 /* translators: %d: number of users */ 1041 'confirmForceResetByRole' => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ), 1042 'noRolesSelected' => __( 'Please select at least one role.', 'vigilante' ), 1043 'forceResetByRole' => __( 'Force Reset for Selected Roles', 'vigilante' ), 1038 1044 // User approval strings 1039 1045 'confirmApprove' => __( 'Approve this user?', 'vigilante' ), … … 2855 2861 </div> 2856 2862 2863 <!-- Reset by Role --> 2864 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;"> 2865 <h4><?php esc_html_e( 'Reset by role', 'vigilante' ); ?></h4> 2866 <p class="description"><?php esc_html_e( 'Select one or more roles to force a password reset for all users with those roles. Ideal for security incidents where you need to reset access quickly.', 'vigilante' ); ?></p> 2867 2868 <?php 2869 $wp_roles = wp_roles(); 2870 $user_counts = count_users(); 2871 $avail_roles = $user_counts['avail_roles'] ?? array(); 2872 $current_user = wp_get_current_user(); 2873 $current_roles = $current_user->roles; 2874 ?> 2875 2876 <fieldset class="vigilante-role-checkboxes" style="margin-top: 10px;"> 2877 <?php foreach ( $wp_roles->roles as $role_slug => $role_data ) : 2878 $count = $avail_roles[ $role_slug ] ?? 0; 2879 if ( 0 === $count ) { 2880 continue; 2881 } 2882 $role_name = translate_user_role( $role_data['name'] ); 2883 ?> 2884 <label style="display: block; margin-bottom: 6px;"> 2885 <input type="checkbox" 2886 class="vigilante-reset-role-checkbox" 2887 value="<?php echo esc_attr( $role_slug ); ?>" 2888 data-count="<?php echo absint( $count ); ?>"> 2889 <?php 2890 printf( 2891 /* translators: 1: Role name, 2: Number of users */ 2892 '%1$s <span class="description">(%2$d)</span>', 2893 esc_html( $role_name ), 2894 absint( $count ) 2895 ); 2896 ?> 2897 <?php if ( in_array( $role_slug, $current_roles, true ) ) : ?> 2898 <em class="description"><?php esc_html_e( '(includes you)', 'vigilante' ); ?></em> 2899 <?php endif; ?> 2900 </label> 2901 <?php endforeach; ?> 2902 </fieldset> 2903 2904 <div id="vigilante-reset-role-summary" style="display: none; margin-top: 10px;"> 2905 <p> 2906 <span class="dashicons dashicons-groups" style="color: #2271b1;"></span> 2907 <strong id="vigilante-reset-role-count">0</strong> 2908 <?php esc_html_e( 'user(s) will be affected.', 'vigilante' ); ?> 2909 </p> 2910 </div> 2911 2912 <div class="vigilante-password-reset-options" id="vigilante-reset-role-self-option" style="display: none; margin-top: 10px;"> 2913 <label> 2914 <input type="checkbox" id="vigilante-reset-role-include-self" value="1"> 2915 <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?> 2916 </label> 2917 </div> 2918 2919 <p class="submit"> 2920 <button type="button" id="vigilante-reset-by-role" class="button button-primary" disabled> 2921 <?php esc_html_e( 'Force Reset for Selected Roles', 'vigilante' ); ?> 2922 </button> 2923 </p> 2924 </div> 2925 2857 2926 <!-- Reset All Users --> 2858 2927 <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;"> -
vigilante/trunk/assets/js/admin.js
r3490838 r3492106 2086 2086 self.forceResetAllUsers(); 2087 2087 }); 2088 2089 // Reset by role - checkbox change 2090 $(document).on('change', '.vigilante-reset-role-checkbox', function() { 2091 self.updateRoleResetSummary(); 2092 }); 2093 2094 // Reset by role button 2095 $(document).on('click', '#vigilante-reset-by-role', function(e) { 2096 e.preventDefault(); 2097 self.forceResetByRoles(); 2098 }); 2088 2099 }, 2089 2100 … … 2301 2312 complete: function() { 2302 2313 $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetAll || 'Force Reset for ALL Users'); 2314 } 2315 }); 2316 }, 2317 2318 /** 2319 * Update role reset summary when checkboxes change 2320 */ 2321 updateRoleResetSummary: function() { 2322 var totalCount = 0; 2323 var includesSelf = false; 2324 2325 $('.vigilante-reset-role-checkbox:checked').each(function() { 2326 totalCount += parseInt($(this).data('count') || 0); 2327 if ($(this).closest('label').find('em').length > 0) { 2328 includesSelf = true; 2329 } 2330 }); 2331 2332 var $summary = $('#vigilante-reset-role-summary'); 2333 var $selfOption = $('#vigilante-reset-role-self-option'); 2334 var $btn = $('#vigilante-reset-by-role'); 2335 2336 if (totalCount > 0) { 2337 $('#vigilante-reset-role-count').text(totalCount); 2338 $summary.show(); 2339 $btn.prop('disabled', false); 2340 } else { 2341 $summary.hide(); 2342 $btn.prop('disabled', true); 2343 } 2344 2345 if (includesSelf) { 2346 $selfOption.show(); 2347 } else { 2348 $selfOption.hide(); 2349 $('#vigilante-reset-role-include-self').prop('checked', false); 2350 } 2351 }, 2352 2353 /** 2354 * Force reset for users with selected roles 2355 */ 2356 forceResetByRoles: function() { 2357 var self = this; 2358 var roles = []; 2359 2360 $('.vigilante-reset-role-checkbox:checked').each(function() { 2361 roles.push($(this).val()); 2362 }); 2363 2364 if (roles.length === 0) { 2365 self.showNotice('error', vigilanteAdmin.strings.noRolesSelected || 'Please select at least one role.'); 2366 return; 2367 } 2368 2369 var totalCount = 0; 2370 $('.vigilante-reset-role-checkbox:checked').each(function() { 2371 totalCount += parseInt($(this).data('count') || 0); 2372 }); 2373 2374 var includeSelf = $('#vigilante-reset-role-include-self').is(':checked'); 2375 2376 var confirmMsg = (vigilanteAdmin.strings.confirmForceResetByRole || 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.').replace('%d', totalCount); 2377 if (includeSelf) { 2378 confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelfAll || 'WARNING: You are including yourself. Your session will end immediately.'); 2379 } 2380 2381 if (!confirm(confirmMsg)) { 2382 return; 2383 } 2384 2385 var $btn = $('#vigilante-reset-by-role'); 2386 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); 2387 2388 $.ajax({ 2389 url: vigilanteAdmin.ajaxUrl, 2390 type: 'POST', 2391 data: { 2392 action: 'vigilante_force_password_reset_by_role', 2393 nonce: vigilanteAdmin.nonce, 2394 roles: roles, 2395 include_self: includeSelf ? 1 : 0 2396 }, 2397 success: function(response) { 2398 if (response.success) { 2399 self.showNotice('success', response.data.message); 2400 2401 // Clear role checkboxes 2402 $('.vigilante-reset-role-checkbox').prop('checked', false); 2403 self.updateRoleResetSummary(); 2404 2405 if (response.data.resetting_self) { 2406 setTimeout(function() { 2407 window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php'; 2408 }, 2000); 2409 } else { 2410 setTimeout(function() { 2411 location.reload(); 2412 }, 1500); 2413 } 2414 } else { 2415 self.showNotice('error', response.data); 2416 } 2417 }, 2418 error: function() { 2419 self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred'); 2420 }, 2421 complete: function() { 2422 $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetByRole || 'Force Reset for Selected Roles'); 2303 2423 } 2304 2424 }); -
vigilante/trunk/includes/class-login-security.php
r3482104 r3492106 842 842 'two-factor', // TOTP setup required 843 843 'grace period', // TOTP grace period expired 844 'Password reset required', // Force password reset by admin 844 845 ); 845 846 -
vigilante/trunk/includes/class-user-security.php
r3490838 r3492106 157 157 add_action( 'login_message', array( $this, 'show_registration_pending_message' ) ); 158 158 } 159 160 // Force password reset login message (always active, independent of settings) 161 add_filter( 'authenticate', array( $this, 'check_force_reset_on_login' ), 30, 3 ); 162 add_action( 'after_password_reset', array( $this, 'clear_force_reset_meta' ), 10, 1 ); 159 163 } 160 164 … … 823 827 wp_set_password( wp_generate_password( 32, true, true ), $user_id ); 824 828 829 // Flag user so login page shows an informative message instead of generic error 830 update_user_meta( $user_id, 'vigilante_force_reset_pending', time() ); 831 825 832 // Send the native WordPress password reset email 826 833 $email_sent = $this->send_native_reset_email( $user, $reset_key ); … … 942 949 } 943 950 951 /** 952 * Force password reset for users with specific roles 953 * 954 * @param array $roles Array of role slugs. 955 * @param int $reset_by_user_id User ID who initiated the reset. 956 * @param bool $exclude_current Whether to exclude current user. 957 * @return array Results with counts and affected roles. 958 */ 959 public function force_password_reset_by_roles( $roles, $reset_by_user_id = 0, $exclude_current = true ) { 960 if ( empty( $roles ) ) { 961 return array( 962 'success' => 0, 963 'failed' => 0, 964 'emails_sent' => 0, 965 'total' => 0, 966 'roles' => array(), 967 ); 968 } 969 970 $user_ids = array(); 971 972 foreach ( $roles as $role ) { 973 $role_users = get_users( array( 974 'role' => $role, 975 'fields' => 'ID', 976 ) ); 977 $user_ids = array_merge( $user_ids, $role_users ); 978 } 979 980 // Remove duplicates (users with multiple roles). 981 $user_ids = array_unique( array_map( 'absint', $user_ids ) ); 982 983 // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Excluding single user is acceptable here. 984 if ( $exclude_current && $reset_by_user_id ) { 985 $user_ids = array_diff( $user_ids, array( $reset_by_user_id ) ); 986 } 987 // phpcs:enable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude 988 989 $results = $this->force_password_reset_bulk( array_values( $user_ids ), $reset_by_user_id ); 990 $results['roles'] = $roles; 991 992 return $results; 993 } 994 995 /** 996 * Show informative message when a user with a forced reset tries to log in 997 * 998 * Hooked to 'authenticate' at priority 30 (after default password check at 20). 999 * When login fails and the user has a pending forced reset, replaces the generic 1000 * "incorrect password" error with an explanation and instructions. 1001 * 1002 * @param WP_User|WP_Error|null $user User object, error, or null. 1003 * @param string $username Username or email. 1004 * @param string $password Password. 1005 * @return WP_User|WP_Error|null 1006 */ 1007 public function check_force_reset_on_login( $user, $username, $password ) { 1008 // Only act on failed login attempts. 1009 if ( ! is_wp_error( $user ) ) { 1010 return $user; 1011 } 1012 1013 // Only replace "incorrect_password" errors. 1014 if ( ! in_array( 'incorrect_password', $user->get_error_codes(), true ) ) { 1015 return $user; 1016 } 1017 1018 // Resolve the user by login or email. 1019 $login_user = get_user_by( 'login', $username ); 1020 if ( ! $login_user ) { 1021 $login_user = get_user_by( 'email', $username ); 1022 } 1023 1024 if ( ! $login_user ) { 1025 return $user; 1026 } 1027 1028 // Check if this user has a pending forced reset. 1029 $force_reset = get_user_meta( $login_user->ID, 'vigilante_force_reset_pending', true ); 1030 if ( ! $force_reset ) { 1031 return $user; 1032 } 1033 1034 // Skip brute force counter for this controlled rejection. 1035 add_filter( 'vigilante_skip_failed_login_count', '__return_true' ); 1036 1037 return new WP_Error( 1038 'vigilante_force_reset', 1039 __( '<strong>Password reset required:</strong> Your password has been reset by the site administrator for security reasons. Please check your email for a link to set a new password.', 'vigilante' ) 1040 ); 1041 } 1042 1043 /** 1044 * Clear force reset meta after user successfully resets their password 1045 * 1046 * Hooked to 'after_password_reset'. 1047 * 1048 * @param WP_User $user User object. 1049 */ 1050 public function clear_force_reset_meta( $user ) { 1051 delete_user_meta( $user->ID, 'vigilante_force_reset_pending' ); 1052 } 1053 944 1054 // ========================================================================= 945 1055 // Registration Approval - Manual approval for new user registrations -
vigilante/trunk/readme.txt
r3490838 r3492106 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 7.27 Stable tag: 1.8.0 8 8 License: GPL v2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 111 111 * Password expiration with configurable intervals 112 112 * Password history - prevent reusing old passwords 113 * Force password reset for all users (post-hack recovery)113 * Force password reset — by specific users, by role, or all users (post-hack recovery) 114 114 * Session limits - control concurrent logins per user 115 115 * Session management - view and revoke active sessions … … 374 374 375 375 == Changelog == 376 377 = 1.8.0 = 378 * New - Force password reset by role: select one or more roles to reset all their users at once, ideal for security incidents 379 * New - Informative login message when a user tries to log in after a forced password reset 376 380 377 381 = 1.7.2 = … … 606 610 == Upgrade Notice == 607 611 612 = 1.8.0 = 613 Added role-based password reset tool for faster incident response and informative login message for users affected by a forced reset. 614 608 615 = 1.7.2 = 609 616 Dashboard recommendations now link directly to settings. Security scan totals include ignored files count. Activity Log tab renamed to Security Audit. Firewall shows your current IP and cache compatibility notes. -
vigilante/trunk/vigilante.php
r3490838 r3492106 4 4 * Plugin URI: https://servicios.ayudawp.com 5 5 * Description: Complete security solution for WordPress. Firewall, 2FA, security headers, login protection, file integrity monitoring, activity logging and more. 6 * Version: 1. 7.26 * Version: 1.8.0 7 7 * Author: Fernando Tellado 8 8 * Author URI: https://ayudawp.com … … 25 25 * Plugin constants 26 26 */ 27 define( 'VIGILANTE_VERSION', '1. 7.2' );27 define( 'VIGILANTE_VERSION', '1.8.0' ); 28 28 define( 'VIGILANTE_PLUGIN_FILE', __FILE__ ); 29 29 define( 'VIGILANTE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
Note: See TracChangeset
for help on using the changeset viewer.