PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.5
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.5
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 All 93 releases
← All changes | admin/class-admin.php +401 -148 3.0.4 → 3.0.5 View file →
@@ -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)