PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.0
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.0
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 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | admin/class-admin.php +814 -24 2.11.113.0.0 View file →
@@ -107,8 +107,11 @@
107 107 add_action( 'admin_init', array( $this, 'redirect_submenu_shortcuts' ) );
108 108 add_action( 'admin_init', array( $this, 'register_settings' ) );
109 109 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
110 110 add_action( 'admin_notices', array( $this, 'show_admin_notices' ) );
111 + // Self-protection speaks in the network admin too: on a network its
112 + // files are shared, and the super administrator is who can repair them.
113 + add_action( 'network_admin_notices', array( $this, 'maybe_show_self_protection_notice' ) );
111 114
112 115 // Highlight correct submenu based on active tab
113 116 add_filter( 'submenu_file', array( $this, 'highlight_submenu_tab' ) );
114 117
@@ -536,8 +539,68 @@
536 539 $this->migrate_pending_approval_per_site();
537 540
538 541 update_option( 'vigilante_db_version', '2.11.10' );
539 542 }
543 +
544 + /*
545 + * 3.0.0: self-protection package. This block must stay the LAST one of
546 + * run_migrations(): every block compares against the same $db_version
547 + * captured at the top, so the last update_option() that runs is the
548 + * one that sticks, and on a jump from any older version it has to be
549 + * this one.
550 + *
551 + * 1. Capture the self-integrity anchor (the manifest fingerprint) and
552 + * run an inline self-check. It covers every update the old code never
553 + * saw through the upgrader hook: from any 2.x, and manual or FTP
554 + * uploads. A plain new is enough, the constructor registers no hooks.
555 + * 2. Drop the cached Security Check report: the Internal category grows
556 + * from 30 to 33 points (the self_integrity check), and the cached
557 + * report would keep the old denominator until the next full scan.
558 + * Same reason and same background refresh as the 2.6.1 block above.
559 + * 3. Remove Vigilant's own files from the ignore list before that check.
560 + * Until 2.11.11 the generic scan listed them like any plugin, and any
561 + * administrator could ignore one (after a false alarm while
562 + * WordPress.org published new checksums, for example). Kept, those
563 + * entries would silence the self-check of those files for good,
564 + * PHP included.
565 + */
566 + if ( version_compare( $db_version, '3.0.0', '<' ) ) {
567 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
568 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
569 + }
570 + $ignored_files = get_option( 'vigilante_ignored_files', array() );
571 + if ( is_array( $ignored_files ) && $ignored_files ) {
572 + $kept_files = array_values(
573 + array_filter(
574 + $ignored_files,
575 + function ( $ignored_file ) {
576 + return ! Vigilante_Self_Integrity::is_own_file_path( (string) $ignored_file );
577 + }
578 + )
579 + );
580 + if ( count( $kept_files ) !== count( $ignored_files ) ) {
581 + update_option( 'vigilante_ignored_files', $kept_files );
582 + if ( $this->activity_log ) {
583 + $this->activity_log->log(
584 + 'system',
585 + 'self_ignored_files_removed',
586 + __( 'Vigilant files were removed from the File Integrity ignore list: self-protection checks them now.', 'vigilante' ),
587 + array( 'removed' => count( $ignored_files ) - count( $kept_files ) ),
588 + 'info'
589 + );
590 + }
591 + }
592 + }
593 + $self_integrity = new Vigilante_Self_Integrity( $this->settings, $this->activity_log );
594 + $self_integrity->run_check( 'migration' );
595 +
596 + delete_option( 'vigilante_analyzer_last_scan' );
597 + if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) {
598 + wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' );
599 + }
600 +
601 + update_option( 'vigilante_db_version', '3.0.0' );
602 + }
540 603 }
541 604
542 605 /**
543 606 * Move the pending-approval flag of a network to a key per site
@@ -724,17 +787,30 @@
724 787 $pending_count = $this->get_pending_approvals_count();
725 788
726 789 // Get security issues with severity
727 790 $security_status = $this->get_security_status_for_badge();
728 -
791 +
792 + /*
793 + * Self-protection: a change to Vigilant own files is the one finding
794 + * that has to be visible from any screen of WordPress, so it adds to
795 + * the counter and paints it red. A verified state, a state with fewer
796 + * references than usual and the check turned off add nothing: a counter
797 + * that is always on is a counter nobody reads.
798 + */
799 + $self_tone = Vigilante_Self_Integrity::tone(
800 + Vigilante_Self_Integrity::display_state(),
801 + Vigilante_Self_Integrity::is_on()
802 + );
803 + $self_badge = in_array( $self_tone, array( 'critical', 'off', 'warning' ), true ) ? 1 : 0;
804 +
729 805 // Total count for badge
730 - $total_badge = $pending_count + $security_status['count'];
731 -
806 + $total_badge = $pending_count + $security_status['count'] + $self_badge;
807 +
732 808 if ( $total_badge > 0 ) {
733 809 // Determine badge color:
734 810 // - Red (awaiting-mod): pending approvals OR critical modules disabled
735 811 // - Orange (update-plugins): only non-critical modules disabled
736 - if ( $pending_count > 0 || $security_status['has_critical'] ) {
812 + if ( $pending_count > 0 || $security_status['has_critical'] || in_array( $self_tone, array( 'critical', 'off' ), true ) ) {
737 813 $badge_class = 'awaiting-mod';
738 814 } else {
739 815 $badge_class = 'update-plugins vigilante-badge-warning';
740 816 }
@@ -776,13 +852,21 @@
776 852 'vigilante-activity-log',
777 853 array( $this, 'redirect_to_tab' )
778 854 );
779 855
780 - // File Integrity shortcut
856 + // File Integrity shortcut, with its own counter when the self-check has
857 + // something to say: that is the screen that explains it.
858 + $fi_menu_title = __( 'File Integrity', 'vigilante' );
859 + if ( $self_badge > 0 ) {
860 + $fi_menu_title .= sprintf(
861 + ' <span class="%s count-1"><span class="pending-count">1</span></span>',
862 + in_array( $self_tone, array( 'critical', 'off' ), true ) ? 'awaiting-mod' : 'update-plugins vigilante-badge-warning'
863 + );
864 + }
781 865 add_submenu_page(
782 866 'vigilante',
783 867 __( 'File Integrity', 'vigilante' ),
784 - __( 'File Integrity', 'vigilante' ),
868 + $fi_menu_title,
785 869 'manage_options',
786 870 'vigilante-file-integrity',
787 871 array( $this, 'redirect_to_tab' )
788 872 );
@@ -1045,8 +1129,28 @@
1045 1129 $score += 3;
1046 1130 }
1047 1131 }
1048 1132
1133 + /*
1134 + * Self-protection (15 points). Having it on is configuration, which is
1135 + * what this card measures; the state of the last check caps the card,
1136 + * because a plugin whose own files were changed is not well configured
1137 + * in any useful sense, whatever the rest of the settings say. The
1138 + * environment block below already mixes measured state into this score
1139 + * (WP_DEBUG, insecure usernames), so the card keeps its meaning.
1140 + */
1141 + $max_score += 15;
1142 + $self_enabled = Vigilante_Self_Integrity::is_on();
1143 + $self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $self_enabled );
1144 + if ( $self_enabled ) {
1145 + $score += 10;
1146 + // The five points are for a check that ran and came out clean: a
1147 + // site that has never checked itself has not earned them.
1148 + if ( in_array( $self_tone, array( 'ok', 'info' ), true ) ) {
1149 + $score += 5;
1150 + }
1151 + }
1152 +
1049 1153 // Environment checks (8 points) - penalize insecure server configuration
1050 1154 $max_score += 8;
1051 1155 $env_score = 8;
1052 1156
@@ -1062,9 +1166,14 @@
1062 1166 }
1063 1167
1064 1168 $score += max( 0, $env_score );
1065 1169
1066 - return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0;
1170 + $percent = $max_score > 0 ? (int) round( ( $score / $max_score ) * 100 ) : 0;
1171 + if ( in_array( $self_tone, array( 'critical', 'off' ), true ) ) {
1172 + // Same cap and same reason as the Security Check score.
1173 + $percent = min( $percent, Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER );
1174 + }
1175 + return $percent;
1067 1176 }
1068 1177
1069 1178 /**
1070 1179 * Get security recommendations based on current settings
@@ -1074,8 +1183,51 @@
1074 1183 */
1075 1184 private function get_security_recommendations( $options ) {
1076 1185 $recommendations = array();
1077 1186
1187 + // Self-protection first: if Vigilant itself cannot be trusted, nothing
1188 + // else on this card means much.
1189 + $vg_self_enabled = Vigilante_Self_Integrity::is_on();
1190 + $vg_self_tone = Vigilante_Self_Integrity::tone( Vigilante_Self_Integrity::display_state(), $vg_self_enabled );
1191 + if ( ! $vg_self_enabled ) {
1192 + $recommendations[] = array(
1193 + 'icon' => 'shield',
1194 + 'priority' => 'high',
1195 + 'tab' => 'file-integrity',
1196 + 'message' => __( 'Turn Vigilant self-protection on, so a change to Vigilant own files does not go unnoticed.', 'vigilante' ),
1197 + );
1198 + } elseif ( 'off' === $vg_self_tone ) {
1199 + $recommendations[] = array(
1200 + 'icon' => 'warning',
1201 + 'priority' => 'critical',
1202 + 'tab' => 'file-integrity',
1203 + 'message' => __( 'Something on this site switched Vigilant self-protection off: File Integrity says which file does it.', 'vigilante' ),
1204 + );
1205 + } elseif ( 'critical' === $vg_self_tone ) {
1206 + $recommendations[] = array(
1207 + 'icon' => 'warning',
1208 + 'priority' => 'critical',
1209 + 'tab' => 'file-integrity',
1210 + 'message' => __( 'Vigilant own files have been changed: repair Vigilant from File Integrity before anything else.', 'vigilante' ),
1211 + );
1212 + } elseif ( 'warning' === $vg_self_tone ) {
1213 + $recommendations[] = array(
1214 + 'icon' => 'shield',
1215 + 'priority' => 'high',
1216 + 'tab' => 'file-integrity',
1217 + 'message' => __( 'Vigilant self-protection needs your attention: File Integrity says what it found and what to do.', 'vigilante' ),
1218 + );
1219 + } elseif ( 'none' === $vg_self_tone ) {
1220 + // The score holds back the points of a check that has not run yet,
1221 + // so the card has to say why instead of just showing a lower number.
1222 + $recommendations[] = array(
1223 + 'icon' => 'shield',
1224 + 'priority' => 'high',
1225 + 'tab' => 'file-integrity',
1226 + 'message' => __( 'Vigilant has not checked its own files yet: run a scan from File Integrity.', 'vigilante' ),
1227 + );
1228 + }
1229 +
1078 1230 // Critical: Firewall disabled
1079 1231 if ( empty( $options['modules']['firewall'] ) ) {
1080 1232 $recommendations[] = array(
1081 1233 'icon' => 'warning',
@@ -1478,8 +1630,9 @@
1478 1630 // File Integrity
1479 1631 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'File Integrity Monitoring', 'vigilante' ), 'label_en' => 'File Integrity Monitoring', 'keywords' => _x( 'file integrity monitoring files checksum checksums tamper', 'settings search keywords', 'vigilante' ) ),
1480 1632 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan schedule', 'vigilante' ), 'label_en' => 'Scan schedule', 'keywords' => _x( 'scan schedule scans scanning check cron', 'settings search keywords', 'vigilante' ) ),
1481 1633 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Instant alert', 'vigilante' ), 'label_en' => 'Instant alert', 'keywords' => _x( 'instant alert alerts notification warning email', 'settings search keywords', 'vigilante' ) ),
1634 + array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'Vigilant self-protection', 'vigilante' ), 'anchor' => 'vigilante-section-fi-self', 'label' => __( 'Vigilant self-protection', 'vigilante' ), 'label_en' => 'Vigilant self-protection', 'keywords' => _x( 'self protection selfprotection self-check autoproteccion manifest sha256 checksums tampering tampered repair reinstall own files guardian integrity of the plugin', 'settings search keywords', 'vigilante' ) ),
1482 1635 array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'Ignored Files', 'vigilante' ), 'anchor' => 'vigilante-section-fi-ignored', 'label' => __( 'Ignored Files', 'vigilante' ), 'label_en' => 'Ignored Files', 'keywords' => _x( 'ignored files file exclude', 'settings search keywords', 'vigilante' ) ),
1483 1636 // Security Audit
1484 1637 array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Retention', 'vigilante' ), 'label_en' => 'Retention', 'keywords' => _x( 'retention keep days storage log', 'settings search keywords', 'vigilante' ) ),
1485 1638 array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Events to Log', 'vigilante' ), 'label_en' => 'Events to Log', 'keywords' => _x( 'events to log', 'settings search keywords', 'vigilante' ) ),
@@ -1632,8 +1785,9 @@
1632 1785 );
1633 1786
1634 1787 wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array(
1635 1788 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
1789 + 'selfBoxUrl' => esc_url( admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' ) ),
1636 1790 'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ),
1637 1791 'currentUserId' => get_current_user_id(),
1638 1792 'logoutUrl' => wp_logout_url( wp_login_url() ),
1639 1793 'adminUrl' => admin_url( 'admin.php?page=vigilante' ),
@@ -1681,8 +1835,13 @@
1681 1835 'file' => __( 'File', 'vigilante' ),
1682 1836 'reason' => __( 'Reason', 'vigilante' ),
1683 1837 'type' => __( 'Type', 'vigilante' ),
1684 1838 'unknown' => __( 'Unknown', 'vigilante' ),
1839 + 'selfType' => __( 'Vigilant (self)', 'vigilante' ),
1840 + 'logWhatHappened' => __( 'What happened', 'vigilante' ),
1841 + 'logWhatItMeans' => __( 'What it means', 'vigilante' ),
1842 + 'logWhatToDo' => __( 'What to do', 'vigilante' ),
1843 + 'logSelfSeeDetails' => __( 'Open File Integrity for the full detail', 'vigilante' ),
1685 1844 'modifiedFiles' => __( 'Modified Files', 'vigilante' ),
1686 1845 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ),
1687 1846 'extraFiles' => __( 'Extra Files', 'vigilante' ),
1688 1847 'extra' => __( 'Extra', 'vigilante' ),
@@ -1934,9 +2093,89 @@
1934 2093
1935 2094 /**
1936 2095 * Show admin notices
1937 2096 */
2097 + /**
2098 + * Self-protection notice.
2099 + *
2100 + * A change to Vigilant own files is shown on every admin screen, because
2101 + * waiting for someone to open the plugin is exactly what an attacker who
2102 + * patched it would want. A warning is shown only on Vigilant screens, so
2103 + * the notice that matters is not diluted. Neither is dismissible, both are
2104 + * for administrators only, and on a network the steps depend on whether
2105 + * the person can install plugins at all.
2106 + */
2107 + public function maybe_show_self_protection_notice() {
2108 + if ( ! current_user_can( 'manage_options' ) ) {
2109 + return;
2110 + }
2111 + $summary = $this->self_integrity_summary();
2112 + $tone = $summary['tone'];
2113 + if ( ! in_array( $tone, array( 'critical', 'off', 'warning' ), true ) ) {
2114 + return;
2115 + }
2116 +
2117 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the screen slug to decide where a notice is shown; no action taken.
2118 + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
2119 + $on_vigilante = ( 0 === strpos( $page, 'vigilante' ) );
2120 + if ( 'warning' === $tone && ! $on_vigilante ) {
2121 + return;
2122 + }
2123 + $alarma = in_array( $tone, array( 'critical', 'off' ), true );
2124 +
2125 + $link = admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' );
2126 + $network = is_multisite() && ! current_user_can( 'update_plugins' );
2127 + ?>
2128 + <div class="notice notice-<?php echo $alarma ? 'error' : 'warning'; ?> vigilante-self-notice">
2129 + <p class="vigilante-self-notice-title">
2130 + <span class="dashicons dashicons-shield" aria-hidden="true"></span>
2131 + <strong>
2132 + <?php
2133 + if ( 'critical' === $tone ) {
2134 + esc_html_e( 'Vigilant detected changes in its own files', 'vigilante' );
2135 + } elseif ( 'off' === $tone ) {
2136 + esc_html_e( 'Vigilant self-protection is switched off by code', 'vigilante' );
2137 + } else {
2138 + echo esc_html( $this->self_integrity_headline( $tone, $summary['state'] ) );
2139 + }
2140 + ?>
2141 + </strong>
2142 + </p>
2143 + <p class="vigilante-self-notice-text">
2144 + <span>
2145 + <?php
2146 + if ( 'critical' === $tone ) {
2147 + esc_html_e( 'Your security plugin may have been tampered with, and while that is true nothing it reports can be trusted.', 'vigilante' );
2148 + } elseif ( 'off' === $tone ) {
2149 + esc_html_e( 'Nothing is checking that Vigilant own files are intact. File Integrity says which file switches it off.', 'vigilante' );
2150 + } else {
2151 + esc_html_e( 'File Integrity says what was found, what it means and what to do about it.', 'vigilante' );
2152 + }
2153 + if ( $network ) {
2154 + echo ' ' . esc_html__( 'Only your network administrator can repair Vigilant, because its files are shared by every site in the network. Let them know.', 'vigilante' );
2155 + }
2156 + ?>
2157 + </span>
2158 + <?php
2159 + /*
2160 + * One button, on the same line as the text it belongs to, and it
2161 + * leads to the whole story. Repairing from a notice, without
2162 + * seeing which files, what it means and what it will do, is
2163 + * asking someone to fix what they have not read: the repair
2164 + * button lives in the card, after all that.
2165 + */
2166 + ?>
2167 + <a class="button button-primary button-small" href="<?php echo esc_url( $link ); ?>">
2168 + <?php esc_html_e( 'See what to do', 'vigilante' ); ?>
2169 + </a>
2170 + </p>
2171 + </div>
2172 + <?php
2173 + }
2174 +
1938 2175 public function show_admin_notices() {
2176 + $this->maybe_show_self_protection_notice();
2177 +
1939 2178 // Activation notice
1940 2179 if ( get_transient( 'vigilante_activated' ) ) {
1941 2180 ?>
1942 2181 <div class="notice notice-success is-dismissible vigilante-activation-notice">
@@ -2439,8 +2678,22 @@
2439 2678 $categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array();
2440 2679 $weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] );
2441 2680 $email_enabled = ! empty( $analyzer_settings['email_on_regression'] );
2442 2681
2682 + /*
2683 + * Self-protection caps the score, and it does so here and not only in
2684 + * the stored report: the report is reused until the next Security
2685 + * Check, so tampering found after it ran would otherwise be shown next
2686 + * to the score the site earned before it happened.
2687 + */
2688 + $self_summary = $this->self_integrity_summary();
2689 + $self_capped = in_array( $self_summary['tone'], array( 'critical', 'off' ), true )
2690 + || ( 'self_integrity' === ( isset( $last_scan['capped_by'] ) ? $last_scan['capped_by'] : '' ) );
2691 + if ( $self_capped && $has_data && $score > Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER ) {
2692 + $score = Vigilante_Security_Analyzer::SCORE_CAP_ON_TAMPER;
2693 + $grade = 'E';
2694 + }
2695 +
2443 2696 $quality = self::analyzer_quality_tag( $score );
2444 2697 ?>
2445 2698 <div class="vigilante-analyzer" id="vigilante-analyzer"
2446 2699 data-has-data="<?php echo $has_data ? '1' : '0'; ?>">
@@ -2465,8 +2718,19 @@
2465 2718 </button>
2466 2719 </div>
2467 2720 </div>
2468 2721
2722 + <?php if ( $self_capped ) : ?>
2723 + <p class="vigilante-analyzer-capped">
2724 + <span class="dashicons dashicons-shield" aria-hidden="true"></span>
2725 + <strong><?php esc_html_e( 'This score is not reliable right now.', 'vigilante' ); ?></strong>
2726 + <?php esc_html_e( 'Vigilant own files have been changed, and every other result on this page is produced by that same code. The score is held at the bottom of the scale until the files verify clean again.', 'vigilante' ); ?>
2727 + <a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' ) ); ?>">
2728 + <?php esc_html_e( 'See what to do', 'vigilante' ); ?>
2729 + </a>
2730 + </p>
2731 + <?php endif; ?>
2732 +
2469 2733 <div class="vigilante-analyzer-summary">
2470 2734 <div class="vigilante-analyzer-score-card">
2471 2735 <?php if ( $has_data && $grade ) : ?>
2472 2736 <div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>">
@@ -2840,8 +3104,58 @@
2840 3104
2841 3105 /**
2842 3106 * Render dashboard tab
2843 3107 */
3108 + /**
3109 + * One line strip at the top of the Dashboard tab: the state of Vigilant own
3110 + * files, above the Configuration Score and the Security Check, because
3111 + * nothing else on this screen means much while it is red.
3112 + */
3113 + private function render_self_protection_strip() {
3114 + $summary = $this->self_integrity_summary();
3115 + $tone = $summary['tone'];
3116 + $state = $summary['state'];
3117 + $count = count( $summary['findings'] );
3118 + $link = admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-self' );
3119 + ?>
3120 + <div class="vigilante-self-strip vigilante-self-strip--<?php echo esc_attr( $tone ); ?>">
3121 + <span class="dashicons dashicons-shield" aria-hidden="true"></span>
3122 + <strong><?php esc_html_e( 'Vigilant self-protection', 'vigilante' ); ?></strong>
3123 + <span class="vigilante-self-strip-state"><?php echo esc_html( $this->self_integrity_headline( $tone, $state ) ); ?></span>
3124 + <?php if ( $count > 0 ) : ?>
3125 + <span class="vigilante-self-strip-count">
3126 + <?php
3127 + printf(
3128 + /* translators: %d: number of findings about Vigilant own files */
3129 + esc_html( _n( '%d finding', '%d findings', $count, 'vigilante' ) ),
3130 + (int) $count
3131 + );
3132 + ?>
3133 + </span>
3134 + <?php elseif ( ! empty( $state['last_check'] ) && 'off' !== $tone ) : ?>
3135 + <span class="vigilante-self-strip-count">
3136 + <?php
3137 + printf(
3138 + /* translators: %s: human time difference, like "2 hours" */
3139 + esc_html__( 'checked %s ago', 'vigilante' ),
3140 + esc_html( human_time_diff( (int) $state['last_check'], time() ) )
3141 + );
3142 + ?>
3143 + </span>
3144 + <?php endif; ?>
3145 + <a href="<?php echo esc_url( $link ); ?>">
3146 + <?php
3147 + if ( in_array( $tone, array( 'critical', 'off', 'warning' ), true ) ) {
3148 + esc_html_e( 'See what to do', 'vigilante' );
3149 + } else {
3150 + esc_html_e( 'See details', 'vigilante' );
3151 + }
3152 + ?>
3153 + </a>
3154 + </div>
3155 + <?php
3156 + }
3157 +
2844 3158 private function render_tab_dashboard() {
2845 3159 $options = $this->settings->get_all_options();
2846 3160 $module_labels = $this->settings->get_module_labels();
2847 3161 $module_descriptions = $this->settings->get_module_descriptions();
@@ -2861,8 +3175,9 @@
2861 3175 $analyzer_categories_def = Vigilante_Security_Analyzer::get_categories();
2862 3176 $analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array();
2863 3177 ?>
2864 3178 <div class="vigilante-dashboard">
3179 + <?php $this->render_self_protection_strip(); ?>
2865 3180 <div class="vigilante-status-card">
2866 3181 <h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2>
2867 3182 <p class="vigilante-score-kind description">
2868 3183 <?php esc_html_e( 'How well Vigilante is configured right now. Pair it with the Security Check below to see the real-world result.', 'vigilante' ); ?>
@@ -6217,8 +6532,19 @@
6217 6532 'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ),
6218 6533 'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ),
6219 6534 'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ),
6220 6535 );
6536 + // Self-protection entries carry what happened, what
6537 + // it means and what to do, from the same catalogue
6538 + // the File Integrity box uses.
6539 + $vg_self_event = Vigilante_Self_Integrity_Guidance::for_log_event(
6540 + (string) ( $log->event_action ?? '' ),
6541 + $log->extra_data ?? '',
6542 + (string) ( $log->severity ?? 'info' )
6543 + );
6544 + if ( null !== $vg_self_event ) {
6545 + $details['self'] = $vg_self_event;
6546 + }
6221 6547 $display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type;
6222 6548 $display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity;
6223 6549 ?>
6224 6550 <tr class="vigilante-severity-<?php echo esc_attr( $log->severity ); ?>">
@@ -6261,8 +6587,403 @@
6261 6587
6262 6588 /**
6263 6589 * Render File Integrity tab
6264 6590 */
6591 + /**
6592 + * Findings of the self-check grouped by the case that explains them, worst
6593 + * first: ten modified files are one case with ten paths, not ten copies of
6594 + * the same explanation.
6595 + *
6596 + * @param array $findings Findings from the state.
6597 + * @return array
6598 + */
6599 + private function self_findings_by_case( $findings ) {
6600 + $groups = array();
6601 + foreach ( (array) $findings as $finding ) {
6602 + if ( ! is_array( $finding ) || 'info' === ( $finding['severity'] ?? '' ) ) {
6603 + continue;
6604 + }
6605 + $guidance = Vigilante_Self_Integrity_Guidance::for_finding( $finding );
6606 + $key = $guidance['key'];
6607 + if ( ! isset( $groups[ $key ] ) ) {
6608 + $guidance['files'] = array();
6609 + $guidance['severity'] = 'warning';
6610 + $groups[ $key ] = $guidance;
6611 + }
6612 + $file = isset( $finding['file'] ) ? (string) $finding['file'] : '';
6613 + if ( '' !== $file && ! in_array( $file, $groups[ $key ]['files'], true ) ) {
6614 + $groups[ $key ]['files'][] = $file;
6615 + }
6616 + if ( 'critical' === ( $finding['severity'] ?? '' ) ) {
6617 + $groups[ $key ]['severity'] = 'critical';
6618 + }
6619 + }
6620 + uasort(
6621 + $groups,
6622 + function ( $a, $b ) {
6623 + $rank = array( 'critical' => 0, 'warning' => 1 );
6624 + $ra = isset( $rank[ $a['severity'] ] ) ? $rank[ $a['severity'] ] : 2;
6625 + $rb = isset( $rank[ $b['severity'] ] ) ? $rank[ $b['severity'] ] : 2;
6626 + return $ra - $rb;
6627 + }
6628 + );
6629 + return $groups;
6630 + }
6631 +
6632 + /**
6633 + * Human label for the context that ran the last self-check.
6634 + *
6635 + * @param string $context Stored context.
6636 + * @return string
6637 + */
6638 + private function self_context_label( $context ) {
6639 + switch ( (string) $context ) {
6640 + case 'scan':
6641 + return __( 'during a file integrity scan', 'vigilante' );
6642 + case 'upgrader':
6643 + return __( 'right after updating Vigilant', 'vigilante' );
6644 + case 'version_change':
6645 + return __( 'after a version change made outside the updater', 'vigilante' );
6646 + case 'migration':
6647 + return __( 'while updating to this version', 'vigilante' );
6648 + case 'activation':
6649 + return __( 'when Vigilant was activated', 'vigilante' );
6650 + case 'watchdog':
6651 + return __( 'from the scheduled task watchdog', 'vigilante' );
6652 + }
6653 + return '';
6654 + }
6655 +
6656 + /**
6657 + * Short line for the badge of the box, the notices and the Dashboard strip.
6658 + *
6659 + * @param string $tone Tone from Vigilante_Self_Integrity::tone().
6660 + * @param array $state State.
6661 + * @return string
6662 + */
6663 + private function self_integrity_headline( $tone, $state ) {
6664 + $files = isset( $state['files_checked'] ) ? (int) $state['files_checked'] : 0;
6665 + $anchors = ( isset( $state['anchors'] ) && is_array( $state['anchors'] ) ) ? $state['anchors'] : array();
6666 + switch ( $tone ) {
6667 + case 'critical':
6668 + return __( 'Changes detected in Vigilant own files', 'vigilante' );
6669 + case 'warning':
6670 + return ( $files < 1 )
6671 + ? __( 'Vigilant could not check its own files', 'vigilante' )
6672 + : __( 'Vigilant self-protection needs your attention', 'vigilante' );
6673 + case 'off':
6674 + return __( 'Self-protection is switched off by code', 'vigilante' );
6675 + case 'none':
6676 + return __( 'Vigilant has not checked its own files yet', 'vigilante' );
6677 + }
6678 + return sprintf(
6679 + /* translators: 1: number of files verified, 2: number of references available, out of three */
6680 + __( 'Verified: %1$d files, %2$d of 3 references', 'vigilante' ),
6681 + $files,
6682 + count( array_filter( $anchors ) )
6683 + );
6684 + }
6685 +
6686 + /**
6687 + * Vigilant self-protection box: the first block of the File Integrity
6688 + * results, with its own colour by severity, what each finding means and
6689 + * how to fix it.
6690 + *
6691 + * It renders whether or not a scan has been stored, because the self-check
6692 + * also runs after every update and once a day: before 3.0.0 this lived in
6693 + * a line above the numeric cards of the last scan, where it was invisible
6694 + * and, without a stored scan, absent.
6695 + *
6696 + * @param array $fi_options File Integrity settings section.
6697 + */
6698 + /**
6699 + * Everything the screens need to say about self-protection, read once:
6700 + * the state, its findings grouped by case, and the tone that colours the
6701 + * box, the menu counter and the notices.
6702 + *
6703 + * @param array|null $fi_options File Integrity settings, read if not given.
6704 + * @return array { state, findings, groups, tone, enabled }
6705 + */
6706 + private function self_integrity_summary( $fi_options = null ) {
6707 + // Four to six screens ask for this in the same page load (menu, notice,
6708 + // box, strip, both scores). The option is cached by the core options
6709 + // layer, but the grouping and the tone are not: memoize per request.
6710 + static $cached = null;
6711 + if ( null !== $cached && ! is_array( $fi_options ) ) {
6712 + return $cached;
6713 + }
6714 + if ( ! is_array( $fi_options ) ) {
6715 + $fi_options = (array) $this->settings->get_section( 'file_integrity' );
6716 + }
6717 + $enabled = Vigilante_Self_Integrity::is_on();
6718 + $state = Vigilante_Self_Integrity::display_state();
6719 + $findings = Vigilante_Self_Integrity::state_findings( $state, $enabled );
6720 + $summary = array(
6721 + 'state' => $state,
6722 + 'findings' => $findings,
6723 + 'groups' => $this->self_findings_by_case( $findings ),
6724 + 'tone' => Vigilante_Self_Integrity::tone( $state, $enabled ),
6725 + 'enabled' => $enabled,
6726 + );
6727 + $cached = $summary;
6728 + return $summary;
6729 + }
6730 +
6731 + private function render_self_protection_box( $fi_options ) {
6732 + $summary = $this->self_integrity_summary( $fi_options );
6733 + $enabled = $summary['enabled'];
6734 + $state = $summary['state'];
6735 + $findings = $summary['findings'];
6736 + $groups = $summary['groups'];
6737 + $tone = $summary['tone'];
6738 + $total = isset( $state['last_findings_total'] ) ? (int) $state['last_findings_total'] : count( $findings );
6739 + $status = array(
6740 + 'status' => isset( $state['last_status'] ) ? (string) $state['last_status'] : '',
6741 + 'files' => isset( $state['files_checked'] ) ? (int) $state['files_checked'] : 0,
6742 + 'anchors' => ( isset( $state['anchors'] ) && is_array( $state['anchors'] ) ) ? $state['anchors'] : array(),
6743 + 'enabled' => $enabled,
6744 + 'has_run' => ! empty( $state['last_check'] ),
6745 + );
6746 +
6747 + /*
6748 + * Nothing to do, nothing to open: one line. A card with a big icon and
6749 + * folded sections for "everything is fine" is furniture, and furniture
6750 + * is what made the previous version of this invisible.
6751 + */
6752 + if ( empty( $groups ) ) {
6753 + $this->render_self_line( $tone, $state, $status );
6754 + return;
6755 + }
6756 +
6757 + $datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
6758 + $context_label = $this->self_context_label( isset( $state['last_context'] ) ? $state['last_context'] : '' );
6759 + $icon = ( 'critical' === $tone ) ? 'dashicons-shield' : 'dashicons-warning';
6760 + ?>
6761 + <div id="vigilante-section-fi-self" class="vigilante-settings-section vigilante-self-box vigilante-self-box--<?php echo esc_attr( $tone ); ?>">
6762 + <div class="vigilante-self-card">
6763 + <div class="vigilante-self-head">
6764 + <span class="vigilante-self-icon"><span class="dashicons <?php echo esc_attr( $icon ); ?>" aria-hidden="true"></span></span>
6765 + <div class="vigilante-self-head-text">
6766 + <h2><?php echo esc_html( $this->self_integrity_headline( $tone, $state ) ); ?></h2>
6767 + <p class="vigilante-self-meta">
6768 + <?php
6769 + printf(
6770 + /* translators: %s: date and time of the last self-check */
6771 + esc_html__( 'Checked on %s', 'vigilante' ),
6772 + esc_html( wp_date( $datetime_format, (int) $state['last_check'] ) )
6773 + );
6774 + if ( '' !== $context_label ) {
6775 + echo ', ' . esc_html( $context_label );
6776 + }
6777 + if ( $status['files'] > 0 ) {
6778 + echo ', ';
6779 + printf(
6780 + /* translators: 1: number of files checked, 2: number of references available, out of three */
6781 + esc_html__( '%1$d files against %2$d of 3 references', 'vigilante' ),
6782 + (int) $status['files'],
6783 + (int) count( array_filter( $status['anchors'] ) )
6784 + );
6785 + }
6786 + ?>
6787 + </p>
6788 + </div>
6789 + </div>
6790 +
6791 + <div class="vigilante-self-cases">
6792 + <?php
6793 + $first = true;
6794 + foreach ( $groups as $group ) {
6795 + $this->render_self_case( $group, $group['files'], $group['severity'], $first );
6796 + $first = false;
6797 + }
6798 +
6799 + if ( in_array( $tone, array( 'critical', 'warning' ), true ) ) {
6800 + $can_repair = class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair();
6801 + if ( ! $can_repair ) {
6802 + if ( class_exists( 'Vigilante_Self_Repair' ) && ! Vigilante_Self_Repair::folder_is_the_distributed_one() ) {
6803 + $vg_reason = 'renamed';
6804 + } elseif ( ! wp_is_file_mod_allowed( 'capability_update_core' ) ) {
6805 + // With DISALLOW_FILE_MODS nobody can do it from the admin,
6806 + // not even a network administrator: telling a subsite admin
6807 + // to ask theirs would send them to someone equally blocked.
6808 + $vg_reason = 'no_caps';
6809 + } elseif ( is_multisite() && ! current_user_can( 'update_plugins' ) ) {
6810 + $vg_reason = 'network';
6811 + } else {
6812 + $vg_reason = 'no_caps';
6813 + }
6814 + $this->render_self_case(
6815 + array(
6816 + 'title' => __( 'How to repair it here', 'vigilante' ),
6817 + 'meaning' => __( 'This site does not let Vigilant replace its own files from this screen.', 'vigilante' ),
6818 + 'steps' => Vigilante_Self_Integrity_Guidance::manual_steps( $vg_reason ),
6819 + ),
6820 + array(),
6821 + '',
6822 + false
6823 + );
6824 + }
6825 + }
6826 + ?>
6827 + </div>
6828 +
6829 + <?php if ( $total > count( $findings ) ) : ?>
6830 + <p class="description">
6831 + <?php
6832 + printf(
6833 + /* translators: 1: findings shown, 2: findings found in total */
6834 + esc_html__( 'Showing %1$d of %2$d findings: the rest are of the same kind.', 'vigilante' ),
6835 + (int) count( $findings ),
6836 + (int) $total
6837 + );
6838 + ?>
6839 + </p>
6840 + <?php endif; ?>
6841 +
6842 + <?php
6843 + /*
6844 + * Reinstalling fixes files, not a filter someone wrote or a hook
6845 + * someone removed: the button only appears when at least one of the
6846 + * cases on screen is one a clean copy solves. Offering it next to
6847 + * "switched off by code" was telling the person to fix something
6848 + * else.
6849 + */
6850 + $vg_offer_repair = false;
6851 + foreach ( $groups as $vg_group ) {
6852 + if ( ! empty( $vg_group['repair'] ) ) {
6853 + $vg_offer_repair = true;
6854 + break;
6855 + }
6856 + }
6857 + ?>
6858 + <p class="vigilante-self-actions">
6859 + <?php if ( $vg_offer_repair && class_exists( 'Vigilante_Self_Repair' ) && Vigilante_Self_Repair::can_repair() ) : ?>
6860 + <a class="button button-primary" href="<?php echo esc_url( Vigilante_Self_Repair::action_url() ); ?>">
6861 + <?php esc_html_e( 'Repair Vigilant', 'vigilante' ); ?>
6862 + </a>
6863 + <span class="description">
6864 + <?php esc_html_e( 'It downloads a clean copy from WordPress.org, asks before changing anything, and keeps your settings and log.', 'vigilante' ); ?>
6865 + </span>
6866 + <?php else : ?>
6867 + <span class="description">
6868 + <?php esc_html_e( 'After fixing it, run a new scan with the Run Scan Now button above.', 'vigilante' ); ?>
6869 + </span>
6870 + <?php endif; ?>
6871 + </p>
6872 + </div>
6873 + </div>
6874 + <?php
6875 + }
6876 +
6877 + /**
6878 + * The same state when there is nothing to act on: one line inside the same
6879 + * card, so the first block of the results is always the same object.
6880 + *
6881 + * @param string $tone Tone from Vigilante_Self_Integrity::tone().
6882 + * @param array $state State.
6883 + * @param array $status Arguments for the guidance catalogue.
6884 + */
6885 + private function render_self_line( $tone, $state, $status ) {
6886 + $guidance = Vigilante_Self_Integrity_Guidance::for_status( $status );
6887 + $icon = ( 'ok' === $tone ) ? 'dashicons-yes-alt' : 'dashicons-shield';
6888 + ?>
6889 + <div id="vigilante-section-fi-self" class="vigilante-settings-section vigilante-self-box vigilante-self-box--<?php echo esc_attr( $tone ); ?> vigilante-self-box--quiet">
6890 + <div class="vigilante-self-card vigilante-self-card--quiet">
6891 + <p class="vigilante-self-line">
6892 + <span class="dashicons <?php echo esc_attr( $icon ); ?>" aria-hidden="true"></span>
6893 + <strong><?php esc_html_e( 'Vigilant self-protection:', 'vigilante' ); ?></strong>
6894 + <span class="vigilante-self-line-state"><?php echo esc_html( $this->self_integrity_headline( $tone, $state ) ); ?></span>
6895 + <?php if ( ! empty( $state['last_check'] ) ) : ?>
6896 + <span class="vigilante-self-line-meta">
6897 + <?php
6898 + printf(
6899 + /* translators: %s: human time difference, like "2 hours" */
6900 + esc_html__( 'checked %s ago', 'vigilante' ),
6901 + esc_html( human_time_diff( (int) $state['last_check'], time() ) )
6902 + );
6903 + ?>
6904 + </span>
6905 + <?php endif; ?>
6906 + </p>
6907 + <?php if ( ! empty( $guidance['steps'] ) && 'none' === $tone ) : ?>
6908 + <p class="description vigilante-self-line-help"><?php echo esc_html( $guidance['steps'][0] ); ?></p>
6909 + <?php endif; ?>
6910 + </div>
6911 + </div>
6912 + <?php
6913 + }
6914 +
6915 + /**
6916 + * One case of the self-protection box, as a row that opens: what it is, how
6917 + * many files, its severity, and inside, the files, what it means and what to
6918 + * do. Plain HTML details, so there is no JavaScript between a finding about
6919 + * the security plugin and the person reading it.
6920 + *
6921 + * @param array $guidance Guidance entry (title, meaning, steps).
6922 + * @param array $files Paths this case was found in.
6923 + * @param string $severity critical|warning, empty for the how-to-repair row.
6924 + * @param bool $open Whether the row starts open.
6925 + */
6926 + private function render_self_case( $guidance, $files = array(), $severity = '', $open = false ) {
6927 + $shown = array_slice( (array) $files, 0, 20 );
6928 + $hidden = count( (array) $files ) - count( $shown );
6929 + ?>
6930 + <details class="vigilante-self-case vigilante-self-case--<?php echo esc_attr( '' !== $severity ? $severity : 'plain' ); ?>"<?php echo $open ? ' open' : ''; ?>>
6931 + <summary>
6932 + <span class="vigilante-self-case-title"><?php echo esc_html( $guidance['title'] ); ?></span>
6933 + <?php if ( ! empty( $files ) ) : ?>
6934 + <span class="vigilante-self-case-count">
6935 + <?php
6936 + printf(
6937 + /* translators: %d: number of files of this finding */
6938 + esc_html( _n( '%d file', '%d files', count( (array) $files ), 'vigilante' ) ),
6939 + (int) count( (array) $files )
6940 + );
6941 + ?>
6942 + </span>
6943 + <?php endif; ?>
6944 + <?php if ( '' !== $severity ) : ?>
6945 + <span class="vigilante-self-case-severity">
6946 + <?php echo esc_html( 'critical' === $severity ? __( 'critical', 'vigilante' ) : __( 'warning', 'vigilante' ) ); ?>
6947 + </span>
6948 + <?php endif; ?>
6949 + </summary>
6950 + <div class="vigilante-self-case-body">
6951 + <?php if ( ! empty( $shown ) ) : ?>
6952 + <ul class="vigilante-self-paths">
6953 + <?php foreach ( $shown as $file ) : ?>
6954 + <li><code><?php echo esc_html( $file ); ?></code></li>
6955 + <?php endforeach; ?>
6956 + <?php if ( $hidden > 0 ) : ?>
6957 + <li>
6958 + <?php
6959 + printf(
6960 + /* translators: %d: number of additional files */
6961 + esc_html__( 'and %d more', 'vigilante' ),
6962 + (int) $hidden
6963 + );
6964 + ?>
6965 + </li>
6966 + <?php endif; ?>
6967 + </ul>
6968 + <?php endif; ?>
6969 + <p class="vigilante-self-meaning">
6970 + <strong><?php esc_html_e( 'What it means:', 'vigilante' ); ?></strong>
6971 + <?php echo esc_html( $guidance['meaning'] ); ?>
6972 + </p>
6973 + <?php if ( ! empty( $guidance['steps'] ) ) : ?>
6974 + <p class="vigilante-self-todo"><strong><?php esc_html_e( 'What to do:', 'vigilante' ); ?></strong></p>
6975 + <ol class="vigilante-self-steps">
6976 + <?php foreach ( $guidance['steps'] as $step ) : ?>
6977 + <li><?php echo esc_html( $step ); ?></li>
6978 + <?php endforeach; ?>
6979 + </ol>
6980 + <?php endif; ?>
6981 + </div>
6982 + </details>
6983 + <?php
6984 + }
6985 +
6265 6986 private function render_tab_file_integrity() {
6266 6987 $is_disabled = $this->render_module_disabled_notice( 'file_integrity' );
6267 6988 $options = $this->settings->get_section( 'file_integrity' );
6268 6989 // On the main site of a network the critical-file scan is the network's
@@ -6273,8 +6994,32 @@
6273 6994 $last_scan = get_option( 'vigilante_last_integrity_scan' );
6274 6995 $last_results = get_option( 'vigilante_last_integrity_results' );
6275 6996 $ignored_files = get_option( 'vigilante_ignored_files', array() );
6276 6997
6998 + /*
6999 + * Vigilant own findings are shown in their own box, the first of the
7000 + * results, with what each one means and how to fix it. They are taken
7001 + * out of the generic tables here (and out of the counters above them)
7002 + * so the same finding is not reported twice and so no row of the
7003 + * security plugin's own files offers an Ignore button. The stored
7004 + * results keep them: the scan email reads its own section from there.
7005 + */
7006 + if ( is_array( $last_results ) ) {
7007 + foreach ( array( 'modified', 'suspicious', 'extra', 'missing' ) as $vg_bucket ) {
7008 + if ( empty( $last_results[ $vg_bucket ] ) || ! is_array( $last_results[ $vg_bucket ] ) ) {
7009 + continue;
7010 + }
7011 + $last_results[ $vg_bucket ] = array_values(
7012 + array_filter(
7013 + $last_results[ $vg_bucket ],
7014 + function ( $vg_item ) {
7015 + return ! ( is_array( $vg_item ) && 'vigilante_self' === ( $vg_item['type'] ?? '' ) );
7016 + }
7017 + )
7018 + );
7019 + }
7020 + }
7021 +
6277 7022 // Backward compat: convert old notify_on_changes to notify_level
6278 7023 $notify_level = $options['notify_level'] ?? '';
6279 7024 if ( empty( $notify_level ) ) {
6280 7025 $notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled';
@@ -6457,8 +7202,10 @@
6457 7202 </form>
6458 7203
6459 7204 <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div>
6460 7205
7206 + <?php $this->render_self_protection_box( $options ); ?>
7207 +
6461 7208 <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?>
6462 7209 <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section">
6463 7210 <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2>
6464 7211 <?php if ( $last_scan ) : ?>
@@ -6583,8 +7330,11 @@
6583 7330 }
6584 7331 } else {
6585 7332 $file_path = (string) $item;
6586 7333 }
7334 + if ( 'vigilante_self' === $file_type ) {
7335 + $file_type = __( 'Vigilant (self)', 'vigilante' );
7336 + }
6587 7337 ?>
6588 7338 <tr>
6589 7339 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6590 7340 <td><code style="color: #d63638;"><?php echo esc_html( $file_path ); ?></code></td>
@@ -6624,8 +7374,11 @@
6624 7374 foreach ( $last_results['extra'] as $item ) {
6625 7375 $file_path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item;
6626 7376 $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' );
6627 7377 $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown';
7378 + if ( 'vigilante_self' === $file_type ) {
7379 + $file_type = __( 'Vigilant (self)', 'vigilante' );
7380 + }
6628 7381 ?>
6629 7382 <tr>
6630 7383 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6631 7384 <td><code style="color: #b32d2e;"><?php echo esc_html( $file_path ); ?></code></td>
@@ -6656,8 +7409,19 @@
6656 7409 $regular_modified[] = $item;
6657 7410 }
6658 7411 }
6659 7412 }
7413 + // A missing file of Vigilant is critical, and the missing
7414 + // files of the generic scan have no table: it is listed with
7415 + // the modified files, or the tab said "All files passed" with
7416 + // a module deleted.
7417 + if ( $last_results && ! empty( $last_results['missing'] ) && is_array( $last_results['missing'] ) ) {
7418 + foreach ( $last_results['missing'] as $item ) {
7419 + if ( is_array( $item ) && 'vigilante_self' === ( $item['type'] ?? '' ) ) {
7420 + $regular_modified[] = $item;
7421 + }
7422 + }
7423 + }
6660 7424 ?>
6661 7425
6662 7426 <?php if ( ! empty( $critical_modified ) ) : ?>
6663 7427 <div class="vigilante-file-list vigilante-critical-config-files">
@@ -6856,8 +7620,11 @@
6856 7620 }
6857 7621 } else {
6858 7622 $file_path = (string) $item;
6859 7623 }
7624 + if ( 'vigilante_self' === $file_type ) {
7625 + $file_type = __( 'Vigilant (self)', 'vigilante' );
7626 + }
6860 7627 ?>
6861 7628 <tr>
6862 7629 <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th>
6863 7630 <td><code><?php echo esc_html( $file_path ); ?></code></td>
@@ -6871,9 +7638,16 @@
6871 7638 </table>
6872 7639 </div>
6873 7640 <?php endif; ?>
6874 7641
6875 - <?php if ( $last_results && empty( $last_results['modified'] ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
7642 + <?php
7643 + // "All files passed" is about every file, and Vigilant's own
7644 + // are files too: with the self-protection box in red or amber
7645 + // above, this line contradicted it (its findings no longer
7646 + // travel in the tables below).
7647 + $vg_self_alarm = in_array( $this->self_integrity_summary( $options )['tone'], array( 'critical', 'off', 'warning' ), true );
7648 + ?>
7649 + <?php if ( $last_results && ! $vg_self_alarm && empty( $critical_modified ) && empty( $regular_modified ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?>
6876 7650 <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;">
6877 7651 <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?>
6878 7652 </p>
6879 7653 <?php endif; ?>
@@ -7979,22 +8753,27 @@
7979 8753 * shut in 2.11.8 and this button was left open, found by the cross
7980 8754 * review of 2.11.8. So for somebody who cannot approve, those entries
7981 8755 * stay and everything else goes.
7982 8756 */
7983 - if ( $this->critical_approval_locked() && is_array( $results ) && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) {
7984 - $critical = array_values(
7985 - array_filter(
7986 - $results['modified'],
7987 - function ( $item ) {
7988 - return is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' );
7989 - }
7990 - )
7991 - );
8757 + // The findings about Vigilant's own files stay too: they report for the
8758 + // whole network, and ignoring them already takes network rights.
8759 + if ( $this->critical_approval_locked() && is_array( $results ) ) {
8760 + $kept = array();
8761 + $found = false;
8762 + foreach ( array( 'modified', 'missing', 'suspicious', 'extra' ) as $bucket ) {
8763 + $kept[ $bucket ] = array_values(
8764 + array_filter(
8765 + isset( $results[ $bucket ] ) && is_array( $results[ $bucket ] ) ? $results[ $bucket ] : array(),
8766 + function ( $item ) {
8767 + return is_array( $item ) && in_array( $item['type'] ?? '', array( 'critical_config', 'vigilante_self' ), true );
8768 + }
8769 + )
8770 + );
8771 + $found = $found || ! empty( $kept[ $bucket ] );
8772 + }
7992 8773
7993 - if ( $critical ) {
7994 - $results['modified'] = $critical;
7995 - $results['suspicious'] = array();
7996 - $results['extra'] = array();
8774 + if ( $found ) {
8775 + $results = array_merge( $results, $kept );
7997 8776 update_option( 'vigilante_last_integrity_results', $results );
7998 8777 update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() );
7999 8778 }
8000 8779 }
@@ -8031,8 +8810,17 @@
8031 8810 if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) {
8032 8811 wp_send_json_error( $this->critical_approval_notice() );
8033 8812 }
8034 8813
8814 + // Vigilant's own files are never ignored, on any site: silencing the
8815 + // check that says the security plugin was changed is the one button
8816 + // an attacker would want on this screen. Since 3.0.0 those findings
8817 + // are not rows of these tables either, so nothing in the interface
8818 + // sends them here; this is the door, not the label.
8819 + if ( Vigilante_Self_Integrity::is_own_file_path( $file ) ) {
8820 + wp_send_json_error( __( 'Findings about Vigilant own files cannot be ignored. File Integrity explains what each one means and how to repair it.', 'vigilante' ) );
8821 + }
8822 +
8035 8823 $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database );
8036 8824 $file_integrity->ignore_file( $file );
8037 8825
8038 8826 // Also remove the file from stored scan results so UI updates
@@ -8097,13 +8885,15 @@
8097 8885 wp_send_json_error( __( 'Invalid request.', 'vigilante' ) );
8098 8886 }
8099 8887
8100 8888 $files = array();
8101 - $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array();
8889 + $locked = $this->critical_approval_locked();
8890 + $shared = $locked ? array( 'wp-config.php', '.htaccess' ) : array();
8102 8891 foreach ( $raw_files as $f ) {
8103 8892 $clean = sanitize_text_field( $f );
8104 - // Same rule as ajax_ignore_file() for the two shared files.
8105 - if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) {
8893 + // Same rule as ajax_ignore_file(): the two shared files when the
8894 + // network locks them, and Vigilant's own files always.
8895 + if ( '' !== $clean && ! in_array( $clean, $shared, true ) && ! Vigilante_Self_Integrity::is_own_file_path( $clean ) ) {
8106 8896 $files[] = $clean;
8107 8897 }
8108 8898 }
8109 8899