| @@ -1030,8 +1030,16 @@ | ||
| 1030 | 1030 | |
| 1031 | 1031 | /** |
| 1032 | 1032 | * Calculate comprehensive security score (0-100) |
| 1033 | 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 | + * | |
| 1034 | 1042 | * @param array $options Plugin options. |
| 1035 | 1043 | * @return int Security score. |
| 1036 | 1044 | */ |
| 1037 | 1045 | private function calculate_security_score( $options ) { |
| @@ -1057,11 +1065,11 @@ | ||
| 1057 | 1065 | } |
| 1058 | 1066 | } |
| 1059 | 1067 | |
| 1060 | 1068 | // Firewall details (10 points) |
| 1069 | + $max_score += 10; | |
| 1061 | 1070 | if ( ! empty( $options['modules']['firewall'] ) ) { |
| 1062 | 1071 | $firewall = $options['firewall'] ?? array(); |
| 1063 | - $max_score += 10; | |
| 1064 | 1072 | |
| 1065 | 1073 | $firewall_checks = array( |
| 1066 | 1074 | 'block_sql_injection', |
| 1067 | 1075 | 'block_xss_attacks', |
| @@ -1079,11 +1087,11 @@ | ||
| 1079 | 1087 | $score += min( 10, $firewall_enabled * 2 ); |
| 1080 | 1088 | } |
| 1081 | 1089 | |
| 1082 | 1090 | // Login security details (10 points) |
| 1091 | + $max_score += 10; | |
| 1083 | 1092 | if ( ! empty( $options['modules']['login_security'] ) ) { |
| 1084 | 1093 | $login = $options['login_security'] ?? array(); |
| 1085 | - $max_score += 10; | |
| 1086 | 1094 | |
| 1087 | 1095 | // Max attempts configured |
| 1088 | 1096 | if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) { |
| 1089 | 1097 | $score += 3; |
| @@ -1098,11 +1106,11 @@ | ||
| 1098 | 1106 | } |
| 1099 | 1107 | } |
| 1100 | 1108 | |
| 1101 | 1109 | // Security headers details (10 points) |
| 1110 | + $max_score += 10; | |
| 1102 | 1111 | if ( ! empty( $options['modules']['security_headers'] ) ) { |
| 1103 | 1112 | $headers = $options['security_headers'] ?? array(); |
| 1104 | - $max_score += 10; | |
| 1105 | 1113 | |
| 1106 | 1114 | if ( ! empty( $headers['x_frame_options'] ) ) { |
| 1107 | 1115 | $score += 2; |
| 1108 | 1116 | } |
| @@ -1117,11 +1125,11 @@ | ||
| 1117 | 1125 | } |
| 1118 | 1126 | } |
| 1119 | 1127 | |
| 1120 | 1128 | // User security details (10 points) |
| 1129 | + $max_score += 10; | |
| 1121 | 1130 | if ( ! empty( $options['modules']['user_security'] ) ) { |
| 1122 | 1131 | $user = $options['user_security'] ?? array(); |
| 1123 | - $max_score += 10; | |
| 1124 | 1132 | |
| 1125 | 1133 | if ( ! empty( $user['block_insecure_usernames'] ) ) { |
| 1126 | 1134 | $score += 3; |
| 1127 | 1135 | } |
| @@ -1135,14 +1143,14 @@ | ||
| 1135 | 1143 | $score += 2; |
| 1136 | 1144 | } |
| 1137 | 1145 | } |
| 1138 | 1146 | |
| 1139 | - // Audit alerts details (6 points) - only when Security Audit is on, | |
| 1140 | - // because the alerting layer rides on top of the activity log. Leaving | |
| 1141 | - // 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; | |
| 1142 | 1151 | if ( ! empty( $options['modules']['activity_log'] ) ) { |
| 1143 | 1152 | $alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array(); |
| 1144 | - $max_score += 6; | |
| 1145 | 1153 | if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) { |
| 1146 | 1154 | $score += 3; |
| 1147 | 1155 | } |
| 1148 | 1156 | if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) { |
| @@ -1163,10 +1171,13 @@ | ||
| 1163 | 1171 | $self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $self_enabled ); |
| 1164 | 1172 | if ( $self_enabled ) { |
| 1165 | 1173 | $score += 10; |
| 1166 | 1174 | // The five points are for a check that ran and came out clean: a |
| 1167 | - // site that has never checked itself has not earned them. | |
| 1168 | - 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 ) ) { | |
| 1169 | 1180 | $score += 5; |
| 1170 | 1181 | } |
| 1171 | 1182 | } |
| 1172 | 1183 | |
| @@ -1195,8 +1206,24 @@ | ||
| 1195 | 1206 | return $percent; |
| 1196 | 1207 | } |
| 1197 | 1208 | |
| 1198 | 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 | + /** | |
| 1199 | 1226 | * Get security recommendations based on current settings |
| 1200 | 1227 | * |
| 1201 | 1228 | * @param array $options Plugin options. |
| 1202 | 1229 | * @return array Array of recommendations. |
| @@ -1207,17 +1234,12 @@ | ||
| 1207 | 1234 | // Self-protection first: if Vigilant itself cannot be trusted, nothing |
| 1208 | 1235 | // else on this card means much. |
| 1209 | 1236 | $vg_self_enabled = Vigilante_Self_Integrity::is_on(); |
| 1210 | 1237 | $vg_self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $vg_self_enabled ); |
| 1211 | - 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 ) { | |
| 1212 | 1241 | $recommendations[] = array( |
| 1213 | - 'icon' => 'shield', | |
| 1214 | - 'priority' => 'high', | |
| 1215 | - 'tab' => 'file-integrity', | |
| 1216 | - 'message' => __( 'Turn Vigilant self-protection on, so a change to Vigilant own files does not go unnoticed.', 'vigilante' ), | |
| 1217 | - ); | |
| 1218 | - } elseif ( 'off' === $vg_self_tone ) { | |
| 1219 | - $recommendations[] = array( | |
| 1220 | 1242 | 'icon' => 'warning', |
| 1221 | 1243 | 'priority' => 'critical', |
| 1222 | 1244 | 'tab' => 'file-integrity', |
| 1223 | 1245 | 'message' => __( 'Something on this site switched Vigilant self-protection off: File Integrity says which file does it.', 'vigilante' ), |
| @@ -1226,9 +1248,9 @@ | ||
| 1226 | 1248 | $recommendations[] = array( |
| 1227 | 1249 | 'icon' => 'warning', |
| 1228 | 1250 | 'priority' => 'critical', |
| 1229 | 1251 | 'tab' => 'file-integrity', |
| 1230 | - '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 ) ), | |
| 1231 | 1253 | ); |
| 1232 | 1254 | } elseif ( 'warning' === $vg_self_tone ) { |
| 1233 | 1255 | $recommendations[] = array( |
| 1234 | 1256 | 'icon' => 'shield', |
| @@ -1839,10 +1861,11 @@ | ||
| 1839 | 1861 | // Header tester strings |
| 1840 | 1862 | 'testHeaders' => __( 'Test Headers', 'vigilante' ), |
| 1841 | 1863 | 'testing' => __( 'Testing...', 'vigilante' ), |
| 1842 | 1864 | 'score' => __( 'Score', 'vigilante' ), |
| 1843 | - 'enabledHeaders' => __( 'Enabled headers', 'vigilante' ), | |
| 1844 | - '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' ), | |
| 1845 | 1868 | 'warnings' => __( 'Warnings', 'vigilante' ), |
| 1846 | 1869 | // File integrity scan results strings |
| 1847 | 1870 | 'scanResults' => __( 'Scan Results', 'vigilante' ), |
| 1848 | 1871 | 'ok' => __( 'OK', 'vigilante' ), |
| @@ -1987,11 +2010,8 @@ | ||
| 1987 | 2010 | // Settings search strings |
| 1988 | 2011 | 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ), |
| 1989 | 2012 | /* translators: %d: number of results that did not fit in the list. */ |
| 1990 | 2013 | 'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ), |
| 1991 | - // Modules string | |
| 1992 | - /* translators: 1: enabled count, 2: total count */ | |
| 1993 | - 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ), | |
| 1994 | 2014 | // Activity log label maps for JS rendering |
| 1995 | 2015 | 'eventTypeLabels' => array( |
| 1996 | 2016 | 'login' => __( 'Login', 'vigilante' ), |
| 1997 | 2017 | '2fa' => __( 'Two-factor', 'vigilante' ), |
| @@ -2075,8 +2095,9 @@ | ||
| 2075 | 2095 | 'analyzerQualityGood' => __( 'Good', 'vigilante' ), |
| 2076 | 2096 | 'analyzerQualityFair' => __( 'Fair', 'vigilante' ), |
| 2077 | 2097 | 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ), |
| 2078 | 2098 | 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ), |
| 2099 | + 'analyzerNotMeasured' => __( 'Not measured', 'vigilante' ), | |
| 2079 | 2100 | 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ), |
| 2080 | 2101 | 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ), |
| 2081 | 2102 | /* translators: %d: number of findings in an info-only category */ |
| 2082 | 2103 | 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ), |
| @@ -2138,9 +2159,11 @@ | ||
| 2138 | 2159 | <p class="vigilante-self-notice-title"> |
| 2139 | 2160 | <span class="dashicons dashicons-shield" aria-hidden="true"></span> |
| 2140 | 2161 | <strong> |
| 2141 | 2162 | <?php |
| 2142 | - 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 ) { | |
| 2143 | 2166 | esc_html_e( 'Vigilant detected changes in its own files', 'vigilante' ); |
| 2144 | 2167 | } elseif ( 'off' === $tone ) { |
| 2145 | 2168 | esc_html_e( 'Vigilant self-protection is switched off by code', 'vigilante' ); |
| 2146 | 2169 | } else { |
| @@ -2151,9 +2174,13 @@ | ||
| 2151 | 2174 | </p> |
| 2152 | 2175 | <p class="vigilante-self-notice-text"> |
| 2153 | 2176 | <span> |
| 2154 | 2177 | <?php |
| 2155 | - 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 ) { | |
| 2156 | 2183 | esc_html_e( 'Your security plugin may have been tampered with, and while that is true nothing it reports can be trusted.', 'vigilante' ); |
| 2157 | 2184 | } elseif ( 'off' === $tone ) { |
| 2158 | 2185 | esc_html_e( 'Nothing is checking that Vigilant own files are intact. File Integrity says which file switches it off.', 'vigilante' ); |
| 2159 | 2186 | } else { |
| @@ -2158,9 +2185,11 @@ | ||
| 2158 | 2185 | esc_html_e( 'Nothing is checking that Vigilant own files are intact. File Integrity says which file switches it off.', 'vigilante' ); |
| 2159 | 2186 | } else { |
| 2160 | 2187 | esc_html_e( 'File Integrity says what was found, what it means and what to do about it.', 'vigilante' ); |
| 2161 | 2188 | } |
| 2162 | - 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'] ) ) { | |
| 2163 | 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' ); |
| 2164 | 2193 | } |
| 2165 | 2194 | ?> |
| 2166 | 2195 | </span> |
| @@ -2662,10 +2691,10 @@ | ||
| 2662 | 2691 | |
| 2663 | 2692 | /** |
| 2664 | 2693 | * Render the Security Analyzer (Security Check) widget + expandable full report. |
| 2665 | 2694 | * |
| 2666 | - * Lives in the Dashboard tab between the Configuration Score status card and | |
| 2667 | - * 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 | |
| 2668 | 2697 | * first paint shows real data; "Scan now" runs the 2-phase AJAX to refresh. |
| 2669 | 2698 | * |
| 2670 | 2699 | * @param array $last_scan Result of Vigilante_Security_Analyzer::get_last_scan(). |
| 2671 | 2700 | * @param array $history Score history (oldest first). |
| @@ -2696,14 +2725,34 @@ | ||
| 2696 | 2725 | */ |
| 2697 | 2726 | $self_summary = $this->self_integrity_summary(); |
| 2698 | 2727 | $self_capped = in_array( $self_summary['tone'], array( 'critical', 'off' ), true ) |
| 2699 | 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 | + } | |
| 2700 | 2746 | if ( $self_capped && $has_data && $score > Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER ) { |
| 2701 | 2747 | $score = Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER; |
| 2702 | - $grade = 'E'; | |
| 2703 | 2748 | } |
| 2749 | + if ( $has_data && '' !== $grade ) { | |
| 2750 | + $grade = Vigilante_SA_Helpers::grade_for_score( $score ); | |
| 2751 | + } | |
| 2704 | 2752 | |
| 2705 | - $quality = self::analyzer_quality_tag( $score ); | |
| 2753 | + $quality = self::analyzer_quality_tag( $score ); | |
| 2754 | + $not_measured = isset( $counts['skip'] ) ? (int) $counts['skip'] : 0; | |
| 2706 | 2755 | ?> |
| 2707 | 2756 | <div class="vigilante-analyzer" id="vigilante-analyzer" |
| 2708 | 2757 | data-has-data="<?php echo $has_data ? '1' : '0'; ?>"> |
| 2709 | 2758 | <div class="vigilante-analyzer-header"> |
| @@ -2728,12 +2777,22 @@ | ||
| 2728 | 2777 | </div> |
| 2729 | 2778 | </div> |
| 2730 | 2779 | |
| 2731 | 2780 | <?php if ( $self_capped ) : ?> |
| 2732 | - <p class="vigilante-analyzer-capped"> | |
| 2781 | + <p class="vigilante-analyzer-capped" data-why="<?php echo esc_attr( $self_capped_why ); ?>"> | |
| 2733 | 2782 | <span class="dashicons dashicons-shield" aria-hidden="true"></span> |
| 2734 | - <strong><?php esc_html_e( 'This score is not reliable right now.', 'vigilante' ); ?></strong> | |
| 2735 | - <?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 | + ?> | |
| 2736 | 2795 | <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' ) ); ?>"> |
| 2737 | 2796 | <?php esc_html_e( 'See what to do', 'vigilante' ); ?> |
| 2738 | 2797 | </a> |
| 2739 | 2798 | </p> |
| @@ -2778,8 +2837,20 @@ | ||
| 2778 | 2837 | <span class="dashicons dashicons-dismiss" aria-hidden="true"></span> |
| 2779 | 2838 | <strong data-role="fail"><?php echo esc_html( isset( $counts['fail'] ) ? $counts['fail'] : 0 ); ?></strong> |
| 2780 | 2839 | <span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Failing', 'vigilante' ); ?></span> |
| 2781 | 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> | |
| 2782 | 2853 | </div> |
| 2783 | 2854 | |
| 2784 | 2855 | <?php |
| 2785 | 2856 | // Sparkline of recent scores. Require at least 3 data points so the trend |
| @@ -2857,18 +2928,29 @@ | ||
| 2857 | 2928 | <div class="vigilante-analyzer-categories" data-role="categories"> |
| 2858 | 2929 | <?php foreach ( $categories_def as $slug => $meta ) : |
| 2859 | 2930 | $cat = isset( $categories[ $slug ] ) ? $categories[ $slug ] : array(); |
| 2860 | 2931 | $earned = isset( $cat['earned'] ) ? (int) $cat['earned'] : 0; |
| 2861 | - // Always use the declared meta as the source of truth for the maximum. | |
| 2862 | - // The cached scan may carry an old max if a check was added/removed | |
| 2863 | - // between releases (see 2.6.1: closed_plugins raised internal from 22 to 28 | |
| 2864 | - // but cached scans still reported max=22 until reset). | |
| 2865 | - $cat_max = (int) $meta['max']; | |
| 2932 | + $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array(); | |
| 2866 | 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']; | |
| 2867 | 2942 | $cat_pct = $cat_max > 0 ? (int) round( ( $earned / $cat_max ) * 100 ) : 0; |
| 2868 | - $checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array(); | |
| 2869 | 2943 | $cat_counts = isset( $cat['counts'] ) && is_array( $cat['counts'] ) ? $cat['counts'] : array( 'pass' => 0, 'warn' => 0, 'fail' => 0, 'info' => 0 ); |
| 2870 | 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 | + } | |
| 2871 | 2953 | $info_count = isset( $cat_counts['info'] ) ? (int) $cat_counts['info'] : 0; |
| 2872 | 2954 | ?> |
| 2873 | 2955 | <details class="vigilante-analyzer-category<?php echo $info_only ? ' vigilante-analyzer-category--info' : ''; ?>" |
| 2874 | 2956 | data-category="<?php echo esc_attr( $slug ); ?>" |
| @@ -2917,9 +2999,9 @@ | ||
| 2917 | 2999 | </span> |
| 2918 | 3000 | <?php endif; ?> |
| 2919 | 3001 | <?php if ( ! $info_only ) : ?> |
| 2920 | 3002 | <span class="vigilante-analyzer-category-score"> |
| 2921 | - <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> | |
| 2922 | 3004 | <span class="vigilante-analyzer-category-score-unit"><?php esc_html_e( 'pts', 'vigilante' ); ?></span> |
| 2923 | 3005 | </span> |
| 2924 | 3006 | <span class="vigilante-analyzer-category-bar" aria-hidden="true"> |
| 2925 | 3007 | <span class="vigilante-analyzer-category-bar-fill vigilante-analyzer-category-bar-fill--<?php echo esc_attr( $cat_quality['slug'] ); ?>" |
| @@ -2996,23 +3078,27 @@ | ||
| 2996 | 3078 | * @param int $pct 0..100. |
| 2997 | 3079 | * @return array{label:string,slug:string} |
| 2998 | 3080 | */ |
| 2999 | 3081 | public static function analyzer_quality_tag( $pct ) { |
| 3000 | - $pct = max( 0, min( 100, (int) $pct ) ); | |
| 3001 | - // "Excellent" is reserved for a perfect score — a single missing point drops to Good. | |
| 3002 | - if ( 100 === $pct ) { | |
| 3003 | - 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'; | |
| 3004 | 3084 | } |
| 3005 | - if ( $pct >= 70 ) { | |
| 3006 | - return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' ); | |
| 3007 | - } | |
| 3008 | - if ( $pct >= 50 ) { | |
| 3009 | - return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' ); | |
| 3010 | - } | |
| 3011 | - if ( $pct >= 30 ) { | |
| 3012 | - return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' ); | |
| 3013 | - } | |
| 3014 | - 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 | + ); | |
| 3015 | 3101 | } |
| 3016 | 3102 | |
| 3017 | 3103 | /** |
| 3018 | 3104 | * Render a single analyzer check row (used both server-side and via JS template). |
| @@ -3114,10 +3200,93 @@ | ||
| 3114 | 3200 | /** |
| 3115 | 3201 | * Render dashboard tab |
| 3116 | 3202 | */ |
| 3117 | 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 | + /** | |
| 3118 | 3287 | * One line strip at the top of the Dashboard tab: the state of Vigilant own |
| 3119 | - * files, above the Configuration Score and the Security Check, because | |
| 3288 | + * files, above the Security Check and the configuration card, because | |
| 3120 | 3289 | * nothing else on this screen means much while it is red. |
| 3121 | 3290 | */ |
| 3122 | 3291 | private function render_self_protection_strip() { |
| 3123 | 3292 | $summary = $this->self_integrity_summary(); |
| @@ -3185,67 +3354,11 @@ | ||
| 3185 | 3354 | $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array(); |
| 3186 | 3355 | ?> |
| 3187 | 3356 | <div class="vigilante-dashboard"> |
| 3188 | 3357 | <?php $this->render_self_protection_strip(); ?> |
| 3189 | - <div class="vigilante-status-card"> | |
| 3190 | - <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2> | |
| 3191 | - <p class="vigilante-score-kind description"> | |
| 3192 | - <?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' ); ?> | |
| 3193 | - </p> | |
| 3194 | - <div class="vigilante-security-score"> | |
| 3195 | - <?php | |
| 3196 | - // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29) | |
| 3197 | - if ( $security_score >= 90 ) { | |
| 3198 | - $grade = 'A'; | |
| 3199 | - } elseif ( $security_score >= 70 ) { | |
| 3200 | - $grade = 'B'; | |
| 3201 | - } elseif ( $security_score >= 50 ) { | |
| 3202 | - $grade = 'C'; | |
| 3203 | - } elseif ( $security_score >= 30 ) { | |
| 3204 | - $grade = 'D'; | |
| 3205 | - } else { | |
| 3206 | - $grade = 'E'; | |
| 3207 | - } | |
| 3208 | - ?> | |
| 3209 | - <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>"> | |
| 3210 | - <span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span> | |
| 3211 | - <span class="vigilante-score-text"><?php echo esc_html( $security_score ); ?>%</span> | |
| 3212 | - </div> | |
| 3213 | - <div class="vigilante-config-status"> | |
| 3214 | - <?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?> | |
| 3215 | - <span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>"> | |
| 3216 | - <?php echo esc_html( $presets[ $active_preset ]['name'] ); ?> | |
| 3217 | - </span> | |
| 3218 | - <?php else : ?> | |
| 3219 | - <span class="vigilante-preset-badge vigilante-preset-custom"> | |
| 3220 | - <?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?> | |
| 3221 | - </span> | |
| 3222 | - <?php endif; ?> | |
| 3223 | - </div> | |
| 3224 | - </div> | |
| 3225 | - | |
| 3226 | - <?php | |
| 3227 | - $recommendations = $this->get_security_recommendations( $options ); | |
| 3228 | - if ( ! empty( $recommendations ) ) : | |
| 3229 | - ?> | |
| 3230 | - <div class="vigilante-recommendations"> | |
| 3231 | - <h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4> | |
| 3232 | - <ul class="vigilante-recommendations-grid"> | |
| 3233 | - <?php foreach ( $recommendations as $rec ) : ?> | |
| 3234 | - <li> | |
| 3235 | - <span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span> | |
| 3236 | - <?php echo esc_html( $rec['message'] ); ?> | |
| 3237 | - <?php if ( ! empty( $rec['tab'] ) ) : ?> | |
| 3238 | - <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> | |
| 3239 | - <?php endif; ?> | |
| 3240 | - </li> | |
| 3241 | - <?php endforeach; ?> | |
| 3242 | - </ul> | |
| 3243 | - </div> | |
| 3244 | - <?php endif; ?> | |
| 3245 | - </div> | |
| 3358 | + <?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?> | |
| 3246 | 3359 | |
| 3247 | - <?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 ); ?> | |
| 3248 | 3361 | |
| 3249 | 3362 | <div class="vigilante-modules-grid"> |
| 3250 | 3363 | <h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2> |
| 3251 | 3364 | <p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p> |
| @@ -6682,8 +6795,15 @@ | ||
| 6682 | 6795 | $files = isset( $state['files_checked'] ) ? (int) $state['files_checked'] : 0; |
| 6683 | 6796 | $anchors = ( isset( $state['anchors'] ) && is_array( $state['anchors'] ) ) ? $state['anchors'] : array(); |
| 6684 | 6797 | switch ( $tone ) { |
| 6685 | 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 | + } | |
| 6686 | 6806 | return __( 'Changes detected in Vigilant own files', 'vigilante' ); |
| 6687 | 6807 | case 'warning': |
| 6688 | 6808 | return ( $files < 1 ) |
| 6689 | 6809 | ? __( 'Vigilant could not check its own files', 'vigilante' ) |
| @@ -6734,14 +6854,31 @@ | ||
| 6734 | 6854 | } |
| 6735 | 6855 | $enabled = Vigilante_Self_Integrity::is_on(); |
| 6736 | 6856 | $state = Vigilante_Self_Integrity::display_state(); |
| 6737 | 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 | + } | |
| 6738 | 6870 | $summary = array( |
| 6739 | - 'state' => $state, | |
| 6740 | - 'findings' => $findings, | |
| 6741 | - 'groups' => $this->self_findings_by_case( $findings ), | |
| 6742 | - 'tone' => Vigilante_Self_Integrity::tone( $state, $enabled ), | |
| 6743 | - '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 ), | |
| 6744 | 6881 | ); |
| 6745 | 6882 | $cached = $summary; |
| 6746 | 6883 | return $summary; |
| 6747 | 6884 | } |
| @@ -6808,14 +6945,42 @@ | ||
| 6808 | 6945 | |
| 6809 | 6946 | <div class="vigilante-self-cases"> |
| 6810 | 6947 | <?php |
| 6811 | 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; | |
| 6812 | 6960 | foreach ( $groups as $group ) { |
| 6813 | - $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 ); | |
| 6814 | 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 | + } | |
| 6815 | 6979 | } |
| 6816 | 6980 | |
| 6817 | - 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 ) ) { | |
| 6818 | 6983 | $can_repair = class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair(); |
| 6819 | 6984 | if ( ! $can_repair ) { |
| 6820 | 6985 | if ( class_exists( 'Vigilante_Self_Repair' ) && ! Vigilante_Self_Repair::folder_is_the_distributed_one() ) { |
| 6821 | 6986 | $vg_reason = 'renamed'; |
| @@ -6843,9 +7008,10 @@ | ||
| 6843 | 7008 | } |
| 6844 | 7009 | ?> |
| 6845 | 7010 | </div> |
| 6846 | 7011 | |
| 6847 | - <?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 ) ) : ?> | |
| 6848 | 7014 | <p class="description"> |
| 6849 | 7015 | <?php |
| 6850 | 7016 | printf( |
| 6851 | 7017 | /* translators: 1: findings shown, 2: findings found in total */ |
| @@ -6856,24 +7022,8 @@ | ||
| 6856 | 7022 | ?> |
| 6857 | 7023 | </p> |
| 6858 | 7024 | <?php endif; ?> |
| 6859 | 7025 | |
| 6860 | - <?php | |
| 6861 | - /* | |
| 6862 | - * Reinstalling fixes files, not a filter someone wrote or a hook | |
| 6863 | - * someone removed: the button only appears when at least one of the | |
| 6864 | - * cases on screen is one a clean copy solves. Offering it next to | |
| 6865 | - * "switched off by code" was telling the person to fix something | |
| 6866 | - * else. | |
| 6867 | - */ | |
| 6868 | - $vg_offer_repair = false; | |
| 6869 | - foreach ( $groups as $vg_group ) { | |
| 6870 | - if ( ! empty( $vg_group['repair'] ) ) { | |
| 6871 | - $vg_offer_repair = true; | |
| 6872 | - break; | |
| 6873 | - } | |
| 6874 | - } | |
| 6875 | - ?> | |
| 6876 | 7026 | <p class="vigilante-self-actions"> |
| 6877 | 7027 | <?php if ( $vg_offer_repair && class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair() ) : ?> |
| 6878 | 7028 | <a class="button button-primary" href="<?php echo esc_url( Vigilante_Self_Repair::action_url() ); ?>"> |
| 6879 | 7029 | <?php esc_html_e( 'Repair Vigilant', 'vigilante' ); ?> |
| @@ -6880,9 +7030,9 @@ | ||
| 6880 | 7030 | </a> |
| 6881 | 7031 | <span class="description"> |
| 6882 | 7032 | <?php esc_html_e( 'It downloads a clean copy from WordPress.org, asks before changing anything, and keeps your settings and log.', 'vigilante' ); ?> |
| 6883 | 7033 | </span> |
| 6884 | - <?php else : ?> | |
| 7034 | + <?php elseif ( $vg_scan_helps ) : ?> | |
| 6885 | 7035 | <span class="description"> |
| 6886 | 7036 | <?php esc_html_e( 'After fixing it, run a new scan with the Run Scan Now button above.', 'vigilante' ); ?> |
| 6887 | 7037 | </span> |
| 6888 | 7038 | <?php endif; ?> |
| @@ -6939,10 +7089,13 @@ | ||
| 6939 | 7089 | * @param array $guidance Guidance entry (title, meaning, steps). |
| 6940 | 7090 | * @param array $files Paths this case was found in. |
| 6941 | 7091 | * @param string $severity critical|warning, empty for the how-to-repair row. |
| 6942 | 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. | |
| 6943 | 7096 | */ |
| 6944 | - 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' ) { | |
| 6945 | 7098 | $shown = array_slice( (array) $files, 0, 20 ); |
| 6946 | 7099 | $hidden = count( (array) $files ) - count( $shown ); |
| 6947 | 7100 | ?> |
| 6948 | 7101 | <details class="vigilante-self-case vigilante-self-case--<?php echo esc_attr( '' !== $severity ? $severity : 'plain' ); ?>"<?php echo $open ? ' open' : ''; ?>> |
| @@ -6950,13 +7103,27 @@ | ||
| 6950 | 7103 | <span class="vigilante-self-case-title"><?php echo esc_html( $guidance['title'] ); ?></span> |
| 6951 | 7104 | <?php if ( ! empty( $files ) ) : ?> |
| 6952 | 7105 | <span class="vigilante-self-case-count"> |
| 6953 | 7106 | <?php |
| 6954 | - printf( | |
| 6955 | - /* translators: %d: number of files of this finding */ | |
| 6956 | - esc_html( _n( '%d file', '%d files', count( (array) $files ), 'vigilante' ) ), | |
| 6957 | - (int) count( (array) $files ) | |
| 6958 | - ); | |
| 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 | + } | |
| 6959 | 7126 | ?> |
| 6960 | 7127 | </span> |
| 6961 | 7128 | <?php endif; ?> |
| 6962 | 7129 | <?php if ( '' !== $severity ) : ?> |
| @@ -9503,13 +9670,99 @@ | ||
| 9503 | 9670 | if ( ! current_user_can( 'manage_options' ) ) { |
| 9504 | 9671 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 9505 | 9672 | } |
| 9506 | 9673 | |
| 9507 | - // Get security grade from settings (not from actual HTTP request) | |
| 9508 | - $security_headers = new Vigilante_Security_Headers( $this->settings ); | |
| 9509 | - $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 | + } | |
| 9510 | 9678 | |
| 9511 | 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 | + ); | |
| 9512 | 9765 | } |
| 9513 | 9766 | |
| 9514 | 9767 | /** |
| 9515 | 9768 | * Sanitize section data (kept for compatibility) |