| @@ -1771,8 +1771,16 @@ | ||
| 1771 | 1771 | $files = array( $hook_extra['plugin'] ); |
| 1772 | 1772 | } |
| 1773 | 1773 | foreach ( $files as $file ) { |
| 1774 | 1774 | $slug = dirname( (string) $file ); |
| 1775 | + // Vigilant itself is verified immediately (not after 90 s) by | |
| 1776 | + // Vigilante_Self_Integrity::handle_upgrader() when the | |
| 1777 | + // self-check is on; do not also open a grace window for it. | |
| 1778 | + // Compared with the folder it really lives in, not the literal | |
| 1779 | + // slug, so a renamed folder is skipped the same way. | |
| 1780 | + if ( dirname( VIGILANTE_PLUGIN_BASENAME ) === $slug && Vigilante_Self_Integrity::is_on() ) { | |
| 1781 | + continue; | |
| 1782 | + } | |
| 1775 | 1783 | if ( '.' !== $slug && '' !== $slug ) { |
| 1776 | 1784 | $targets['plugin'][] = $slug; |
| 1777 | 1785 | } |
| 1778 | 1786 | } |
| @@ -1841,8 +1849,17 @@ | ||
| 1841 | 1849 | */ |
| 1842 | 1850 | private function verify_updated_slug( $type, $slug ) { |
| 1843 | 1851 | $grace_key = 'vigilante_fi_grace_' . $type . '_' . md5( $slug ); |
| 1844 | 1852 | |
| 1853 | + // Defensive skip for the first 2.11.x -> 3.0.x update: the OLD code in | |
| 1854 | + // memory scheduled this event including Vigilant's own slug, and by the | |
| 1855 | + // time it fires the NEW code (this one) is running with the self-check | |
| 1856 | + // handling Vigilant on its own. | |
| 1857 | + if ( 'plugin' === $type && dirname( VIGILANTE_PLUGIN_BASENAME ) === $slug && Vigilante_Self_Integrity::is_on() ) { | |
| 1858 | + delete_transient( $grace_key ); | |
| 1859 | + return; | |
| 1860 | + } | |
| 1861 | + | |
| 1845 | 1862 | if ( 'plugin' === $type ) { |
| 1846 | 1863 | if ( ! function_exists( 'get_plugins' ) ) { |
| 1847 | 1864 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 1848 | 1865 | } |
| @@ -1993,8 +2010,41 @@ | ||
| 1993 | 2010 | |
| 1994 | 2011 | // Use settings from options page |
| 1995 | 2012 | $options = is_array( $this->options ) ? $this->options : array(); |
| 1996 | 2013 | |
| 2014 | + // Vigilant self-check runs FIRST and exempt from the time budget: | |
| 2015 | + // ~60 small-file hashes cost < 50 ms and the guardian must never be | |
| 2016 | + // dropped by the budget on plugin-heavy sites. User exclusions do not | |
| 2017 | + // apply to it (see Vigilante_Self_Integrity::run_check()). | |
| 2018 | + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) { | |
| 2019 | + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php'; | |
| 2020 | + } | |
| 2021 | + if ( Vigilante_Self_Integrity::is_on() ) { | |
| 2022 | + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) { | |
| 2023 | + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php'; | |
| 2024 | + } | |
| 2025 | + $self = new Vigilante_Self_Integrity( $this->settings, $this->activity_log ); | |
| 2026 | + $self_result = $self->run_check( 'scan' ); | |
| 2027 | + // run_check() above updates this site's own state (status line and | |
| 2028 | + // the analyzer check keep working everywhere). The plugin files are | |
| 2029 | + // shared by the whole installation, so the self findings are folded | |
| 2030 | + // Self-protection has its own alert, and it does not travel in the | |
| 2031 | + // scan digest any more. That digest is governed by a notification | |
| 2032 | + // setting that can be switched off, and switching off "tell me about | |
| 2033 | + // changed files" was also switching off the alarm of the plugin | |
| 2034 | + // itself. So the findings stay out of the scan results (they have | |
| 2035 | + // their own block in File Integrity, with what each one means and how | |
| 2036 | + // to repair it) and a critical one sends its own email from here, | |
| 2037 | + // wherever the scan runs. maybe_send_self_alert() keeps it to the | |
| 2038 | + // site that owns the shared files and dedupes by set of findings. | |
| 2039 | + foreach ( (array) $self_result['findings'] as $self_finding ) { | |
| 2040 | + if ( 'critical' === ( $self_finding['severity'] ?? '' ) ) { | |
| 2041 | + $self->maybe_send_self_alert( $self_result['findings'], 'scan' ); | |
| 2042 | + break; | |
| 2043 | + } | |
| 2044 | + } | |
| 2045 | + } | |
| 2046 | + | |
| 1997 | 2047 | // Scan uploads for suspicious files FIRST (highest security priority) |
| 1998 | 2048 | // PHP files in uploads are almost always malware |
| 1999 | 2049 | if ( ! empty( $options['scan_uploads'] ) && ! $this->is_time_exceeded() ) { |
| 2000 | 2050 | $upload_results = $this->scan_uploads(); |
| @@ -3514,8 +3564,17 @@ | ||
| 3514 | 3564 | if ( '.' === $plugin_slug ) { |
| 3515 | 3565 | continue; |
| 3516 | 3566 | } |
| 3517 | 3567 | |
| 3568 | + // With the self-check on, Vigilant itself is verified by the | |
| 3569 | + // sha256 triple-anchor block at the start of run_scan(): scanning | |
| 3570 | + // it here again would duplicate findings and the md5 fetch. With | |
| 3571 | + // the check off by filter, Vigilant is a regular plugin (legacy | |
| 3572 | + // behaviour). | |
| 3573 | + if ( dirname( VIGILANTE_PLUGIN_BASENAME ) === $plugin_slug && Vigilante_Self_Integrity::is_on() ) { | |
| 3574 | + continue; | |
| 3575 | + } | |
| 3576 | + | |
| 3518 | 3577 | // Skip slugs in their post-update grace window: wp.org may still be |
| 3519 | 3578 | // publishing the new version's checksums, so a scheduled scan here |
| 3520 | 3579 | // would raise benign "modified/extra" noise. The dedicated post-update |
| 3521 | 3580 | // verifier (vigilante_fi_postupdate_verify) handles these instead. |
| @@ -4762,8 +4821,26 @@ | ||
| 4762 | 4821 | if ( is_multisite() && is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) { |
| 4763 | 4822 | return true; |
| 4764 | 4823 | } |
| 4765 | 4824 | |
| 4825 | + // Findings about the manifest and the version of Vigilant | |
| 4826 | + // itself are not about one file, so no entry of the list may | |
| 4827 | + // hide them, on a single site either: ignoring the row of | |
| 4828 | + // MANIFEST.sha256 took a replaced manifest out of the email. | |
| 4829 | + // Vigilante_Self_Integrity::filter_ignored_findings() keeps | |
| 4830 | + // them the same way. | |
| 4831 | + if ( is_array( $item ) && 'vigilante_self' === ( $item['type'] ?? '' ) && in_array( $item['self_finding'] ?? '', array( 'manifest_replaced', 'manifest_unverified', 'manifest_missing', 'manifest_invalid', 'self_downgraded' ), true ) ) { | |
| 4832 | + return true; | |
| 4833 | + } | |
| 4834 | + // Nor the findings of the walk of Vigilant's folder (a folder | |
| 4835 | + // that cannot be listed, the folder that could not be walked): | |
| 4836 | + // their path ends in a slash, and ignoring that row left the | |
| 4837 | + // scan with no row and no email while the self-protection | |
| 4838 | + // status stayed critical. | |
| 4839 | + if ( is_array( $item ) && 'vigilante_self' === ( $item['type'] ?? '' ) && '/' === substr( (string) ( $item['file'] ?? '' ), -1 ) ) { | |
| 4840 | + return true; | |
| 4841 | + } | |
| 4842 | + | |
| 4766 | 4843 | $file = is_array( $item ) && isset( $item['file'] ) ? $item['file'] : ''; |
| 4767 | 4844 | return ! in_array( $file, $this->ignored_files, true ); |
| 4768 | 4845 | } |
| 4769 | 4846 | ) |
| @@ -4800,9 +4877,17 @@ | ||
| 4800 | 4877 | // a security-critical finding, same tier as a suspicious file. |
| 4801 | 4878 | $closed_plugins = $this->collect_closed_plugins_for_email(); |
| 4802 | 4879 | $has_closed = ! empty( $closed_plugins ); |
| 4803 | 4880 | |
| 4881 | + /* | |
| 4882 | + * Self-protection is not part of this decision any more. Its alert is | |
| 4883 | + * its own and no setting switches it off, so this email is again about | |
| 4884 | + * the files of the site: core, plugins, themes, uploads and the two | |
| 4885 | + * shared configuration files. | |
| 4886 | + */ | |
| 4804 | 4887 | $has_suspicious = ! empty( $results['suspicious'] ) || ! empty( $results['extra'] ) || $has_critical_config || $has_closed; |
| 4888 | + // Missing files of core, plugins or themes still do not send the email on | |
| 4889 | + // their own: it has no section to list them in, so it would arrive empty. | |
| 4805 | 4890 | $has_modified = ! empty( $results['modified'] ); |
| 4806 | 4891 | |
| 4807 | 4892 | // Instant alert: send for suspicious, extra, critical_config, modified |
| 4808 | 4893 | // files, or closed plugins. |
| @@ -4921,8 +5006,37 @@ | ||
| 4921 | 5006 | $regular_modified[] = $item; |
| 4922 | 5007 | } |
| 4923 | 5008 | } |
| 4924 | 5009 | |
| 5010 | + /* | |
| 5011 | + * Self-protection does not travel in this email any more: it has its own | |
| 5012 | + * alert, which no setting switches off (Vigilante_Self_Integrity:: | |
| 5013 | + * maybe_send_self_alert()). Older stored results can still carry its | |
| 5014 | + * rows, so they are dropped here instead of being listed as ordinary | |
| 5015 | + * files. | |
| 5016 | + */ | |
| 5017 | + foreach ( array( 'suspicious', 'extra', 'missing' ) as $self_bucket ) { | |
| 5018 | + if ( empty( $results[ $self_bucket ] ) || ! is_array( $results[ $self_bucket ] ) ) { | |
| 5019 | + continue; | |
| 5020 | + } | |
| 5021 | + $results[ $self_bucket ] = array_values( | |
| 5022 | + array_filter( | |
| 5023 | + $results[ $self_bucket ], | |
| 5024 | + function ( $item ) { | |
| 5025 | + return ! ( is_array( $item ) && 'vigilante_self' === ( $item['type'] ?? '' ) ); | |
| 5026 | + } | |
| 5027 | + ) | |
| 5028 | + ); | |
| 5029 | + } | |
| 5030 | + $regular_modified = array_values( | |
| 5031 | + array_filter( | |
| 5032 | + $regular_modified, | |
| 5033 | + function ( $item ) { | |
| 5034 | + return ! ( is_array( $item ) && 'vigilante_self' === ( $item['type'] ?? '' ) ); | |
| 5035 | + } | |
| 5036 | + ) | |
| 5037 | + ); | |
| 5038 | + | |
| 4925 | 5039 | $suspicious_count = count( $results['suspicious'] ?? array() ); |
| 4926 | 5040 | $extra_count = count( $results['extra'] ?? array() ); |
| 4927 | 5041 | $critical_config_count = count( $critical_config ); |
| 4928 | 5042 | $modified_count = count( $regular_modified ); |
| @@ -4927,10 +5041,10 @@ | ||
| 4927 | 5041 | $critical_config_count = count( $critical_config ); |
| 4928 | 5042 | $modified_count = count( $regular_modified ); |
| 4929 | 5043 | $closed_count = count( $closed_plugins ); |
| 4930 | 5044 | |
| 4931 | - // Use more urgent subject when suspicious files, critical config changes | |
| 4932 | - // or closed plugins are found (all three are security-critical). | |
| 5045 | + // Use more urgent subject when suspicious files, critical config changes, | |
| 5046 | + // closed plugins or self-integrity findings are found (all security-critical). | |
| 4933 | 5047 | if ( $suspicious_count > 0 || $critical_config_count > 0 || $closed_count > 0 ) { |
| 4934 | 5048 | $subject = sprintf( |
| 4935 | 5049 | /* translators: %s: Site name */ |
| 4936 | 5050 | __( '[%s] SECURITY ALERT: File integrity issues detected', 'vigilante' ), |