| @@ -1008,10 +1008,38 @@ | ||
| 1008 | 1008 | return count( $pending_users ); |
| 1009 | 1009 | } |
| 1010 | 1010 | |
| 1011 | 1011 | /** |
| 1012 | + * Whether XML-RPC is blocked, whole or its pingback methods | |
| 1013 | + * | |
| 1014 | + * Both levels have to say so: the three-way setting under WP Hardening, read | |
| 1015 | + * through the module's own resolver, and the WP Hardening module that | |
| 1016 | + * enforces it. Until 3.0.2 the Dashboard read login_security[disable_xmlrpc], | |
| 1017 | + * the checkbox that moved to WP Hardening in 2.9.7 and is never written any | |
| 1018 | + * more, so the score never granted its three points and the recommendation | |
| 1019 | + * to disable XML-RPC stayed on for good. | |
| 1020 | + * | |
| 1021 | + * @since 3.0.2 | |
| 1022 | + * | |
| 1023 | + * @param array $options Plugin options. | |
| 1024 | + * @return bool | |
| 1025 | + */ | |
| 1026 | + private function is_xmlrpc_protected( $options ) { | |
| 1027 | + return ! empty( $options['modules']['wp_hardening'] ) | |
| 1028 | + && 'none' !== Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); | |
| 1029 | + } | |
| 1030 | + | |
| 1031 | + /** | |
| 1012 | 1032 | * Calculate comprehensive security score (0-100) |
| 1013 | 1033 | * |
| 1034 | + * The maximum is the same for every site, 129 points. A module that is | |
| 1035 | + * switched off loses its weight and the points of its details, which stay | |
| 1036 | + * in the total. Until 3.0.4 the details of a module only counted, on both | |
| 1037 | + * sides of the fraction, while the module was on: a site with almost | |
| 1038 | + * everything off kept a middling score, because what it had switched off | |
| 1039 | + * left the total as well, and switching Security Audit off with no alert | |
| 1040 | + * configured raised the score. | |
| 1041 | + * | |
| 1014 | 1042 | * @param array $options Plugin options. |
| 1015 | 1043 | * @return int Security score. |
| 1016 | 1044 | */ |
| 1017 | 1045 | private function calculate_security_score( $options ) { |
| @@ -1037,11 +1065,11 @@ | ||
| 1037 | 1065 | } |
| 1038 | 1066 | } |
| 1039 | 1067 | |
| 1040 | 1068 | // Firewall details (10 points) |
| 1069 | + $max_score += 10; | |
| 1041 | 1070 | if ( ! empty( $options['modules']['firewall'] ) ) { |
| 1042 | 1071 | $firewall = $options['firewall'] ?? array(); |
| 1043 | - $max_score += 10; | |
| 1044 | 1072 | |
| 1045 | 1073 | $firewall_checks = array( |
| 1046 | 1074 | 'block_sql_injection', |
| 1047 | 1075 | 'block_xss_attacks', |
| @@ -1059,18 +1087,18 @@ | ||
| 1059 | 1087 | $score += min( 10, $firewall_enabled * 2 ); |
| 1060 | 1088 | } |
| 1061 | 1089 | |
| 1062 | 1090 | // Login security details (10 points) |
| 1091 | + $max_score += 10; | |
| 1063 | 1092 | if ( ! empty( $options['modules']['login_security'] ) ) { |
| 1064 | 1093 | $login = $options['login_security'] ?? array(); |
| 1065 | - $max_score += 10; | |
| 1066 | 1094 | |
| 1067 | 1095 | // Max attempts configured |
| 1068 | 1096 | if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) { |
| 1069 | 1097 | $score += 3; |
| 1070 | 1098 | } |
| 1071 | - // XML-RPC disabled | |
| 1072 | - if ( ! empty( $login['disable_xmlrpc'] ) ) { | |
| 1099 | + // XML-RPC blocked, whole or its pingback methods | |
| 1100 | + if ( $this->is_xmlrpc_protected( $options ) ) { | |
| 1073 | 1101 | $score += 3; |
| 1074 | 1102 | } |
| 1075 | 1103 | // 2FA enabled |
| 1076 | 1104 | if ( ! empty( $login['two_factor']['enabled'] ) ) { |
| @@ -1078,11 +1106,11 @@ | ||
| 1078 | 1106 | } |
| 1079 | 1107 | } |
| 1080 | 1108 | |
| 1081 | 1109 | // Security headers details (10 points) |
| 1110 | + $max_score += 10; | |
| 1082 | 1111 | if ( ! empty( $options['modules']['security_headers'] ) ) { |
| 1083 | 1112 | $headers = $options['security_headers'] ?? array(); |
| 1084 | - $max_score += 10; | |
| 1085 | 1113 | |
| 1086 | 1114 | if ( ! empty( $headers['x_frame_options'] ) ) { |
| 1087 | 1115 | $score += 2; |
| 1088 | 1116 | } |
| @@ -1097,11 +1125,11 @@ | ||
| 1097 | 1125 | } |
| 1098 | 1126 | } |
| 1099 | 1127 | |
| 1100 | 1128 | // User security details (10 points) |
| 1129 | + $max_score += 10; | |
| 1101 | 1130 | if ( ! empty( $options['modules']['user_security'] ) ) { |
| 1102 | 1131 | $user = $options['user_security'] ?? array(); |
| 1103 | - $max_score += 10; | |
| 1104 | 1132 | |
| 1105 | 1133 | if ( ! empty( $user['block_insecure_usernames'] ) ) { |
| 1106 | 1134 | $score += 3; |
| 1107 | 1135 | } |
| @@ -1115,14 +1143,14 @@ | ||
| 1115 | 1143 | $score += 2; |
| 1116 | 1144 | } |
| 1117 | 1145 | } |
| 1118 | 1146 | |
| 1119 | - // Audit alerts details (6 points) - only when Security Audit is on, | |
| 1120 | - // because the alerting layer rides on top of the activity log. Leaving | |
| 1121 | - // both alert legs off keeps these points unearned. | |
| 1147 | + // Audit alerts details (6 points) - earned only when Security Audit is | |
| 1148 | + // on, because the alerting layer rides on top of the activity log. | |
| 1149 | + // Leaving both alert legs off keeps these points unearned. | |
| 1150 | + $max_score += 6; | |
| 1122 | 1151 | if ( ! empty( $options['modules']['activity_log'] ) ) { |
| 1123 | 1152 | $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array(); |
| 1124 | - $max_score += 6; | |
| 1125 | 1153 | if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) { |
| 1126 | 1154 | $score += 3; |
| 1127 | 1155 | } |
| 1128 | 1156 | if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) { |
| @@ -1143,10 +1171,13 @@ | ||
| 1143 | 1171 | $self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $self_enabled ); |
| 1144 | 1172 | if ( $self_enabled ) { |
| 1145 | 1173 | $score += 10; |
| 1146 | 1174 | // The five points are for a check that ran and came out clean: a |
| 1147 | - // site that has never checked itself has not earned them. | |
| 1148 | - if ( in_array( $self_tone, array( 'ok', 'info' ), true ) ) { | |
| 1175 | + // site that has never checked itself has not earned them. Tasks | |
| 1176 | + // that Vigilant had to schedule again, while that is only a | |
| 1177 | + // warning, do not take them away: the files are as clean as they | |
| 1178 | + // were (Vigilante_Self_Integrity::score_tone()). | |
| 1179 | + if ( in_array( Vigilante_Self_Integrity::score_tone( Vigilante_Self_Integrity::display_state(), $self_enabled ), array( 'ok', 'info' ), true ) ) { | |
| 1149 | 1180 | $score += 5; |
| 1150 | 1181 | } |
| 1151 | 1182 | } |
| 1152 | 1183 | |
| @@ -1175,8 +1206,24 @@ | ||
| 1175 | 1206 | return $percent; |
| 1176 | 1207 | } |
| 1177 | 1208 | |
| 1178 | 1209 | /** |
| 1210 | + * Recommendation of the Dashboard for a critical self-protection state | |
| 1211 | + * | |
| 1212 | + * @param string $kind What the alarm is about, from Vigilante_Self_Integrity::alarm_kind(). | |
| 1213 | + * @return string | |
| 1214 | + */ | |
| 1215 | + private function self_critical_recommendation( $kind ) { | |
| 1216 | + if ( 'tasks' === $kind ) { | |
| 1217 | + return __( 'Something keeps removing Vigilant scheduled tasks: File Integrity says what to look for.', 'vigilante' ); | |
| 1218 | + } | |
| 1219 | + if ( 'hooks' === $kind ) { | |
| 1220 | + return __( 'Something removed the hooks of Vigilant self-protection: File Integrity says what to look for.', 'vigilante' ); | |
| 1221 | + } | |
| 1222 | + return __( 'Vigilant own files have been changed: repair Vigilant from File Integrity before anything else.', 'vigilante' ); | |
| 1223 | + } | |
| 1224 | + | |
| 1225 | + /** | |
| 1179 | 1226 | * Get security recommendations based on current settings |
| 1180 | 1227 | * |
| 1181 | 1228 | * @param array $options Plugin options. |
| 1182 | 1229 | * @return array Array of recommendations. |
| @@ -1187,17 +1234,12 @@ | ||
| 1187 | 1234 | // Self-protection first: if Vigilant itself cannot be trusted, nothing |
| 1188 | 1235 | // else on this card means much. |
| 1189 | 1236 | $vg_self_enabled = Vigilante_Self_Integrity::is_on(); |
| 1190 | 1237 | $vg_self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $vg_self_enabled ); |
| 1191 | - if ( ! $vg_self_enabled ) { | |
| 1238 | + // Switched off has no setting to turn back on: it takes code on the | |
| 1239 | + // site, and the recommendation says where to look for it. | |
| 1240 | + if ( 'off' === $vg_self_tone ) { | |
| 1192 | 1241 | $recommendations[] = array( |
| 1193 | - 'icon' => 'shield', | |
| 1194 | - 'priority' => 'high', | |
| 1195 | - 'tab' => 'file-integrity', | |
| 1196 | - 'message' => __( 'Turn Vigilant self-protection on, so a change to Vigilant own files does not go unnoticed.', 'vigilante' ), | |
| 1197 | - ); | |
| 1198 | - } elseif ( 'off' === $vg_self_tone ) { | |
| 1199 | - $recommendations[] = array( | |
| 1200 | 1242 | 'icon' => 'warning', |
| 1201 | 1243 | 'priority' => 'critical', |
| 1202 | 1244 | 'tab' => 'file-integrity', |
| 1203 | 1245 | 'message' => __( 'Something on this site switched Vigilant self-protection off: File Integrity says which file does it.', 'vigilante' ), |
| @@ -1206,9 +1248,9 @@ | ||
| 1206 | 1248 | $recommendations[] = array( |
| 1207 | 1249 | 'icon' => 'warning', |
| 1208 | 1250 | 'priority' => 'critical', |
| 1209 | 1251 | 'tab' => 'file-integrity', |
| 1210 | - 'message' => __( 'Vigilant own files have been changed: repair Vigilant from File Integrity before anything else.', 'vigilante' ), | |
| 1252 | + 'message' => $this->self_critical_recommendation( Vigilante_Self_Integrity::alarm_kind( Vigilante_Self_Integrity::display_state(), $vg_self_enabled ) ), | |
| 1211 | 1253 | ); |
| 1212 | 1254 | } elseif ( 'warning' === $vg_self_tone ) { |
| 1213 | 1255 | $recommendations[] = array( |
| 1214 | 1256 | 'icon' => 'shield', |
| @@ -1310,14 +1352,14 @@ | ||
| 1310 | 1352 | ); |
| 1311 | 1353 | } |
| 1312 | 1354 | |
| 1313 | 1355 | // Low: XML-RPC enabled (only if login security is active) |
| 1314 | - if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) { | |
| 1356 | + if ( ! empty( $options['modules']['login_security'] ) && ! $this->is_xmlrpc_protected( $options ) ) { | |
| 1315 | 1357 | $recommendations[] = array( |
| 1316 | 1358 | 'icon' => 'info', |
| 1317 | 1359 | 'priority' => 'low', |
| 1318 | 1360 | 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ), |
| 1319 | - 'tab' => 'login', | |
| 1361 | + 'tab' => 'wp-hardening', | |
| 1320 | 1362 | ); |
| 1321 | 1363 | } |
| 1322 | 1364 | |
| 1323 | 1365 | // Low: Strong passwords not enforced (only if user security is active) |
| @@ -1809,9 +1851,8 @@ | ||
| 1809 | 1851 | 'scanComplete' => __( 'Scan complete', 'vigilante' ), |
| 1810 | 1852 | 'loading' => __( 'Loading...', 'vigilante' ), |
| 1811 | 1853 | 'searching' => __( 'Searching...', 'vigilante' ), |
| 1812 | 1854 | 'noUsersFound' => __( 'No users found', 'vigilante' ), |
| 1813 | - 'searchError' => __( 'Error searching users', 'vigilante' ), | |
| 1814 | 1855 | 'sending' => __( 'Sending...', 'vigilante' ), |
| 1815 | 1856 | 'sendNotification' => __( 'Send notification now', 'vigilante' ), |
| 1816 | 1857 | 'notificationsSent' => __( 'notifications sent', 'vigilante' ), |
| 1817 | 1858 | 'skipped' => __( 'skipped', 'vigilante' ), |
| @@ -1820,10 +1861,11 @@ | ||
| 1820 | 1861 | // Header tester strings |
| 1821 | 1862 | 'testHeaders' => __( 'Test Headers', 'vigilante' ), |
| 1822 | 1863 | 'testing' => __( 'Testing...', 'vigilante' ), |
| 1823 | 1864 | 'score' => __( 'Score', 'vigilante' ), |
| 1824 | - 'enabledHeaders' => __( 'Enabled headers', 'vigilante' ), | |
| 1825 | - 'missingHeaders' => __( 'Missing headers', 'vigilante' ), | |
| 1865 | + 'enabledHeaders' => __( 'Passed', 'vigilante' ), | |
| 1866 | + 'missingHeaders' => __( 'Failing', 'vigilante' ), | |
| 1867 | + 'headersMeasured' => __( 'Measured right now on the home page of the site, with the same checks as Security Check.', 'vigilante' ), | |
| 1826 | 1868 | 'warnings' => __( 'Warnings', 'vigilante' ), |
| 1827 | 1869 | // File integrity scan results strings |
| 1828 | 1870 | 'scanResults' => __( 'Scan Results', 'vigilante' ), |
| 1829 | 1871 | 'ok' => __( 'OK', 'vigilante' ), |
| @@ -1830,9 +1872,9 @@ | ||
| 1830 | 1872 | 'modified' => __( 'Modified', 'vigilante' ), |
| 1831 | 1873 | 'suspicious' => __( 'Suspicious', 'vigilante' ), |
| 1832 | 1874 | 'totalScanned' => __( 'Total Scanned', 'vigilante' ), |
| 1833 | 1875 | 'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ), |
| 1834 | - 'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ), | |
| 1876 | + 'suspiciousWarning' => __( 'Files that can run code where none is expected, or that contain code written the way malware hides itself. Real infections show up here, and so do harmless files that plugins generate. Check the path and the reason before acting.', 'vigilante' ), | |
| 1835 | 1877 | 'file' => __( 'File', 'vigilante' ), |
| 1836 | 1878 | 'reason' => __( 'Reason', 'vigilante' ), |
| 1837 | 1879 | 'type' => __( 'Type', 'vigilante' ), |
| 1838 | 1880 | 'unknown' => __( 'Unknown', 'vigilante' ), |
| @@ -1841,13 +1883,13 @@ | ||
| 1841 | 1883 | 'logWhatItMeans' => __( 'What it means', 'vigilante' ), |
| 1842 | 1884 | 'logWhatToDo' => __( 'What to do', 'vigilante' ), |
| 1843 | 1885 | 'logSelfSeeDetails' => __( 'Open File Integrity for the full detail', 'vigilante' ), |
| 1844 | 1886 | 'modifiedFiles' => __( 'Modified Files', 'vigilante' ), |
| 1845 | - 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ), | |
| 1887 | + 'modifiedDescription' => __( 'These files do not match the copy WordPress.org publishes for the installed version.', 'vigilante' ), | |
| 1846 | 1888 | 'extraFiles' => __( 'Extra Files', 'vigilante' ), |
| 1847 | 1889 | 'extra' => __( 'Extra', 'vigilante' ), |
| 1848 | 1890 | 'ignored' => __( 'Ignored', 'vigilante' ), |
| 1849 | - 'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ), | |
| 1891 | + 'extraDescription' => __( 'Files that are not part of WordPress, or of the plugin or theme they sit in. Many are legitimate, and an injected file looks the same from here, so confirm where each one comes from.', 'vigilante' ), | |
| 1850 | 1892 | 'actions' => __( 'Actions', 'vigilante' ), |
| 1851 | 1893 | 'ignore' => __( 'Ignore', 'vigilante' ), |
| 1852 | 1894 | 'ignoring' => __( 'Ignoring...', 'vigilante' ), |
| 1853 | 1895 | 'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ), |
| @@ -1855,9 +1897,8 @@ | ||
| 1855 | 1897 | 'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1856 | 1898 | 'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ), |
| 1857 | 1899 | 'selectAll' => __( 'Select all', 'vigilante' ), |
| 1858 | 1900 | 'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ), |
| 1859 | - 'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ), | |
| 1860 | 1901 | 'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ), |
| 1861 | 1902 | 'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ), |
| 1862 | 1903 | 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1863 | 1904 | 'bulkProcessing' => __( 'Processing...', 'vigilante' ), |
| @@ -1862,11 +1903,11 @@ | ||
| 1862 | 1903 | 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1863 | 1904 | 'bulkProcessing' => __( 'Processing...', 'vigilante' ), |
| 1864 | 1905 | /* translators: %d: number of files selected for bulk action. */ |
| 1865 | 1906 | 'bulkSelectedCount' => __( '%d selected', 'vigilante' ), |
| 1866 | - 'allClear' => __( 'All files verified - no issues found!', 'vigilante' ), | |
| 1907 | + 'allClear' => __( 'Good job! Nothing to report in the files that were checked.', 'vigilante' ), | |
| 1867 | 1908 | 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ), |
| 1868 | - 'criticalConfigDesc' => __( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ), | |
| 1909 | + 'criticalConfigDesc' => __( 'These files changed since you last approved them. They are a common target for code injection, and they are also rewritten by cache, security and redirection plugins, often on every update. Review the changes and approve them if they belong to something you use: approving records the current content as the reference and does not touch the file. Vigilant\'s own blocks are excluded from this check, and Vigilant never restores these files by itself.', 'vigilante' ), | |
| 1869 | 1910 | 'approve' => __( 'Approve', 'vigilante' ), |
| 1870 | 1911 | 'approving' => __( 'Approving...', 'vigilante' ), |
| 1871 | 1912 | 'approvalLockedNotice' => $this->critical_approval_notice(), |
| 1872 | 1913 | 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ), |
| @@ -1959,13 +2000,10 @@ | ||
| 1959 | 2000 | 'noLogsToExport' => __( 'No logs to export', 'vigilante' ), |
| 1960 | 2001 | 'exportFailed' => __( 'Export failed', 'vigilante' ), |
| 1961 | 2002 | 'exportLogs' => __( 'Export Logs', 'vigilante' ), |
| 1962 | 2003 | // Backup strings |
| 1963 | - 'backupCreated' => __( 'Backup created successfully.', 'vigilante' ), | |
| 1964 | 2004 | 'createBackupNow' => __( 'Download Backup', 'vigilante' ), |
| 1965 | 2005 | 'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ), |
| 1966 | - /* translators: %d: number of tables */ | |
| 1967 | - 'tablesCount' => __( '%d tables', 'vigilante' ), | |
| 1968 | 2006 | /* translators: 1: table count, 2: human-readable size */ |
| 1969 | 2007 | 'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ), |
| 1970 | 2008 | /* translators: 1: selected count, 2: human-readable size */ |
| 1971 | 2009 | 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ), |
| @@ -1972,15 +2010,12 @@ | ||
| 1972 | 2010 | // Settings search strings |
| 1973 | 2011 | 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ), |
| 1974 | 2012 | /* translators: %d: number of results that did not fit in the list. */ |
| 1975 | 2013 | 'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ), |
| 1976 | - 'searchInTab' => __( 'in', 'vigilante' ), | |
| 1977 | - // Modules string | |
| 1978 | - /* translators: 1: enabled count, 2: total count */ | |
| 1979 | - 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ), | |
| 1980 | 2014 | // Activity log label maps for JS rendering |
| 1981 | 2015 | 'eventTypeLabels' => array( |
| 1982 | 2016 | 'login' => __( 'Login', 'vigilante' ), |
| 2017 | + '2fa' => __( 'Two-factor', 'vigilante' ), | |
| 1983 | 2018 | 'user' => __( 'User', 'vigilante' ), |
| 1984 | 2019 | 'content' => __( 'Content', 'vigilante' ), |
| 1985 | 2020 | 'plugin' => __( 'Plugin', 'vigilante' ), |
| 1986 | 2021 | 'theme' => __( 'Theme', 'vigilante' ), |
| @@ -2046,10 +2081,8 @@ | ||
| 2046 | 2081 | /* translators: 1: first item number, 2: last item number, 3: total items */ |
| 2047 | 2082 | 'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ), |
| 2048 | 2083 | 'paginationEmpty' => __( '0 items', 'vigilante' ), |
| 2049 | 2084 | // Security Analyzer strings |
| 2050 | - 'analyzerScanNow' => __( 'Scan now', 'vigilante' ), | |
| 2051 | - 'analyzerScanning' => __( 'Scanning…', 'vigilante' ), | |
| 2052 | 2085 | 'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ), |
| 2053 | 2086 | 'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ), |
| 2054 | 2087 | 'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ), |
| 2055 | 2088 | 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ), |
| @@ -2055,11 +2088,8 @@ | ||
| 2055 | 2088 | 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ), |
| 2056 | 2089 | 'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ), |
| 2057 | 2090 | 'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ), |
| 2058 | 2091 | 'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ), |
| 2059 | - 'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ), | |
| 2060 | - 'analyzerJustNow' => __( 'just now', 'vigilante' ), | |
| 2061 | - 'analyzerAgo' => __( 'ago', 'vigilante' ), | |
| 2062 | 2092 | 'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ), |
| 2063 | 2093 | 'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ), |
| 2064 | 2094 | 'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ), |
| 2065 | 2095 | 'analyzerQualityGood' => __( 'Good', 'vigilante' ), |
| @@ -2065,9 +2095,9 @@ | ||
| 2065 | 2095 | 'analyzerQualityGood' => __( 'Good', 'vigilante' ), |
| 2066 | 2096 | 'analyzerQualityFair' => __( 'Fair', 'vigilante' ), |
| 2067 | 2097 | 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ), |
| 2068 | 2098 | 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ), |
| 2069 | - 'analyzerPts' => __( 'pts', 'vigilante' ), | |
| 2099 | + 'analyzerNotMeasured' => __( 'Not measured', 'vigilante' ), | |
| 2070 | 2100 | 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ), |
| 2071 | 2101 | 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ), |
| 2072 | 2102 | /* translators: %d: number of findings in an info-only category */ |
| 2073 | 2103 | 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ), |
| @@ -2129,9 +2159,11 @@ | ||
| 2129 | 2159 | <p class="vigilante-self-notice-title"> |
| 2130 | 2160 | <span class="dashicons dashicons-shield" aria-hidden="true"></span> |
| 2131 | 2161 | <strong> |
| 2132 | 2162 | <?php |
| 2133 | - if ( 'critical' === $tone ) { | |
| 2163 | + if ( 'critical' === $tone && in_array( $summary['kind'], array( 'tasks', 'hooks' ), true ) ) { | |
| 2164 | + echo esc_html( $this->self_integrity_headline( $tone, $summary['state'] ) ); | |
| 2165 | + } elseif ( 'critical' === $tone ) { | |
| 2134 | 2166 | esc_html_e( 'Vigilant detected changes in its own files', 'vigilante' ); |
| 2135 | 2167 | } elseif ( 'off' === $tone ) { |
| 2136 | 2168 | esc_html_e( 'Vigilant self-protection is switched off by code', 'vigilante' ); |
| 2137 | 2169 | } else { |
| @@ -2142,9 +2174,13 @@ | ||
| 2142 | 2174 | </p> |
| 2143 | 2175 | <p class="vigilante-self-notice-text"> |
| 2144 | 2176 | <span> |
| 2145 | 2177 | <?php |
| 2146 | - if ( 'critical' === $tone ) { | |
| 2178 | + if ( 'critical' === $tone && ! empty( $summary['tasks'] ) ) { | |
| 2179 | + esc_html_e( 'Vigilant scheduled them again each time, but something keeps removing them. File Integrity says what to look for.', 'vigilante' ); | |
| 2180 | + } elseif ( 'critical' === $tone && 'hooks' === $summary['kind'] ) { | |
| 2181 | + esc_html_e( 'Code on this site removed hooks that run the self-check of Vigilant. File Integrity says which ones and what to look for.', 'vigilante' ); | |
| 2182 | + } elseif ( 'critical' === $tone ) { | |
| 2147 | 2183 | esc_html_e( 'Your security plugin may have been tampered with, and while that is true nothing it reports can be trusted.', 'vigilante' ); |
| 2148 | 2184 | } elseif ( 'off' === $tone ) { |
| 2149 | 2185 | esc_html_e( 'Nothing is checking that Vigilant own files are intact. File Integrity says which file switches it off.', 'vigilante' ); |
| 2150 | 2186 | } else { |
| @@ -2149,9 +2185,11 @@ | ||
| 2149 | 2185 | esc_html_e( 'Nothing is checking that Vigilant own files are intact. File Integrity says which file switches it off.', 'vigilante' ); |
| 2150 | 2186 | } else { |
| 2151 | 2187 | esc_html_e( 'File Integrity says what was found, what it means and what to do about it.', 'vigilante' ); |
| 2152 | 2188 | } |
| 2153 | - if ( $network ) { | |
| 2189 | + // Repairing is for a case about the files: tasks, removed | |
| 2190 | + // hooks and a filter are not repaired with a clean copy. | |
| 2191 | + if ( $network && ! empty( $summary['about_files'] ) ) { | |
| 2154 | 2192 | echo ' ' . esc_html__( 'Only your network administrator can repair Vigilant, because its files are shared by every site in the network. Let them know.', 'vigilante' ); |
| 2155 | 2193 | } |
| 2156 | 2194 | ?> |
| 2157 | 2195 | </span> |
| @@ -2653,10 +2691,10 @@ | ||
| 2653 | 2691 | |
| 2654 | 2692 | /** |
| 2655 | 2693 | * Render the Security Analyzer (Security Check) widget + expandable full report. |
| 2656 | 2694 | * |
| 2657 | - * Lives in the Dashboard tab between the Configuration Score status card and | |
| 2658 | - * the modules grid. Uses the last persisted scan to hydrate server-side so the | |
| 2695 | + * Lives in the Dashboard tab above the configuration card and the modules | |
| 2696 | + * grid: it carries the only letter of the page. Uses the last persisted scan to hydrate server-side so the | |
| 2659 | 2697 | * first paint shows real data; "Scan now" runs the 2-phase AJAX to refresh. |
| 2660 | 2698 | * |
| 2661 | 2699 | * @param array $last_scan Result of Vigilante_Security_Analyzer::get_last_scan(). |
| 2662 | 2700 | * @param array $history Score history (oldest first). |
| @@ -2687,14 +2725,34 @@ | ||
| 2687 | 2725 | */ |
| 2688 | 2726 | $self_summary = $this->self_integrity_summary(); |
| 2689 | 2727 | $self_capped = in_array( $self_summary['tone'], array( 'critical', 'off' ), true ) |
| 2690 | 2728 | || ( 'self_integrity' === ( isset( $last_scan['capped_by'] ) ? $last_scan['capped_by'] : '' ) ); |
| 2729 | + // Why the note under the title says the score is capped: something | |
| 2730 | + // that is true now, about tasks or about files, or only what the | |
| 2731 | + // stored report found when it ran. In that last case the note does not | |
| 2732 | + // say that anything is wrong now: a report stored before the notice | |
| 2733 | + // about tasks closed, or before a repair, kept saying in the present | |
| 2734 | + // that the files had been changed until the next scan. | |
| 2735 | + $self_capped_now = in_array( $self_summary['tone'], array( 'critical', 'off' ), true ); | |
| 2736 | + $self_capped_why = 'stored'; | |
| 2737 | + if ( $self_capped_now ) { | |
| 2738 | + $self_capped_why = 'files'; | |
| 2739 | + if ( 'tasks' === $self_summary['kind'] ) { | |
| 2740 | + $self_capped_why = 'tasks'; | |
| 2741 | + } elseif ( in_array( $self_summary['kind'], array( 'off', 'hooks' ), true ) ) { | |
| 2742 | + // The check itself is switched off or unhooked: no file was found changed. | |
| 2743 | + $self_capped_why = 'check'; | |
| 2744 | + } | |
| 2745 | + } | |
| 2691 | 2746 | if ( $self_capped && $has_data && $score > Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER ) { |
| 2692 | 2747 | $score = Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER; |
| 2693 | - $grade = 'E'; | |
| 2694 | 2748 | } |
| 2749 | + if ( $has_data && '' !== $grade ) { | |
| 2750 | + $grade = Vigilante_SA_Helpers::grade_for_score( $score ); | |
| 2751 | + } | |
| 2695 | 2752 | |
| 2696 | - $quality = self::analyzer_quality_tag( $score ); | |
| 2753 | + $quality = self::analyzer_quality_tag( $score ); | |
| 2754 | + $not_measured = isset( $counts['skip'] ) ? (int) $counts['skip'] : 0; | |
| 2697 | 2755 | ?> |
| 2698 | 2756 | <div class="vigilante-analyzer" id="vigilante-analyzer" |
| 2699 | 2757 | data-has-data="<?php echo $has_data ? '1' : '0'; ?>"> |
| 2700 | 2758 | <div class="vigilante-analyzer-header"> |
| @@ -2703,9 +2761,9 @@ | ||
| 2703 | 2761 | <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span> |
| 2704 | 2762 | <?php esc_html_e( 'Security Check', 'vigilante' ); ?> |
| 2705 | 2763 | </h2> |
| 2706 | 2764 | <p class="description"> |
| 2707 | - <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?> | |
| 2765 | + <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus internal checks impossible from the outside.', 'vigilante' ); ?> | |
| 2708 | 2766 | </p> |
| 2709 | 2767 | <p class="vigilante-analyzer-last-scan" data-role="ran-at"> |
| 2710 | 2768 | <span class="dashicons dashicons-clock" aria-hidden="true"></span> |
| 2711 | 2769 | <?php echo esc_html( $ran_at_human ); ?> |
| @@ -2719,12 +2777,22 @@ | ||
| 2719 | 2777 | </div> |
| 2720 | 2778 | </div> |
| 2721 | 2779 | |
| 2722 | 2780 | <?php if ( $self_capped ) : ?> |
| 2723 | - <p class="vigilante-analyzer-capped"> | |
| 2781 | + <p class="vigilante-analyzer-capped" data-why="<?php echo esc_attr( $self_capped_why ); ?>"> | |
| 2724 | 2782 | <span class="dashicons dashicons-shield" aria-hidden="true"></span> |
| 2725 | - <strong><?php esc_html_e( 'This score is not reliable right now.', 'vigilante' ); ?></strong> | |
| 2726 | - <?php esc_html_e( 'Vigilant own files have been changed, and every other result on this page is produced by that same code. The score is held at the bottom of the scale until the files verify clean again.', 'vigilante' ); ?> | |
| 2783 | + <strong><?php 'stored' === $self_capped_why ? esc_html_e( 'This score is from an earlier scan.', 'vigilante' ) : esc_html_e( 'This score is not reliable right now.', 'vigilante' ); ?></strong> | |
| 2784 | + <?php | |
| 2785 | + if ( 'stored' === $self_capped_why ) { | |
| 2786 | + esc_html_e( 'When this scan ran, Vigilant self-protection was reporting a critical finding, so the score was held at the bottom of the scale. Run a new scan to bring it up to date.', 'vigilante' ); | |
| 2787 | + } elseif ( 'tasks' === $self_capped_why ) { | |
| 2788 | + esc_html_e( 'Something keeps removing Vigilant scheduled tasks, so its scans and its daily check do not run when they should. The score is held at the bottom of the scale while that goes on.', 'vigilante' ); | |
| 2789 | + } elseif ( 'check' === $self_capped_why ) { | |
| 2790 | + esc_html_e( 'Code on this site is keeping Vigilant self-protection from running as it should, so the code that produces every other result on this page is not being checked as it should. The score is held at the bottom of the scale while that goes on.', 'vigilante' ); | |
| 2791 | + } else { | |
| 2792 | + esc_html_e( 'Vigilant own files have been changed, and every other result on this page is produced by that same code. The score is held at the bottom of the scale until the files verify clean again.', 'vigilante' ); | |
| 2793 | + } | |
| 2794 | + ?> | |
| 2727 | 2795 | <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' ) ); ?>"> |
| 2728 | 2796 | <?php esc_html_e( 'See what to do', 'vigilante' ); ?> |
| 2729 | 2797 | </a> |
| 2730 | 2798 | </p> |
| @@ -2769,8 +2837,20 @@ | ||
| 2769 | 2837 | <span class="dashicons dashicons-dismiss" aria-hidden="true"></span> |
| 2770 | 2838 | <strong data-role="fail"><?php echo esc_html( isset( $counts['fail'] ) ? $counts['fail'] : 0 ); ?></strong> |
| 2771 | 2839 | <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Failing', 'vigilante' ); ?></span> |
| 2772 | 2840 | </span> |
| 2841 | + <?php | |
| 2842 | + // What could not be measured is left out of the score, so | |
| 2843 | + // it is said here: a score over fewer checks is not the | |
| 2844 | + // same score. | |
| 2845 | + ?> | |
| 2846 | + <span class="vigilante-analyzer-count vigilante-analyzer-count--skip" data-role="skip-wrap" | |
| 2847 | + title="<?php esc_attr_e( 'Checks that could not be run this time. They are not part of the score.', 'vigilante' ); ?>" | |
| 2848 | + <?php echo $not_measured > 0 ? '' : 'hidden'; ?>> | |
| 2849 | + <span class="dashicons dashicons-minus" aria-hidden="true"></span> | |
| 2850 | + <strong data-role="skip"><?php echo esc_html( $not_measured ); ?></strong> | |
| 2851 | + <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Not measured', 'vigilante' ); ?></span> | |
| 2852 | + </span> | |
| 2773 | 2853 | </div> |
| 2774 | 2854 | |
| 2775 | 2855 | <?php |
| 2776 | 2856 | // Sparkline of recent scores. Require at least 3 data points so the trend |
| @@ -2848,18 +2928,29 @@ | ||
| 2848 | 2928 | <div class="vigilante-analyzer-categories" data-role="categories"> |
| 2849 | 2929 | <?php foreach ( $categories_def as $slug => $meta ) : |
| 2850 | 2930 | $cat = isset( $categories[ $slug ] ) ? $categories[ $slug ] : array(); |
| 2851 | 2931 | $earned = isset( $cat['earned'] ) ? (int) $cat['earned'] : 0; |
| 2852 | - // Always use the declared meta as the source of truth for the maximum. | |
| 2853 | - // The cached scan may carry an old max if a check was added/removed | |
| 2854 | - // between releases (see 2.6.1: closed_plugins raised internal from 22 to 28 | |
| 2855 | - // but cached scans still reported max=22 until reset). | |
| 2856 | - $cat_max = (int) $meta['max']; | |
| 2932 | + $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array(); | |
| 2857 | 2933 | $info_only = ! empty( $meta['info_only'] ) || 0 === (int) $meta['max']; |
| 2934 | + // The maximum of a category is what its checks could | |
| 2935 | + // score this time: one that could not be measured, or | |
| 2936 | + // that is only informative, is not in it. The report | |
| 2937 | + // adds it up from the checks themselves | |
| 2938 | + // (Vigilante_Security_Analyzer::summarize_categories()), | |
| 2939 | + // so it cannot drift from them. Before the first scan | |
| 2940 | + // there are no checks and the declared figure is shown. | |
| 2941 | + $cat_max = ( ! empty( $checks ) && isset( $cat['max'] ) ) ? (int) $cat['max'] : (int) $meta['max']; | |
| 2858 | 2942 | $cat_pct = $cat_max > 0 ? (int) round( ( $earned / $cat_max ) * 100 ) : 0; |
| 2859 | - $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array(); | |
| 2860 | 2943 | $cat_counts = isset( $cat['counts'] ) && is_array( $cat['counts'] ) ? $cat['counts'] : array( 'pass' => 0, 'warn' => 0, 'fail' => 0, 'info' => 0 ); |
| 2861 | 2944 | $cat_quality = self::analyzer_quality_tag( $cat_pct ); |
| 2945 | + // Nothing in the category could be scored: that is not | |
| 2946 | + // a zero, and it does not get the red label of one. | |
| 2947 | + if ( ! $info_only && ! empty( $checks ) && 0 === $cat_max ) { | |
| 2948 | + $cat_quality = array( | |
| 2949 | + 'label' => __( 'Not measured', 'vigilante' ), | |
| 2950 | + 'slug' => 'info', | |
| 2951 | + ); | |
| 2952 | + } | |
| 2862 | 2953 | $info_count = isset( $cat_counts['info'] ) ? (int) $cat_counts['info'] : 0; |
| 2863 | 2954 | ?> |
| 2864 | 2955 | <details class="vigilante-analyzer-category<?php echo $info_only ? ' vigilante-analyzer-category--info' : ''; ?>" |
| 2865 | 2956 | data-category="<?php echo esc_attr( $slug ); ?>" |
| @@ -2908,9 +2999,9 @@ | ||
| 2908 | 2999 | </span> |
| 2909 | 3000 | <?php endif; ?> |
| 2910 | 3001 | <?php if ( ! $info_only ) : ?> |
| 2911 | 3002 | <span class="vigilante-analyzer-category-score"> |
| 2912 | - <span data-role="earned"><?php echo esc_html( $earned ); ?></span><span class="vigilante-analyzer-category-score-sep">/</span><?php echo esc_html( $cat_max ); ?> | |
| 3003 | + <span data-role="earned"><?php echo esc_html( $earned ); ?></span><span class="vigilante-analyzer-category-score-sep">/</span><span data-role="max"><?php echo esc_html( $cat_max ); ?></span> | |
| 2913 | 3004 | <span class="vigilante-analyzer-category-score-unit"><?php esc_html_e( 'pts', 'vigilante' ); ?></span> |
| 2914 | 3005 | </span> |
| 2915 | 3006 | <span class="vigilante-analyzer-category-bar" aria-hidden="true"> |
| 2916 | 3007 | <span class="vigilante-analyzer-category-bar-fill vigilante-analyzer-category-bar-fill--<?php echo esc_attr( $cat_quality['slug'] ); ?>" |
| @@ -2987,23 +3078,27 @@ | ||
| 2987 | 3078 | * @param int $pct 0..100. |
| 2988 | 3079 | * @return array{label:string,slug:string} |
| 2989 | 3080 | */ |
| 2990 | 3081 | public static function analyzer_quality_tag( $pct ) { |
| 2991 | - $pct = max( 0, min( 100, (int) $pct ) ); | |
| 2992 | - // "Excellent" is reserved for a perfect score — a single missing point drops to Good. | |
| 2993 | - if ( 100 === $pct ) { | |
| 2994 | - return array( 'label' => __( 'Excellent', 'vigilante' ), 'slug' => 'a' ); | |
| 3082 | + if ( ! class_exists( 'Vigilante_SA_Helpers' ) ) { | |
| 3083 | + require_once VIGILANTE_INCLUDES_DIR . 'security-analyzer/class-sa-helpers.php'; | |
| 2995 | 3084 | } |
| 2996 | - if ( $pct >= 70 ) { | |
| 2997 | - return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' ); | |
| 2998 | - } | |
| 2999 | - if ( $pct >= 50 ) { | |
| 3000 | - return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' ); | |
| 3001 | - } | |
| 3002 | - if ( $pct >= 30 ) { | |
| 3003 | - return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' ); | |
| 3004 | - } | |
| 3005 | - return array( 'label' => __( 'Critical', 'vigilante' ), 'slug' => 'e' ); | |
| 3085 | + | |
| 3086 | + // The word follows the letter, and the letter has one scale: | |
| 3087 | + // Vigilante_SA_Helpers::grade_for_score(). | |
| 3088 | + $labels = array( | |
| 3089 | + 'A' => __( 'Excellent', 'vigilante' ), | |
| 3090 | + 'B' => __( 'Good', 'vigilante' ), | |
| 3091 | + 'C' => __( 'Fair', 'vigilante' ), | |
| 3092 | + 'D' => __( 'Poor', 'vigilante' ), | |
| 3093 | + 'E' => __( 'Critical', 'vigilante' ), | |
| 3094 | + ); | |
| 3095 | + $grade = Vigilante_SA_Helpers::grade_for_score( $pct ); | |
| 3096 | + | |
| 3097 | + return array( | |
| 3098 | + 'label' => $labels[ $grade ], | |
| 3099 | + 'slug' => strtolower( $grade ), | |
| 3100 | + ); | |
| 3006 | 3101 | } |
| 3007 | 3102 | |
| 3008 | 3103 | /** |
| 3009 | 3104 | * Render a single analyzer check row (used both server-side and via JS template). |
| @@ -3105,10 +3200,93 @@ | ||
| 3105 | 3200 | /** |
| 3106 | 3201 | * Render dashboard tab |
| 3107 | 3202 | */ |
| 3108 | 3203 | /** |
| 3204 | + * The Vigilant configuration card of the Dashboard: how much is switched on | |
| 3205 | + * | |
| 3206 | + * A bar with a percentage and, on purpose, neither a circle nor a letter. | |
| 3207 | + * Until 3.0.4 this was the Configuration Score, drawn exactly like the | |
| 3208 | + * score of Security Check next to it, and two letters side by side read | |
| 3209 | + * as two grades of the same site that did not agree. The grade is the one | |
| 3210 | + * of Security Check, which measures the site; this only counts settings, | |
| 3211 | + * so it sits with the recommendations and the module switches it counts. | |
| 3212 | + * The figure is still the one of calculate_security_score(). | |
| 3213 | + * | |
| 3214 | + * @since 3.0.5 | |
| 3215 | + * | |
| 3216 | + * @param int $security_score What calculate_security_score() gives, 0 to 100. | |
| 3217 | + * @param array $options The settings of the plugin. | |
| 3218 | + * @param string|bool $active_preset Slug of the preset in force, if any. | |
| 3219 | + * @param array $presets The presets, by slug. | |
| 3220 | + */ | |
| 3221 | + private function render_configuration_card( $security_score, $options, $active_preset, $presets ) { | |
| 3222 | + $modules = ( isset( $options['modules'] ) && is_array( $options['modules'] ) ) ? $options['modules'] : array(); | |
| 3223 | + $modules_on = count( array_filter( $modules ) ); | |
| 3224 | + $level = max( 0, min( 100, (int) $security_score ) ); | |
| 3225 | + ?> | |
| 3226 | + <div class="vigilante-status-card vigilante-configuration-card"> | |
| 3227 | + <h2><?php esc_html_e( 'Vigilant configuration', 'vigilante' ); ?></h2> | |
| 3228 | + <p class="vigilante-score-kind description"> | |
| 3229 | + <?php esc_html_e( 'How much of what Vigilant offers is switched on here.', 'vigilante' ); ?> | |
| 3230 | + </p> | |
| 3231 | + <div class="vigilante-configuration-level"> | |
| 3232 | + <div class="vigilante-configuration-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" | |
| 3233 | + aria-valuenow="<?php echo esc_attr( $level ); ?>" | |
| 3234 | + aria-label="<?php esc_attr_e( 'Vigilant configuration', 'vigilante' ); ?>"> | |
| 3235 | + <span class="vigilante-configuration-bar-fill" style="width: <?php echo esc_attr( $level ); ?>%;"></span> | |
| 3236 | + </div> | |
| 3237 | + <strong class="vigilante-configuration-percent"><?php echo esc_html( $level ); ?>%</strong> | |
| 3238 | + <div class="vigilante-config-status"> | |
| 3239 | + <?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?> | |
| 3240 | + <span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>"> | |
| 3241 | + <?php echo esc_html( $presets[ $active_preset ]['name'] ); ?> | |
| 3242 | + </span> | |
| 3243 | + <?php else : ?> | |
| 3244 | + <span class="vigilante-preset-badge vigilante-preset-custom"> | |
| 3245 | + <?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?> | |
| 3246 | + </span> | |
| 3247 | + <?php endif; ?> | |
| 3248 | + </div> | |
| 3249 | + </div> | |
| 3250 | + <p class="vigilante-configuration-modules description"> | |
| 3251 | + <?php | |
| 3252 | + echo esc_html( | |
| 3253 | + sprintf( | |
| 3254 | + /* translators: 1: number of modules switched on, 2: number of modules */ | |
| 3255 | + __( '%1$d of %2$d modules switched on.', 'vigilante' ), | |
| 3256 | + $modules_on, | |
| 3257 | + count( $modules ) | |
| 3258 | + ) | |
| 3259 | + ); | |
| 3260 | + ?> | |
| 3261 | + </p> | |
| 3262 | + | |
| 3263 | + <?php | |
| 3264 | + $recommendations = $this->get_security_recommendations( $options ); | |
| 3265 | + if ( ! empty( $recommendations ) ) : | |
| 3266 | + ?> | |
| 3267 | + <div class="vigilante-recommendations"> | |
| 3268 | + <h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4> | |
| 3269 | + <ul class="vigilante-recommendations-grid"> | |
| 3270 | + <?php foreach ( $recommendations as $rec ) : ?> | |
| 3271 | + <li> | |
| 3272 | + <span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span> | |
| 3273 | + <?php echo esc_html( $rec['message'] ); ?> | |
| 3274 | + <?php if ( ! empty( $rec['tab'] ) ) : ?> | |
| 3275 | + <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $rec['tab'] ) ); ?>" class="vigilante-rec-link" title="<?php esc_attr_e( 'Go to settings', 'vigilante' ); ?>"><span class="dashicons dashicons-arrow-right-alt2"></span></a> | |
| 3276 | + <?php endif; ?> | |
| 3277 | + </li> | |
| 3278 | + <?php endforeach; ?> | |
| 3279 | + </ul> | |
| 3280 | + </div> | |
| 3281 | + <?php endif; ?> | |
| 3282 | + </div> | |
| 3283 | + <?php | |
| 3284 | + } | |
| 3285 | + | |
| 3286 | + /** | |
| 3109 | 3287 | * One line strip at the top of the Dashboard tab: the state of Vigilant own |
| 3110 | - * files, above the Configuration Score and the Security Check, because | |
| 3288 | + * files, above the Security Check and the configuration card, because | |
| 3111 | 3289 | * nothing else on this screen means much while it is red. |
| 3112 | 3290 | */ |
| 3113 | 3291 | private function render_self_protection_strip() { |
| 3114 | 3292 | $summary = $this->self_integrity_summary(); |
| @@ -3176,67 +3354,11 @@ | ||
| 3176 | 3354 | $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array(); |
| 3177 | 3355 | ?> |
| 3178 | 3356 | <div class="vigilante-dashboard"> |
| 3179 | 3357 | <?php $this->render_self_protection_strip(); ?> |
| 3180 | - <div class="vigilante-status-card"> | |
| 3181 | - <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2> | |
| 3182 | - <p class="vigilante-score-kind description"> | |
| 3183 | - <?php esc_html_e( 'How well Vigilante is configured right now. Pair it with the Security Check below to see the real-world result.', 'vigilante' ); ?> | |
| 3184 | - </p> | |
| 3185 | - <div class="vigilante-security-score"> | |
| 3186 | - <?php | |
| 3187 | - // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29) | |
| 3188 | - if ( $security_score >= 90 ) { | |
| 3189 | - $grade = 'A'; | |
| 3190 | - } elseif ( $security_score >= 70 ) { | |
| 3191 | - $grade = 'B'; | |
| 3192 | - } elseif ( $security_score >= 50 ) { | |
| 3193 | - $grade = 'C'; | |
| 3194 | - } elseif ( $security_score >= 30 ) { | |
| 3195 | - $grade = 'D'; | |
| 3196 | - } else { | |
| 3197 | - $grade = 'E'; | |
| 3198 | - } | |
| 3199 | - ?> | |
| 3200 | - <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>"> | |
| 3201 | - <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span> | |
| 3202 | - <span class="vigilante-score-text"><?php echo esc_html( $security_score ); ?>%</span> | |
| 3203 | - </div> | |
| 3204 | - <div class="vigilante-config-status"> | |
| 3205 | - <?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?> | |
| 3206 | - <span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>"> | |
| 3207 | - <?php echo esc_html( $presets[ $active_preset ]['name'] ); ?> | |
| 3208 | - </span> | |
| 3209 | - <?php else : ?> | |
| 3210 | - <span class="vigilante-preset-badge vigilante-preset-custom"> | |
| 3211 | - <?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?> | |
| 3212 | - </span> | |
| 3213 | - <?php endif; ?> | |
| 3214 | - </div> | |
| 3215 | - </div> | |
| 3216 | - | |
| 3217 | - <?php | |
| 3218 | - $recommendations = $this->get_security_recommendations( $options ); | |
| 3219 | - if ( ! empty( $recommendations ) ) : | |
| 3220 | - ?> | |
| 3221 | - <div class="vigilante-recommendations"> | |
| 3222 | - <h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4> | |
| 3223 | - <ul class="vigilante-recommendations-grid"> | |
| 3224 | - <?php foreach ( $recommendations as $rec ) : ?> | |
| 3225 | - <li> | |
| 3226 | - <span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span> | |
| 3227 | - <?php echo esc_html( $rec['message'] ); ?> | |
| 3228 | - <?php if ( ! empty( $rec['tab'] ) ) : ?> | |
| 3229 | - <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $rec['tab'] ) ); ?>" class="vigilante-rec-link" title="<?php esc_attr_e( 'Go to settings', 'vigilante' ); ?>"><span class="dashicons dashicons-arrow-right-alt2"></span></a> | |
| 3230 | - <?php endif; ?> | |
| 3231 | - </li> | |
| 3232 | - <?php endforeach; ?> | |
| 3233 | - </ul> | |
| 3234 | - </div> | |
| 3235 | - <?php endif; ?> | |
| 3236 | - </div> | |
| 3358 | + <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?> | |
| 3237 | 3359 | |
| 3238 | - <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?> | |
| 3360 | + <?php $this->render_configuration_card( $security_score, $options, $active_preset, $presets ); ?> | |
| 3239 | 3361 | |
| 3240 | 3362 | <div class="vigilante-modules-grid"> |
| 3241 | 3363 | <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2> |
| 3242 | 3364 | <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p> |
| @@ -3721,8 +3843,9 @@ | ||
| 3721 | 3843 | <label> |
| 3722 | 3844 | <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>> |
| 3723 | 3845 | <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?> |
| 3724 | 3846 | </label> |
| 3847 | + <p class="description"><?php esc_html_e( 'The list also stops SEO crawlers such as Ahrefs, Semrush, Majestic and Screaming Frog, and command line clients such as curl, wget and python-requests. To let one of them through, add its name to the User-Agent Whitelist below. On Apache part of the list is applied in .htaccess, before WordPress loads, so those requests get a 403 and do not appear in the Security Audit.', 'vigilante' ); ?></p> | |
| 3725 | 3848 | </td> |
| 3726 | 3849 | </tr> |
| 3727 | 3850 | </table> |
| 3728 | 3851 | |
| @@ -4035,9 +4158,9 @@ | ||
| 4035 | 4158 | <th scope="row"><?php esc_html_e( 'Limit HTTP Methods', 'vigilante' ); ?></th> |
| 4036 | 4159 | <td> |
| 4037 | 4160 | <label> |
| 4038 | 4161 | <input type="checkbox" name="firewall[limit_http_methods]" value="1" <?php checked( ! empty( $options['limit_http_methods'] ) ); ?>> |
| 4039 | - <?php esc_html_e( 'Allow only GET, POST, HEAD (blocks PUT, DELETE, TRACE, etc.)', 'vigilante' ); ?> | |
| 4162 | + <?php esc_html_e( 'Block the HTTP methods WordPress does not use (TRACE, TRACK, CONNECT and any unknown one). GET, POST, HEAD, OPTIONS, PUT, PATCH and DELETE keep working.', 'vigilante' ); ?> | |
| 4040 | 4163 | </label> |
| 4041 | 4164 | </td> |
| 4042 | 4165 | </tr> |
| 4043 | 4166 | </table> |
| @@ -4773,8 +4896,9 @@ | ||
| 4773 | 4896 | <label> |
| 4774 | 4897 | <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>> |
| 4775 | 4898 | <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?> |
| 4776 | 4899 | </label> |
| 4900 | + <p class="description"><?php esc_html_e( 'The default policy includes upgrade-insecure-requests, which asks browsers to request every http:// resource over https. Vigilant writes it only when the site address starts with https://, because on a site served over http it would stop every stylesheet, script and image from loading. If your site is served over https through a proxy or CDN that terminates TLS, set the site address to https:// in Settings > General; the rules follow the next time this tab is saved or Vigilant is updated.', 'vigilante' ); ?></p> | |
| 4777 | 4901 | </td> |
| 4778 | 4902 | </tr> |
| 4779 | 4903 | <tr> |
| 4780 | 4904 | <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th> |
| @@ -6317,9 +6441,9 @@ | ||
| 6317 | 6441 | <select id="vigilante-f-audit-alerts-immediate-min-severity" name="audit_alerts[immediate][min_severity]"> |
| 6318 | 6442 | <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option> |
| 6319 | 6443 | <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option> |
| 6320 | 6444 | </select> |
| 6321 | - <p class="description"><?php esc_html_e( 'A new administrator, a closed plugin or a privilege escalation are all logged as Critical, so "Critical only" already covers them.', 'vigilante' ); ?></p> | |
| 6445 | + <p class="description"><?php esc_html_e( 'A closed plugin is logged as Critical, so "Critical only" covers it. A new administrator and a change of role are logged as Warning: they have their own emails, in Users > Admin monitoring, and reach this alert only with "Warning and Critical".', 'vigilante' ); ?></p> | |
| 6322 | 6446 | </td> |
| 6323 | 6447 | </tr> |
| 6324 | 6448 | <tr id="field-audit-alerts-threshold"> |
| 6325 | 6449 | <th scope="row"><?php esc_html_e( 'Threshold alerts', 'vigilante' ); ?></th> |
| @@ -6412,8 +6536,9 @@ | ||
| 6412 | 6536 | |
| 6413 | 6537 | // Label maps for translated display |
| 6414 | 6538 | $type_labels = array( |
| 6415 | 6539 | 'login' => __( 'Login', 'vigilante' ), |
| 6540 | + '2fa' => __( 'Two-factor', 'vigilante' ), | |
| 6416 | 6541 | 'user' => __( 'User', 'vigilante' ), |
| 6417 | 6542 | 'content' => __( 'Content', 'vigilante' ), |
| 6418 | 6543 | 'plugin' => __( 'Plugin', 'vigilante' ), |
| 6419 | 6544 | 'theme' => __( 'Theme', 'vigilante' ), |
| @@ -6442,8 +6567,9 @@ | ||
| 6442 | 6567 | <input type="text" id="vigilante-log-search" aria-label="<?php esc_attr_e( 'Search the activity log', 'vigilante' ); ?>" size="1" placeholder="<?php esc_attr_e( 'Search logs (min. 3 characters)...', 'vigilante' ); ?>" class="vigilante-log-search-input"> |
| 6443 | 6568 | <select id="vigilante-log-type-filter" aria-label="<?php esc_attr_e( 'Filter the log by event type', 'vigilante' ); ?>"> |
| 6444 | 6569 | <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option> |
| 6445 | 6570 | <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option> |
| 6571 | + <option value="2fa"><?php esc_html_e( 'Two-factor', 'vigilante' ); ?></option> | |
| 6446 | 6572 | <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option> |
| 6447 | 6573 | <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option> |
| 6448 | 6574 | <option value="plugin"><?php esc_html_e( 'Plugin', 'vigilante' ); ?></option> |
| 6449 | 6575 | <option value="theme"><?php esc_html_e( 'Theme', 'vigilante' ); ?></option> |
| @@ -6669,8 +6795,15 @@ | ||
| 6669 | 6795 | $files = isset( $state['files_checked'] ) ? (int) $state['files_checked'] : 0; |
| 6670 | 6796 | $anchors = ( isset( $state['anchors'] ) && is_array( $state['anchors'] ) ) ? $state['anchors'] : array(); |
| 6671 | 6797 | switch ( $tone ) { |
| 6672 | 6798 | case 'critical': |
| 6799 | + $kind = Vigilante_Self_Integrity::alarm_kind( $state, Vigilante_Self_Integrity::is_on() ); | |
| 6800 | + if ( 'tasks' === $kind ) { | |
| 6801 | + return __( 'Vigilant scheduled tasks keep being removed', 'vigilante' ); | |
| 6802 | + } | |
| 6803 | + if ( 'hooks' === $kind ) { | |
| 6804 | + return __( 'Something removed the hooks of Vigilant self-protection', 'vigilante' ); | |
| 6805 | + } | |
| 6673 | 6806 | return __( 'Changes detected in Vigilant own files', 'vigilante' ); |
| 6674 | 6807 | case 'warning': |
| 6675 | 6808 | return ( $files < 1 ) |
| 6676 | 6809 | ? __( 'Vigilant could not check its own files', 'vigilante' ) |
| @@ -6721,14 +6854,31 @@ | ||
| 6721 | 6854 | } |
| 6722 | 6855 | $enabled = Vigilante_Self_Integrity::is_on(); |
| 6723 | 6856 | $state = Vigilante_Self_Integrity::display_state(); |
| 6724 | 6857 | $findings = Vigilante_Self_Integrity::state_findings( $state, $enabled ); |
| 6858 | + $groups = $this->self_findings_by_case( $findings ); | |
| 6859 | + // Whether any case on screen is one a clean copy of the plugin fixes, | |
| 6860 | + // which is what the guidance marks as repair: the notice only sends | |
| 6861 | + // to whoever can repair then. Tasks, removed hooks, a filter and a | |
| 6862 | + // check that has not run for days are not. | |
| 6863 | + $about_files = false; | |
| 6864 | + foreach ( $groups as $group ) { | |
| 6865 | + if ( ! empty( $group['repair'] ) ) { | |
| 6866 | + $about_files = true; | |
| 6867 | + break; | |
| 6868 | + } | |
| 6869 | + } | |
| 6725 | 6870 | $summary = array( |
| 6726 | - 'state' => $state, | |
| 6727 | - 'findings' => $findings, | |
| 6728 | - 'groups' => $this->self_findings_by_case( $findings ), | |
| 6729 | - 'tone' => Vigilante_Self_Integrity::tone( $state, $enabled ), | |
| 6730 | - 'enabled' => $enabled, | |
| 6871 | + 'state' => $state, | |
| 6872 | + 'findings' => $findings, | |
| 6873 | + 'groups' => $groups, | |
| 6874 | + 'about_files' => $about_files, | |
| 6875 | + 'tone' => Vigilante_Self_Integrity::tone( $state, $enabled ), | |
| 6876 | + 'enabled' => $enabled, | |
| 6877 | + // What the alarm is about (files, off, hooks or tasks), so no | |
| 6878 | + // text says that files changed when that is not what was found. | |
| 6879 | + 'kind' => Vigilante_Self_Integrity::alarm_kind( $state, $enabled ), | |
| 6880 | + 'tasks' => Vigilante_Self_Integrity::alarm_is_about_tasks( $state, $enabled ), | |
| 6731 | 6881 | ); |
| 6732 | 6882 | $cached = $summary; |
| 6733 | 6883 | return $summary; |
| 6734 | 6884 | } |
| @@ -6795,14 +6945,42 @@ | ||
| 6795 | 6945 | |
| 6796 | 6946 | <div class="vigilante-self-cases"> |
| 6797 | 6947 | <?php |
| 6798 | 6948 | $first = true; |
| 6949 | + /* | |
| 6950 | + * Reinstalling fixes files, not a filter someone wrote, a hook | |
| 6951 | + * someone removed or a scheduled task that went missing: the | |
| 6952 | + * button only appears when at least one of the cases on screen | |
| 6953 | + * is one a clean copy solves. Offering it next to "switched off | |
| 6954 | + * by code" was telling the person to fix something else. And | |
| 6955 | + * when every case is about scheduled tasks there is nothing to | |
| 6956 | + * repair, so the note about repairing by hand stays out too. | |
| 6957 | + */ | |
| 6958 | + $vg_offer_repair = false; | |
| 6959 | + $vg_scan_helps = false; | |
| 6799 | 6960 | foreach ( $groups as $group ) { |
| 6800 | - $this->render_self_case( $group, $group['files'], $group['severity'], $first ); | |
| 6961 | + $vg_is_task = ( 0 === strpos( (string) $group['key'], 'cron_' ) ); | |
| 6962 | + $vg_unit = 'file'; | |
| 6963 | + if ( $vg_is_task ) { | |
| 6964 | + $vg_unit = 'task'; | |
| 6965 | + } elseif ( 'hooks_removed' === $group['key'] ) { | |
| 6966 | + $vg_unit = 'hook'; | |
| 6967 | + } | |
| 6968 | + $this->render_self_case( $group, $group['files'], $group['severity'], $first, $vg_unit ); | |
| 6801 | 6969 | $first = false; |
| 6970 | + if ( ! empty( $group['repair'] ) ) { | |
| 6971 | + $vg_offer_repair = true; | |
| 6972 | + } | |
| 6973 | + // A scan checks files: it clears nothing about a task, which | |
| 6974 | + // closes on its own, nor about removed hooks, which the next | |
| 6975 | + // admin page looks for again. | |
| 6976 | + if ( ! $vg_is_task && 'hooks_removed' !== $group['key'] ) { | |
| 6977 | + $vg_scan_helps = true; | |
| 6978 | + } | |
| 6802 | 6979 | } |
| 6803 | 6980 | |
| 6804 | - if ( in_array( $tone, array( 'critical', 'warning' ), true ) ) { | |
| 6981 | + // How to replace the files by hand is for a case a clean copy fixes. | |
| 6982 | + if ( $vg_offer_repair && in_array( $tone, array( 'critical', 'warning' ), true ) ) { | |
| 6805 | 6983 | $can_repair = class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair(); |
| 6806 | 6984 | if ( ! $can_repair ) { |
| 6807 | 6985 | if ( class_exists( 'Vigilante_Self_Repair' ) && ! Vigilante_Self_Repair::folder_is_the_distributed_one() ) { |
| 6808 | 6986 | $vg_reason = 'renamed'; |
| @@ -6830,9 +7008,10 @@ | ||
| 6830 | 7008 | } |
| 6831 | 7009 | ?> |
| 6832 | 7010 | </div> |
| 6833 | 7011 | |
| 6834 | - <?php if ( $total > count( $findings ) ) : ?> | |
| 7012 | + <?php // Switched off, the box only holds that case: the stored total is of the last check that ran. ?> | |
| 7013 | + <?php if ( $enabled && $total > count( $findings ) ) : ?> | |
| 6835 | 7014 | <p class="description"> |
| 6836 | 7015 | <?php |
| 6837 | 7016 | printf( |
| 6838 | 7017 | /* translators: 1: findings shown, 2: findings found in total */ |
| @@ -6843,24 +7022,8 @@ | ||
| 6843 | 7022 | ?> |
| 6844 | 7023 | </p> |
| 6845 | 7024 | <?php endif; ?> |
| 6846 | 7025 | |
| 6847 | - <?php | |
| 6848 | - /* | |
| 6849 | - * Reinstalling fixes files, not a filter someone wrote or a hook | |
| 6850 | - * someone removed: the button only appears when at least one of the | |
| 6851 | - * cases on screen is one a clean copy solves. Offering it next to | |
| 6852 | - * "switched off by code" was telling the person to fix something | |
| 6853 | - * else. | |
| 6854 | - */ | |
| 6855 | - $vg_offer_repair = false; | |
| 6856 | - foreach ( $groups as $vg_group ) { | |
| 6857 | - if ( ! empty( $vg_group['repair'] ) ) { | |
| 6858 | - $vg_offer_repair = true; | |
| 6859 | - break; | |
| 6860 | - } | |
| 6861 | - } | |
| 6862 | - ?> | |
| 6863 | 7026 | <p class="vigilante-self-actions"> |
| 6864 | 7027 | <?php if ( $vg_offer_repair && class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair() ) : ?> |
| 6865 | 7028 | <a class="button button-primary" href="<?php echo esc_url( Vigilante_Self_Repair::action_url() ); ?>"> |
| 6866 | 7029 | <?php esc_html_e( 'Repair Vigilant', 'vigilante' ); ?> |
| @@ -6867,9 +7030,9 @@ | ||
| 6867 | 7030 | </a> |
| 6868 | 7031 | <span class="description"> |
| 6869 | 7032 | <?php esc_html_e( 'It downloads a clean copy from WordPress.org, asks before changing anything, and keeps your settings and log.', 'vigilante' ); ?> |
| 6870 | 7033 | </span> |
| 6871 | - <?php else : ?> | |
| 7034 | + <?php elseif ( $vg_scan_helps ) : ?> | |
| 6872 | 7035 | <span class="description"> |
| 6873 | 7036 | <?php esc_html_e( 'After fixing it, run a new scan with the Run Scan Now button above.', 'vigilante' ); ?> |
| 6874 | 7037 | </span> |
| 6875 | 7038 | <?php endif; ?> |
| @@ -6926,10 +7089,13 @@ | ||
| 6926 | 7089 | * @param array $guidance Guidance entry (title, meaning, steps). |
| 6927 | 7090 | * @param array $files Paths this case was found in. |
| 6928 | 7091 | * @param string $severity critical|warning, empty for the how-to-repair row. |
| 6929 | 7092 | * @param bool $open Whether the row starts open. |
| 7093 | + * @param string $unit What is being counted, file, task or hook: a | |
| 7094 | + * scheduled task and a removed hook were counted | |
| 7095 | + * as "1 file" until 3.0.4. | |
| 6930 | 7096 | */ |
| 6931 | - private function render_self_case( $guidance, $files = array(), $severity = '', $open = false ) { | |
| 7097 | + private function render_self_case( $guidance, $files = array(), $severity = '', $open = false, $unit = 'file' ) { | |
| 6932 | 7098 | $shown = array_slice( (array) $files, 0, 20 ); |
| 6933 | 7099 | $hidden = count( (array) $files ) - count( $shown ); |
| 6934 | 7100 | ?> |
| 6935 | 7101 | <details class="vigilante-self-case vigilante-self-case--<?php echo esc_attr( '' !== $severity ? $severity : 'plain' ); ?>"<?php echo $open ? ' open' : ''; ?>> |
| @@ -6937,13 +7103,27 @@ | ||
| 6937 | 7103 | <span class="vigilante-self-case-title"><?php echo esc_html( $guidance['title'] ); ?></span> |
| 6938 | 7104 | <?php if ( ! empty( $files ) ) : ?> |
| 6939 | 7105 | <span class="vigilante-self-case-count"> |
| 6940 | 7106 | <?php |
| 6941 | - printf( | |
| 6942 | - /* translators: %d: number of files of this finding */ | |
| 6943 | - esc_html( _n( '%d file', '%d files', count( (array) $files ), 'vigilante' ) ), | |
| 6944 | - (int) count( (array) $files ) | |
| 6945 | - ); | |
| 7107 | + if ( 'task' === $unit ) { | |
| 7108 | + printf( | |
| 7109 | + /* translators: %d: number of scheduled tasks of this finding */ | |
| 7110 | + esc_html( _n( '%d task', '%d tasks', count( (array) $files ), 'vigilante' ) ), | |
| 7111 | + (int) count( (array) $files ) | |
| 7112 | + ); | |
| 7113 | + } elseif ( 'hook' === $unit ) { | |
| 7114 | + printf( | |
| 7115 | + /* translators: %d: number of removed hooks of this finding */ | |
| 7116 | + esc_html( _n( '%d hook', '%d hooks', count( (array) $files ), 'vigilante' ) ), | |
| 7117 | + (int) count( (array) $files ) | |
| 7118 | + ); | |
| 7119 | + } else { | |
| 7120 | + printf( | |
| 7121 | + /* translators: %d: number of files of this finding */ | |
| 7122 | + esc_html( _n( '%d file', '%d files', count( (array) $files ), 'vigilante' ) ), | |
| 7123 | + (int) count( (array) $files ) | |
| 7124 | + ); | |
| 7125 | + } | |
| 6946 | 7126 | ?> |
| 6947 | 7127 | </span> |
| 6948 | 7128 | <?php endif; ?> |
| 6949 | 7129 | <?php if ( '' !== $severity ) : ?> |
| @@ -6987,8 +7167,49 @@ | ||
| 6987 | 7167 | </details> |
| 6988 | 7168 | <?php |
| 6989 | 7169 | } |
| 6990 | 7170 | |
| 7171 | + /** | |
| 7172 | + * Print what each kind of finding in a list usually is and what to do | |
| 7173 | + * | |
| 7174 | + * One line per kind actually present in the list, from the single source | |
| 7175 | + * of that text, Vigilante_File_Integrity::finding_guidance(). | |
| 7176 | + * | |
| 7177 | + * @param array $items Findings of one section of the last scan. | |
| 7178 | + */ | |
| 7179 | + private function render_fi_guidance( $items ) { | |
| 7180 | + $seen = array(); | |
| 7181 | + $guides = array(); | |
| 7182 | + | |
| 7183 | + foreach ( (array) $items as $item ) { | |
| 7184 | + $type = is_array( $item ) ? (string) ( $item['type'] ?? '' ) : ''; | |
| 7185 | + | |
| 7186 | + if ( '' === $type || 'vigilante_self' === $type || isset( $seen[ $type ] ) ) { | |
| 7187 | + continue; | |
| 7188 | + } | |
| 7189 | + $seen[ $type ] = true; | |
| 7190 | + | |
| 7191 | + $guide = Vigilante_File_Integrity::finding_guidance( $type ); | |
| 7192 | + | |
| 7193 | + if ( '' !== $guide['advice'] ) { | |
| 7194 | + // A plugin and a theme share their text: one line for both, | |
| 7195 | + // under the two names. | |
| 7196 | + $guides[ $guide['advice'] ][] = $guide['label']; | |
| 7197 | + } | |
| 7198 | + } | |
| 7199 | + | |
| 7200 | + if ( empty( $guides ) ) { | |
| 7201 | + return; | |
| 7202 | + } | |
| 7203 | + ?> | |
| 7204 | + <ul class="vigilante-fi-guidance" style="margin: 8px 0 12px 18px; list-style: disc;"> | |
| 7205 | + <?php foreach ( $guides as $advice => $labels ) : ?> | |
| 7206 | + <li><strong><?php echo esc_html( implode( ', ', $labels ) ); ?>:</strong> <?php echo esc_html( $advice ); ?></li> | |
| 7207 | + <?php endforeach; ?> | |
| 7208 | + </ul> | |
| 7209 | + <?php | |
| 7210 | + } | |
| 7211 | + | |
| 6991 | 7212 | private function render_tab_file_integrity() { |
| 6992 | 7213 | $is_disabled = $this->render_module_disabled_notice( 'file_integrity' ); |
| 6993 | 7214 | $options = $this->settings->get_section( 'file_integrity' ); |
| 6994 | 7215 | // On the main site of a network the critical-file scan is the network's |
| @@ -7060,9 +7281,10 @@ | ||
| 7060 | 7281 | <h2> |
| 7061 | 7282 | <?php esc_html_e( 'File Integrity Monitoring', 'vigilante' ); ?> |
| 7062 | 7283 | <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 7063 | 7284 | </h2> |
| 7064 | - <p><?php esc_html_e( 'Detects file modifications using WordPress.org checksums.', 'vigilante' ); ?></p> | |
| 7285 | + <p><?php esc_html_e( 'Compares WordPress, and the plugins and themes that come from WordPress.org, with the official copy of each file, looks for code where there should be none (uploads and the site root) and watches wp-config.php and .htaccess for changes.', 'vigilante' ); ?></p> | |
| 7286 | + <p class="description"><?php esc_html_e( 'What it cannot compare: plugins and themes that are not on WordPress.org, must-use plugins, drop-ins (advanced-cache.php, object-cache.php and the like) and the other folders of wp-content have no official copy. They are searched for hidden code, which finds malware that disguises itself and misses most code written in the open. The results name what could not be compared.', 'vigilante' ); ?></p> | |
| 7065 | 7287 | |
| 7066 | 7288 | <table class="form-table"> |
| 7067 | 7289 | <tr> |
| 7068 | 7290 | <th scope="row"><?php esc_html_e( 'Automatic Scans', 'vigilante' ); ?></th> |
| @@ -7085,13 +7307,13 @@ | ||
| 7085 | 7307 | <tr> |
| 7086 | 7308 | <th scope="row"><label for="vigilante-f-file-integrity-notify-level"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></label></th> |
| 7087 | 7309 | <td> |
| 7088 | 7310 | <select id="vigilante-f-file-integrity-notify-level" name="file_integrity[notify_level]"> |
| 7089 | - <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option> | |
| 7090 | - <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option> | |
| 7311 | + <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'Every finding, modified files included', 'vigilante' ); ?></option> | |
| 7312 | + <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Serious findings only', 'vigilante' ); ?></option> | |
| 7091 | 7313 | <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option> |
| 7092 | 7314 | </select> |
| 7093 | - <p class="description"><?php esc_html_e( '"Suspicious files only" reduces noise by skipping modified file notifications. Recommended for most sites.', 'vigilante' ); ?></p> | |
| 7315 | + <p class="description"><?php esc_html_e( 'Serious findings are suspicious and extra files, changes in wp-config.php or .htaccess and closed plugins. That is the recommended choice: a file that is only modified is rarely more than an update that did not finish. The email goes out when a scan finds something new: a finding you were already told about is not sent again, unless the file changes once more.', 'vigilante' ); ?></p> | |
| 7094 | 7316 | </td> |
| 7095 | 7317 | </tr> |
| 7096 | 7318 | <tr> |
| 7097 | 7319 | <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th> |
| @@ -7097,11 +7319,11 @@ | ||
| 7097 | 7319 | <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th> |
| 7098 | 7320 | <td> |
| 7099 | 7321 | <label> |
| 7100 | 7322 | <input type="checkbox" name="file_integrity[instant_alert]" value="1" <?php checked( ! empty( $options['instant_alert'] ) ); ?>> |
| 7101 | - <?php esc_html_e( 'Send immediate alert when modified, suspicious or additional files are detected, or when a closed plugin is found', 'vigilante' ); ?> | |
| 7323 | + <?php esc_html_e( 'Email every finding when a scan ends, modified files included, and email a closed plugin as soon as the daily check finds it', 'vigilante' ); ?> | |
| 7102 | 7324 | </label> |
| 7103 | - <p class="description"><?php esc_html_e( 'Fires even if the Email Notifications setting above is set to Disabled.', 'vigilante' ); ?></p> | |
| 7325 | + <p class="description"><?php esc_html_e( 'This takes precedence over the choice above, Disabled included.', 'vigilante' ); ?></p> | |
| 7104 | 7326 | <p class="description"> |
| 7105 | 7327 | <?php |
| 7106 | 7328 | printf( |
| 7107 | 7329 | /* translators: %s: Link to notification settings */ |
| @@ -7127,29 +7349,29 @@ | ||
| 7127 | 7349 | <td> |
| 7128 | 7350 | <fieldset> |
| 7129 | 7351 | <label> |
| 7130 | 7352 | <input type="checkbox" name="file_integrity[scan_core]" value="1" <?php checked( $options['scan_core'] ?? true ); ?>> |
| 7131 | - <?php esc_html_e( 'Core files (compare against WordPress.org checksums)', 'vigilante' ); ?> | |
| 7353 | + <?php esc_html_e( 'WordPress core (compared with WordPress.org) and the other files in the site root', 'vigilante' ); ?> | |
| 7132 | 7354 | </label> |
| 7133 | 7355 | <br> |
| 7134 | 7356 | <label> |
| 7135 | 7357 | <input type="checkbox" name="file_integrity[scan_plugins]" value="1" <?php checked( $options['scan_plugins'] ?? true ); ?>> |
| 7136 | - <?php esc_html_e( 'Plugins (WordPress.org repository plugins)', 'vigilante' ); ?> | |
| 7358 | + <?php esc_html_e( 'Plugins (compared with WordPress.org; the rest, and the other folders of wp-content, are only searched for hidden code)', 'vigilante' ); ?> | |
| 7137 | 7359 | </label> |
| 7138 | 7360 | <br> |
| 7139 | 7361 | <label> |
| 7140 | 7362 | <input type="checkbox" name="file_integrity[scan_themes]" value="1" <?php checked( $options['scan_themes'] ?? true ); ?>> |
| 7141 | - <?php esc_html_e( 'Themes (WordPress.org repository themes)', 'vigilante' ); ?> | |
| 7363 | + <?php esc_html_e( 'Themes (compared with WordPress.org; the rest are only searched for hidden code)', 'vigilante' ); ?> | |
| 7142 | 7364 | </label> |
| 7143 | 7365 | <br> |
| 7144 | 7366 | <label> |
| 7145 | 7367 | <input type="checkbox" name="file_integrity[scan_uploads]" value="1" <?php checked( $options['scan_uploads'] ?? true ); ?>> |
| 7146 | - <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?> | |
| 7368 | + <?php esc_html_e( 'Uploads directory (PHP and other scripts, double extensions, .htaccess and PHP configuration files)', 'vigilante' ); ?> | |
| 7147 | 7369 | </label> |
| 7148 | 7370 | <br> |
| 7149 | 7371 | <label> |
| 7150 | 7372 | <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>> |
| 7151 | - <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?> | |
| 7373 | + <?php esc_html_e( 'wp-config.php and .htaccess (any change since you last approved them)', 'vigilante' ); ?> | |
| 7152 | 7374 | <?php if ( $vg_main_locked ) : ?> |
| 7153 | 7375 | <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span> |
| 7154 | 7376 | <?php endif; ?> |
| 7155 | 7377 | </label> |
| @@ -7163,10 +7385,10 @@ | ||
| 7163 | 7385 | </tr> |
| 7164 | 7386 | <tr> |
| 7165 | 7387 | <th scope="row"><label for="vigilante-f-file-integrity-excluded-paths"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></label></th> |
| 7166 | 7388 | <td> |
| 7167 | - <textarea id="vigilante-f-file-integrity-excluded-paths" name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/cache wp-content/languages"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea> | |
| 7168 | - <p class="description"><?php esc_html_e( 'One path per line, relative to the WordPress root. A path such as wp-content/cache excludes exactly that folder and everything under it. A name on its own, such as cache, excludes any folder called exactly that, wherever it is.', 'vigilante' ); ?></p> | |
| 7389 | + <textarea id="vigilante-f-file-integrity-excluded-paths" name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/backups"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea> | |
| 7390 | + <p class="description"><?php esc_html_e( 'One path per line, relative to the WordPress root. A path such as wp-content/backups excludes exactly that folder and everything under it. A name on its own, such as backups, excludes any folder called exactly that, wherever it is. Nothing in an excluded folder is looked at, so keep the list short.', 'vigilante' ); ?></p> | |
| 7169 | 7391 | </td> |
| 7170 | 7392 | </tr> |
| 7171 | 7393 | <tr> |
| 7172 | 7394 | <th scope="row"><label for="vigilante-f-file-integrity-excluded-extensions"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></label></th> |
| @@ -7209,11 +7431,49 @@ | ||
| 7209 | 7431 | <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div> |
| 7210 | 7432 | |
| 7211 | 7433 | <?php $this->render_self_protection_box( $options ); ?> |
| 7212 | 7434 | |
| 7435 | + <?php | |
| 7436 | + // Whole folders of code left out of every scan. A partial scan tells | |
| 7437 | + // how to check the rest by excluding what it did check, and those | |
| 7438 | + // lines have to come out afterwards: with them in place a scan comes | |
| 7439 | + // out complete and clean without having looked there. Said for as | |
| 7440 | + // long as they are in the list, whoever put them there and why. | |
| 7441 | + $vg_fi_tool = new Vigilante_File_Integrity( $this->settings, $this->database ); | |
| 7442 | + $vg_excluded_code = $vg_fi_tool->excluded_code_folders(); | |
| 7443 | + $vg_folders_to_skip = $vg_fi_tool->folders_to_exclude( $last_results ); | |
| 7444 | + ?> | |
| 7445 | + <?php if ( ! empty( $vg_excluded_code ) ) : ?> | |
| 7446 | + <div id="vigilante-fi-excluded-code" class="notice notice-warning inline"> | |
| 7447 | + <p> | |
| 7448 | + <strong> | |
| 7449 | + <?php | |
| 7450 | + printf( | |
| 7451 | + /* translators: %s: Comma separated list of folders */ | |
| 7452 | + esc_html__( 'Excluded Paths holds whole folders of plugins or themes, or the uploads folder: %s.', 'vigilante' ), | |
| 7453 | + esc_html( implode( ', ', $vg_excluded_code ) ) | |
| 7454 | + ); | |
| 7455 | + ?> | |
| 7456 | + </strong> | |
| 7457 | + <?php esc_html_e( 'No scan looks inside them, the scheduled one included, and a scan can come out complete and clean without having checked them. If you added them to check the rest of a large site, take them out once that scan is done.', 'vigilante' ); ?> | |
| 7458 | + </p> | |
| 7459 | + </div> | |
| 7460 | + <?php endif; ?> | |
| 7461 | + | |
| 7213 | 7462 | <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?> |
| 7214 | 7463 | <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section"> |
| 7215 | 7464 | <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2> |
| 7465 | + <?php | |
| 7466 | + // A scan the server stops halfway stores nothing, and neither | |
| 7467 | + // does one WordPress never starts: the tab kept showing the | |
| 7468 | + // last result that did finish, with nothing to say that scans | |
| 7469 | + // had stopped. Its date is the only sign. | |
| 7470 | + if ( $last_scan && $this->settings->is_module_enabled( 'file_integrity' ) && Vigilante_File_Integrity::is_scan_overdue( $last_scan, $options['scan_frequency'] ?? 'daily' ) ) : | |
| 7471 | + ?> | |
| 7472 | + <div class="notice notice-warning inline"> | |
| 7473 | + <p><strong><?php esc_html_e( 'Scans are scheduled, and none has finished since the date below.', 'vigilante' ); ?></strong> <?php esc_html_e( 'Either WordPress is not running its scheduled tasks, or the server stops the scan before it ends, and a scan that is stopped stores no result. What you see here is old. Run a scan now: if it finishes, the scheduled tasks are what is failing. If it does not, the site is too large for the time the server gives a request, and adding large folders that do not need checking to Excluded Paths leaves more time for the rest.', 'vigilante' ); ?></p> | |
| 7474 | + </div> | |
| 7475 | + <?php endif; ?> | |
| 7216 | 7476 | <?php if ( $last_scan ) : ?> |
| 7217 | 7477 | <p> |
| 7218 | 7478 | <?php |
| 7219 | 7479 | // Build the "X files scanned" hint inline with the date so it doesn't |
| @@ -7296,12 +7556,112 @@ | ||
| 7296 | 7556 | </div> |
| 7297 | 7557 | <?php endif; ?> |
| 7298 | 7558 | </div> |
| 7299 | 7559 | |
| 7560 | + <?php | |
| 7561 | + // A scan that could not finish, or could not get the official | |
| 7562 | + // copy to compare with, checked less than it seems. Until now | |
| 7563 | + // its errors were stored and never shown, and the tab could | |
| 7564 | + // say that every file had passed. | |
| 7565 | + $vg_scan_errors = ( $last_results && ! empty( $last_results['errors'] ) && is_array( $last_results['errors'] ) ) ? $last_results['errors'] : array(); | |
| 7566 | + $vg_scan_partial = ! empty( $vg_scan_errors ) || ( $last_results && ! empty( $last_results['incomplete'] ) ); | |
| 7567 | + | |
| 7568 | + // Files of the official copies that are not on the server. Core | |
| 7569 | + // files under wp-content are left out: results stored before | |
| 7570 | + // the bundled themes and plugins got their own turn list | |
| 7571 | + // there the ones people delete on purpose. | |
| 7572 | + $vg_missing_core = array(); | |
| 7573 | + if ( $last_results && ! empty( $last_results['missing'] ) && is_array( $last_results['missing'] ) ) { | |
| 7574 | + foreach ( $last_results['missing'] as $item ) { | |
| 7575 | + if ( ! is_array( $item ) || ! isset( $item['file'] ) ) { | |
| 7576 | + continue; | |
| 7577 | + } | |
| 7578 | + | |
| 7579 | + $vg_missing_type = (string) ( $item['type'] ?? '' ); | |
| 7580 | + | |
| 7581 | + if ( in_array( $vg_missing_type, array( 'plugin', 'theme' ), true ) | |
| 7582 | + || ( 'core' === $vg_missing_type && 0 !== strpos( (string) $item['file'], 'wp-content/' ) ) ) { | |
| 7583 | + $vg_missing_core[] = (string) $item['file']; | |
| 7584 | + } | |
| 7585 | + } | |
| 7586 | + } | |
| 7587 | + ?> | |
| 7588 | + | |
| 7589 | + <?php if ( $last_results && ! empty( $last_results['unverified'] ) && is_array( $last_results['unverified'] ) ) : ?> | |
| 7590 | + <p class="description"> | |
| 7591 | + <?php | |
| 7592 | + printf( | |
| 7593 | + /* translators: %s: Comma separated list of plugin and theme names */ | |
| 7594 | + esc_html__( 'No official copy to compare with, so only searched for hidden code: %s.', 'vigilante' ), | |
| 7595 | + esc_html( implode( ', ', array_unique( array_map( 'strval', $last_results['unverified'] ) ) ) ) | |
| 7596 | + ); | |
| 7597 | + ?> | |
| 7598 | + </p> | |
| 7599 | + <?php endif; ?> | |
| 7600 | + | |
| 7601 | + <?php | |
| 7602 | + // What the scan did not get to, by name. A scan starts from | |
| 7603 | + // the beginning every time, so on a site too large for it | |
| 7604 | + // these are the same on every scan, and whoever reads the | |
| 7605 | + // result has to know that nothing here was looked at. | |
| 7606 | + $vg_scan_notes = array_merge( $vg_scan_errors, Vigilante_File_Integrity::not_checked_lines( $last_results ) ); | |
| 7607 | + ?> | |
| 7608 | + <?php if ( $vg_scan_partial ) : ?> | |
| 7609 | + <div class="notice notice-warning inline"> | |
| 7610 | + <p><strong><?php esc_html_e( 'The last scan could not check everything, so its result is partial.', 'vigilante' ); ?></strong></p> | |
| 7611 | + <?php if ( ! empty( $vg_scan_notes ) ) : ?> | |
| 7612 | + <ul style="margin: 0 0 8px 18px; list-style: disc;"> | |
| 7613 | + <?php foreach ( $vg_scan_notes as $vg_scan_note ) : ?> | |
| 7614 | + <li><?php echo esc_html( (string) $vg_scan_note ); ?></li> | |
| 7615 | + <?php endforeach; ?> | |
| 7616 | + </ul> | |
| 7617 | + <?php endif; ?> | |
| 7618 | + <p> | |
| 7619 | + <?php | |
| 7620 | + printf( | |
| 7621 | + /* translators: %d: Seconds a scan runs for at most */ | |
| 7622 | + esc_html__( 'Run the scan again: a second scan is faster, because what it downloads from WordPress.org is kept for a while. If the result is still partial, either the server cannot reach WordPress.org or the site is larger than a scan can check in its %d seconds, and then every scan leaves the same part unchecked. Adding large folders that do not need checking to Excluded Paths leaves more time for the rest.', 'vigilante' ), | |
| 7623 | + (int) Vigilante_File_Integrity::MAX_SCAN_SECONDS | |
| 7624 | + ); | |
| 7625 | + ?> | |
| 7626 | + </p> | |
| 7627 | + <?php if ( ! empty( $vg_folders_to_skip ) ) : ?> | |
| 7628 | + <p><strong><?php esc_html_e( 'To check what was left out:', 'vigilante' ); ?></strong></p> | |
| 7629 | + <ol style="margin: 0 0 8px 18px;"> | |
| 7630 | + <li><?php esc_html_e( 'Deal first with what this scan found in the plugins and themes it did check: the next result will not list it. Then add the lines below, which are their folders, to Excluded Paths, higher up on this page, and save.', 'vigilante' ); ?></li> | |
| 7631 | + <li><?php esc_html_e( 'Press Run Scan Now. The scan spends its time on the rest. If it comes out partial again, repeat with the lines it gives then.', 'vigilante' ); ?></li> | |
| 7632 | + <li><?php esc_html_e( 'Take those lines out of Excluded Paths. While they are there no scan looks inside those folders, the scheduled one included.', 'vigilante' ); ?></li> | |
| 7633 | + </ol> | |
| 7634 | + <textarea id="vigilante-fi-folders-to-exclude" readonly rows="<?php echo (int) min( 12, max( 3, count( $vg_folders_to_skip ) ) ); ?>" class="large-text code" style="margin-bottom: 8px;"><?php echo esc_textarea( implode( "\n", $vg_folders_to_skip ) ); ?></textarea> | |
| 7635 | + <?php endif; ?> | |
| 7636 | + </div> | |
| 7637 | + <?php endif; ?> | |
| 7638 | + | |
| 7639 | + <?php if ( ! empty( $vg_missing_core ) ) : ?> | |
| 7640 | + <div class="vigilante-file-list"> | |
| 7641 | + <h3><?php esc_html_e( 'Missing Files', 'vigilante' ); ?></h3> | |
| 7642 | + <p class="description"><?php esc_html_e( 'These files are in the official copy and not on the server. Reinstall what they belong to (WordPress from Dashboard > Updates, a plugin or a theme from its own screen) and scan again.', 'vigilante' ); ?></p> | |
| 7643 | + <ul style="margin: 8px 0 12px 18px; list-style: disc;"> | |
| 7644 | + <?php foreach ( array_slice( $vg_missing_core, 0, 20 ) as $vg_missing_file ) : ?> | |
| 7645 | + <li><code><?php echo esc_html( $vg_missing_file ); ?></code></li> | |
| 7646 | + <?php endforeach; ?> | |
| 7647 | + </ul> | |
| 7648 | + <?php if ( count( $vg_missing_core ) > 20 ) : ?> | |
| 7649 | + <p class="description"> | |
| 7650 | + <?php | |
| 7651 | + /* translators: %d: Number of additional files */ | |
| 7652 | + printf( esc_html__( '... and %d more', 'vigilante' ), (int) ( count( $vg_missing_core ) - 20 ) ); | |
| 7653 | + ?> | |
| 7654 | + </p> | |
| 7655 | + <?php endif; ?> | |
| 7656 | + </div> | |
| 7657 | + <?php endif; ?> | |
| 7658 | + | |
| 7300 | 7659 | <?php if ( ! empty( $last_results['suspicious'] ) ) : ?> |
| 7301 | 7660 | <div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore"> |
| 7302 | 7661 | <h3 style="color: #d63638;"><?php esc_html_e( 'Suspicious Files', 'vigilante' ); ?></h3> |
| 7303 | - <p class="description" style="color: #d63638;"><?php esc_html_e( '⚠ Warning: These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ); ?></p> | |
| 7662 | + <p class="description" style="color: #d63638;"><?php esc_html_e( '⚠ Files that can run code where none is expected, or that contain code written the way malware hides itself. Real infections show up here, and so do harmless files that plugins generate. Check the path and the reason before acting.', 'vigilante' ); ?></p> | |
| 7663 | + <?php $this->render_fi_guidance( $last_results['suspicious'] ); ?> | |
| 7304 | 7664 | <div class="vigilante-fi-bulk-bar"> |
| 7305 | 7665 | <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 7306 | 7666 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 7307 | 7667 | </div> |
| @@ -7337,8 +7697,11 @@ | ||
| 7337 | 7697 | $file_path = (string) $item; |
| 7338 | 7698 | } |
| 7339 | 7699 | if ( 'vigilante_self' === $file_type ) { |
| 7340 | 7700 | $file_type = __( 'Vigilant (self)', 'vigilante' ); |
| 7701 | + } else { | |
| 7702 | + // A name a person can read, instead of the internal slug. | |
| 7703 | + $file_type = Vigilante_File_Integrity::finding_guidance( $file_type )['label']; | |
| 7341 | 7704 | } |
| 7342 | 7705 | ?> |
| 7343 | 7706 | <tr> |
| 7344 | 7707 | <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| @@ -7357,9 +7720,10 @@ | ||
| 7357 | 7720 | |
| 7358 | 7721 | <?php if ( ! empty( $last_results['extra'] ) ) : ?> |
| 7359 | 7722 | <div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore"> |
| 7360 | 7723 | <h3 style="color: #b32d2e;"><?php esc_html_e( 'Extra Files', 'vigilante' ); ?></h3> |
| 7361 | - <p class="description"><?php esc_html_e( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org. May be legitimate customizations or injected backdoors.', 'vigilante' ); ?></p> | |
| 7724 | + <p class="description"><?php esc_html_e( 'Files that are not part of WordPress, or of the plugin or theme they sit in. Many are legitimate, and an injected file looks the same from here, so confirm where each one comes from.', 'vigilante' ); ?></p> | |
| 7725 | + <?php $this->render_fi_guidance( $last_results['extra'] ); ?> | |
| 7362 | 7726 | <div class="vigilante-fi-bulk-bar"> |
| 7363 | 7727 | <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 7364 | 7728 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 7365 | 7729 | </div> |
| @@ -7381,8 +7745,11 @@ | ||
| 7381 | 7745 | $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' ); |
| 7382 | 7746 | $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown'; |
| 7383 | 7747 | if ( 'vigilante_self' === $file_type ) { |
| 7384 | 7748 | $file_type = __( 'Vigilant (self)', 'vigilante' ); |
| 7749 | + } else { | |
| 7750 | + // A name a person can read, instead of the internal slug. | |
| 7751 | + $file_type = Vigilante_File_Integrity::finding_guidance( $file_type )['label']; | |
| 7385 | 7752 | } |
| 7386 | 7753 | ?> |
| 7387 | 7754 | <tr> |
| 7388 | 7755 | <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| @@ -7431,9 +7798,9 @@ | ||
| 7431 | 7798 | <?php if ( ! empty( $critical_modified ) ) : ?> |
| 7432 | 7799 | <div class="vigilante-file-list vigilante-critical-config-files"> |
| 7433 | 7800 | <h3 style="color: #e36210;"><?php esc_html_e( 'Critical config files modified', 'vigilante' ); ?></h3> |
| 7434 | 7801 | <p class="description"> |
| 7435 | - <?php esc_html_e( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ); ?> | |
| 7802 | + <?php esc_html_e( 'These files changed since you last approved them. They are a common target for code injection, and they are also rewritten by cache, security and redirection plugins, often on every update. Review the changes and approve them if they belong to something you use: approving records the current content as the reference and does not touch the file. Vigilant\'s own blocks are excluded from this check, and Vigilant never restores these files by itself.', 'vigilante' ); ?> | |
| 7436 | 7803 | </p> |
| 7437 | 7804 | <table class="wp-list-table widefat fixed striped"> |
| 7438 | 7805 | <thead> |
| 7439 | 7806 | <tr> |
| @@ -7594,9 +7961,10 @@ | ||
| 7594 | 7961 | |
| 7595 | 7962 | <?php if ( ! empty( $regular_modified ) ) : ?> |
| 7596 | 7963 | <div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore"> |
| 7597 | 7964 | <h3><?php esc_html_e( 'Modified Files', 'vigilante' ); ?></h3> |
| 7598 | - <p class="description"><?php esc_html_e( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ); ?></p> | |
| 7965 | + <p class="description"><?php esc_html_e( 'These files do not match the copy WordPress.org publishes for the installed version.', 'vigilante' ); ?></p> | |
| 7966 | + <?php $this->render_fi_guidance( $regular_modified ); ?> | |
| 7599 | 7967 | <div class="vigilante-fi-bulk-bar"> |
| 7600 | 7968 | <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 7601 | 7969 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 7602 | 7970 | </div> |
| @@ -7627,8 +7995,11 @@ | ||
| 7627 | 7995 | $file_path = (string) $item; |
| 7628 | 7996 | } |
| 7629 | 7997 | if ( 'vigilante_self' === $file_type ) { |
| 7630 | 7998 | $file_type = __( 'Vigilant (self)', 'vigilante' ); |
| 7999 | + } else { | |
| 8000 | + // A name a person can read, instead of the internal slug. | |
| 8001 | + $file_type = Vigilante_File_Integrity::finding_guidance( $file_type )['label']; | |
| 7631 | 8002 | } |
| 7632 | 8003 | ?> |
| 7633 | 8004 | <tr> |
| 7634 | 8005 | <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| @@ -7650,11 +8021,11 @@ | ||
| 7650 | 8021 | // above, this line contradicted it (its findings no longer |
| 7651 | 8022 | // travel in the tables below). |
| 7652 | 8023 | $vg_self_alarm = in_array( $this->self_integrity_summary( $options )['tone'], array( 'critical', 'off', 'warning' ), true ); |
| 7653 | 8024 | ?> |
| 7654 | - <?php if ( $last_results && ! $vg_self_alarm && empty( $critical_modified ) && empty( $regular_modified ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?> | |
| 8025 | + <?php if ( $last_results && ! $vg_self_alarm && empty( $critical_modified ) && empty( $regular_modified ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed && ! $vg_scan_partial && empty( $vg_missing_core ) ) : ?> | |
| 7655 | 8026 | <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;"> |
| 7656 | - <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?> | |
| 8027 | + <?php esc_html_e( 'Good job! Nothing to report in the files that were checked.', 'vigilante' ); ?> | |
| 7657 | 8028 | </p> |
| 7658 | 8029 | <?php endif; ?> |
| 7659 | 8030 | <?php endif; ?> |
| 7660 | 8031 | </div> |
| @@ -7703,9 +8074,9 @@ | ||
| 7703 | 8074 | |
| 7704 | 8075 | <?php if ( ! empty( $ignored_files ) ) : ?> |
| 7705 | 8076 | <div id="vigilante-section-fi-ignored" class="vigilante-settings-section"> |
| 7706 | 8077 | <h2><?php esc_html_e( 'Ignored Files', 'vigilante' ); ?></h2> |
| 7707 | - <p class="description"><?php esc_html_e( 'These files are excluded from scan results and email notifications. They will still be scanned but any findings will be hidden.', 'vigilante' ); ?></p> | |
| 8078 | + <p class="description"><?php esc_html_e( 'These files are excluded from scan results and email notifications. They will still be scanned but any findings will be hidden. To leave a whole folder out, add it to Excluded Paths instead.', 'vigilante' ); ?></p> | |
| 7708 | 8079 | <div class="vigilante-paginated-section" data-bulk-mode="unignore"> |
| 7709 | 8080 | <div class="vigilante-fi-bulk-bar"> |
| 7710 | 8081 | <button type="button" class="button vigilante-bulk-unignore" disabled><?php esc_html_e( 'Stop ignoring selected', 'vigilante' ); ?></button> |
| 7711 | 8082 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| @@ -7794,9 +8165,9 @@ | ||
| 7794 | 8165 | } elseif ( isset( $_POST['data'] ) ) { |
| 7795 | 8166 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 7796 | 8167 | $raw_data = wp_unslash( $_POST['data'] ); |
| 7797 | 8168 | parse_str( $raw_data, $data ); |
| 7798 | - $data = map_deep( $data, 'sanitize_textarea_field' ); | |
| 8169 | + $data = $this->sanitize_posted_settings( $data ); | |
| 7799 | 8170 | } else { |
| 7800 | 8171 | $data = array(); |
| 7801 | 8172 | } |
| 7802 | 8173 | |
| @@ -8186,8 +8557,48 @@ | ||
| 8186 | 8557 | return array( 'sent' => $sent, 'failed' => $failed ); |
| 8187 | 8558 | } |
| 8188 | 8559 | |
| 8189 | 8560 | /** |
| 8561 | + * Sanitize the settings a tab posted, once the serialized form is parsed | |
| 8562 | + * | |
| 8563 | + * Every value goes through sanitize_textarea_field() except the lists of | |
| 8564 | + * file paths (Vigilante_Settings::path_list_keys()). That one deletes every | |
| 8565 | + * %XX octet and, when it finds one, collapses repeated spaces, so an | |
| 8566 | + * exclusion for a folder called foto%20x was saved as fotox and excluded | |
| 8567 | + * nothing (measured on 1 oct 2026 against 3.0.1). The paths are read as | |
| 8568 | + * paths before the generic sanitizer runs and put back after it. | |
| 8569 | + * | |
| 8570 | + * @since 3.0.2 | |
| 8571 | + * | |
| 8572 | + * @param array $data Parsed form: section => key => value. | |
| 8573 | + * @return array | |
| 8574 | + */ | |
| 8575 | + private function sanitize_posted_settings( $data ) { | |
| 8576 | + $paths = Vigilante_Settings::take_path_lists( $data ); | |
| 8577 | + $data = map_deep( $data, 'sanitize_textarea_field' ); | |
| 8578 | + | |
| 8579 | + return Vigilante_Settings::restore_path_lists( $data, $paths ); | |
| 8580 | + } | |
| 8581 | + | |
| 8582 | + /** | |
| 8583 | + * Sanitize an imported settings file, once it is decoded | |
| 8584 | + * | |
| 8585 | + * Same rule as sanitize_posted_settings(): the lists of paths keep what the | |
| 8586 | + * export wrote, so an exclusion survives an export and an import. | |
| 8587 | + * | |
| 8588 | + * @since 3.0.2 | |
| 8589 | + * | |
| 8590 | + * @param array $imported Decoded export. | |
| 8591 | + * @return array | |
| 8592 | + */ | |
| 8593 | + private function sanitize_imported_settings( $imported ) { | |
| 8594 | + $paths = Vigilante_Settings::take_path_lists( $imported ); | |
| 8595 | + $imported = map_deep( $imported, 'sanitize_text_field' ); | |
| 8596 | + | |
| 8597 | + return Vigilante_Settings::restore_path_lists( $imported, $paths ); | |
| 8598 | + } | |
| 8599 | + | |
| 8600 | + /** | |
| 8190 | 8601 | * Process section data maintaining proper types from defaults |
| 8191 | 8602 | * |
| 8192 | 8603 | * @param array $submitted_data Data submitted from form. |
| 8193 | 8604 | * @param array $defaults Default values for this section. |
| @@ -8514,9 +8925,9 @@ | ||
| 8514 | 8925 | wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) ); |
| 8515 | 8926 | } |
| 8516 | 8927 | |
| 8517 | 8928 | // Sanitize imported data recursively |
| 8518 | - $imported = map_deep( $imported, 'sanitize_text_field' ); | |
| 8929 | + $imported = $this->sanitize_imported_settings( $imported ); | |
| 8519 | 8930 | |
| 8520 | 8931 | // Validate structure: only sections and keys of the schema survive, and |
| 8521 | 8932 | // every value takes the type of its default. Until 2.11.0 this was an |
| 8522 | 8933 | // array_replace_recursive() of the file over the defaults, so any key in |
| @@ -8899,8 +9310,12 @@ | ||
| 8899 | 9310 | |
| 8900 | 9311 | delete_option( 'vigilante_last_integrity_results' ); |
| 8901 | 9312 | delete_option( 'vigilante_last_integrity_scan' ); |
| 8902 | 9313 | |
| 9314 | + // Starting over also means being told again: the next scan emails | |
| 9315 | + // whatever it finds, as if nothing had been reported before. | |
| 9316 | + delete_option( Vigilante_File_Integrity::NOTIFIED_OPTION ); | |
| 9317 | + | |
| 8903 | 9318 | /* |
| 8904 | 9319 | * A pending change to wp-config.php or the root .htaccess is closed by |
| 8905 | 9320 | * approving it, which takes the network. Clearing the results was one |
| 8906 | 9321 | * more way to close it without, until the next scan: the ignore list was |
| @@ -8952,9 +9367,29 @@ | ||
| 8952 | 9367 | if ( ! current_user_can( 'manage_options' ) ) { |
| 8953 | 9368 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8954 | 9369 | } |
| 8955 | 9370 | |
| 8956 | - $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; | |
| 9371 | + /* | |
| 9372 | + * The path as the scan reported it, which is what the interface sends | |
| 9373 | + * back. sanitize_text_field() is the wrong reader for it: it deletes | |
| 9374 | + * every %XX octet and collapses repeated spaces, so a file named | |
| 9375 | + * foto%20nueva.php was stored as fotonueva.php, the scan compared the | |
| 9376 | + * list with the paths it finds one by one, never matched, and the | |
| 9377 | + * warning came back (measured on 1 oct 2026 against 3.0.1). The four | |
| 9378 | + * handlers of the ignore list read the path the same way. | |
| 9379 | + * | |
| 9380 | + * Known limits, found by the cross review, that only a file name with a | |
| 9381 | + * stray < in it reaches. wp_strip_all_tags() shortens such a name at the | |
| 9382 | + * < instead of refusing it, so the stored entry can match another file | |
| 9383 | + * (a.php<b is stored as a.php), and the value ignore_file() returns is | |
| 9384 | + * not read, so a refusal of its own is answered as success. The fix, if | |
| 9385 | + * a site ever needs it, is to answer with an error when | |
| 9386 | + * wp_strip_all_tags() changes what was sent and to check that return | |
| 9387 | + * value. An entry the old reader stored for such a name (it wrote the | |
| 9388 | + * < as <) is listed but "Stop ignoring" cannot remove it; "Clear all | |
| 9389 | + * ignored files" does. | |
| 9390 | + */ | |
| 9391 | + $file = ( isset( $_POST['file'] ) && is_string( $_POST['file'] ) ) ? wp_strip_all_tags( wp_unslash( $_POST['file'] ) ) : ''; | |
| 8957 | 9392 | |
| 8958 | 9393 | if ( empty( $file ) ) { |
| 8959 | 9394 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 8960 | 9395 | } |
| @@ -9007,9 +9442,9 @@ | ||
| 9007 | 9442 | if ( ! current_user_can( 'manage_options' ) ) { |
| 9008 | 9443 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 9009 | 9444 | } |
| 9010 | 9445 | |
| 9011 | - $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; | |
| 9446 | + $file = ( isset( $_POST['file'] ) && is_string( $_POST['file'] ) ) ? wp_strip_all_tags( wp_unslash( $_POST['file'] ) ) : ''; | |
| 9012 | 9447 | |
| 9013 | 9448 | if ( empty( $file ) ) { |
| 9014 | 9449 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 9015 | 9450 | } |
| @@ -9042,9 +9477,9 @@ | ||
| 9042 | 9477 | $files = array(); |
| 9043 | 9478 | $locked = $this->critical_approval_locked(); |
| 9044 | 9479 | $shared = $locked ? array( 'wp-config.php', '.htaccess' ) : array(); |
| 9045 | 9480 | foreach ( $raw_files as $f ) { |
| 9046 | - $clean = sanitize_text_field( $f ); | |
| 9481 | + $clean = is_string( $f ) ? wp_strip_all_tags( $f ) : ''; | |
| 9047 | 9482 | // Same rule as ajax_ignore_file(): the two shared files when the |
| 9048 | 9483 | // network locks them, and Vigilant's own files always. |
| 9049 | 9484 | if ( '' !== $clean && ! in_array( $clean, $shared, true ) && ! Vigilante_Self_Integrity::is_own_file_path( $clean ) ) { |
| 9050 | 9485 | $files[] = $clean; |
| @@ -9111,9 +9546,9 @@ | ||
| 9111 | 9546 | } |
| 9112 | 9547 | |
| 9113 | 9548 | $files = array(); |
| 9114 | 9549 | foreach ( $raw_files as $f ) { |
| 9115 | - $clean = sanitize_text_field( $f ); | |
| 9550 | + $clean = is_string( $f ) ? wp_strip_all_tags( $f ) : ''; | |
| 9116 | 9551 | if ( '' !== $clean ) { |
| 9117 | 9552 | $files[] = $clean; |
| 9118 | 9553 | } |
| 9119 | 9554 | } |
| @@ -9235,13 +9670,99 @@ | ||
| 9235 | 9670 | if ( ! current_user_can( 'manage_options' ) ) { |
| 9236 | 9671 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 9237 | 9672 | } |
| 9238 | 9673 | |
| 9239 | - // Get security grade from settings (not from actual HTTP request) | |
| 9240 | - $security_headers = new Vigilante_Security_Headers( $this->settings ); | |
| 9241 | - $results = $security_headers->get_security_grade(); | |
| 9674 | + $results = $this->headers_test_results(); | |
| 9675 | + if ( is_wp_error( $results ) ) { | |
| 9676 | + wp_send_json_error( $results->get_error_message() ); | |
| 9677 | + } | |
| 9242 | 9678 | |
| 9243 | 9679 | wp_send_json_success( $results ); |
| 9680 | + } | |
| 9681 | + | |
| 9682 | + /** | |
| 9683 | + * What the Test Headers button shows: the headers that reach the browser | |
| 9684 | + * | |
| 9685 | + * Asks the home page and runs the header checks of Security Check on the | |
| 9686 | + * answer, so the two can never say different things about the same site. | |
| 9687 | + * Until 3.0.4 the button read the settings of the tab, graded them on a | |
| 9688 | + * scale of its own and listed them as enabled headers: on a server that | |
| 9689 | + * does not apply the rules of Vigilant it showed a B while Security | |
| 9690 | + * Check, which does look, said that no header arrived. | |
| 9691 | + * | |
| 9692 | + * @since 3.0.5 | |
| 9693 | + * | |
| 9694 | + * The lists carry text read from the answer of the site (the value of a | |
| 9695 | + * header), so whoever prints them escapes them: displayHeaderResults() | |
| 9696 | + * in assets/js/admin.js. | |
| 9697 | + * | |
| 9698 | + * They travel as passed, failing and notes. The names they had until | |
| 9699 | + * 3.0.4 (headers, missing and warnings) stay in the answer, empty: a | |
| 9700 | + * page that was open when the plugin was updated still runs the script | |
| 9701 | + * it loaded, which prints those three without escaping them, and it must | |
| 9702 | + * get the grade and no text read from the site. | |
| 9703 | + * | |
| 9704 | + * @return array|WP_Error grade, score (0 to 100), passed, failing and notes. | |
| 9705 | + */ | |
| 9706 | + private function headers_test_results() { | |
| 9707 | + if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) { | |
| 9708 | + require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php'; | |
| 9709 | + } | |
| 9710 | + // The constructor loads the checks and their helpers. | |
| 9711 | + new Vigilante_Security_Analyzer( $this->settings, $this->activity_log ); | |
| 9712 | + $no_answer = new WP_Error( 'vigilante_no_answer', __( 'Could not fetch the homepage to read response headers.', 'vigilante' ) ); | |
| 9713 | + // A new request every time: the button is pressed right after saving. | |
| 9714 | + // With no answer there is nothing to grade, and the checks would ask | |
| 9715 | + // for the page a second time. | |
| 9716 | + if ( null === Vigilante_SA_Helpers::probe_home( true ) ) { | |
| 9717 | + return $no_answer; | |
| 9718 | + } | |
| 9719 | + | |
| 9720 | + $category = new Vigilante_SA_Category_Headers( $this->settings ); | |
| 9721 | + $passed = array(); | |
| 9722 | + $failed = array(); | |
| 9723 | + $notes = array(); | |
| 9724 | + $earned = 0; | |
| 9725 | + $max = 0; | |
| 9726 | + | |
| 9727 | + foreach ( $category->run( 'slow' ) as $check ) { | |
| 9728 | + if ( Vigilante_SA_Check_Result::STATE_SKIP === $check->state ) { | |
| 9729 | + continue; | |
| 9730 | + } | |
| 9731 | + if ( Vigilante_SA_Check_Result::STATE_INFO === $check->state ) { | |
| 9732 | + $notes[] = $check->detail; | |
| 9733 | + continue; | |
| 9734 | + } | |
| 9735 | + $earned += (int) $check->score; | |
| 9736 | + $max += (int) $check->max; | |
| 9737 | + // The three lists are the three counters of Security Check. | |
| 9738 | + if ( Vigilante_SA_Check_Result::STATE_PASS === $check->state ) { | |
| 9739 | + $passed[] = $check->detail; | |
| 9740 | + } elseif ( Vigilante_SA_Check_Result::STATE_WARN === $check->state ) { | |
| 9741 | + $notes[] = $check->detail; | |
| 9742 | + } else { | |
| 9743 | + $failed[] = $check->detail; | |
| 9744 | + } | |
| 9745 | + } | |
| 9746 | + | |
| 9747 | + if ( $max < 1 ) { | |
| 9748 | + return $no_answer; | |
| 9749 | + } | |
| 9750 | + | |
| 9751 | + $score = (int) round( 100 * $earned / $max ); | |
| 9752 | + $sender = new Vigilante_Security_Headers( $this->settings ); | |
| 9753 | + | |
| 9754 | + return array( | |
| 9755 | + 'grade' => Vigilante_SA_Helpers::grade_for_score( $score ), | |
| 9756 | + 'score' => $score, | |
| 9757 | + 'passed' => $passed, | |
| 9758 | + 'failing' => $failed, | |
| 9759 | + 'notes' => array_merge( $notes, $sender->get_csp_warnings() ), | |
| 9760 | + // For the script of a page opened before the update: see above. | |
| 9761 | + 'headers' => array(), | |
| 9762 | + 'missing' => array(), | |
| 9763 | + 'warnings' => array(), | |
| 9764 | + ); | |
| 9244 | 9765 | } |
| 9245 | 9766 | |
| 9246 | 9767 | /** |
| 9247 | 9768 | * Sanitize section data (kept for compatibility) |