| @@ -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 | |