PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.2
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.2
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 All 93 releases
← All changes | includes/class-self-integrity.php +62 -664 3.0.5 → 3.0.2 View file →
@@ -134,38 +134,8 @@
134 134 */
135 135 const WATCHDOG_REPEAT_WINDOW = 2592000;
136 136
137 137 /**
138 - * How long after the last restoration a repeated one is still shown
139 - * (7 days). After a week with no task missing the finding closes on its
140 - * own: until 3.0.4 it stayed for the whole 30 days whatever the site did,
141 - * while the screen asked for a new scan that could not clear it.
142 - */
143 - const WATCHDOG_RECENT_WINDOW = 604800;
144 -
145 - /**
146 - * Restorations of the same task inside WATCHDOG_RECENT_WINDOW that mean
147 - * something is removing it over and over. Fewer than that is a warning:
148 - * WordPress itself drops a recurring task when it cannot save its next run
149 - * (wp-cron.php reschedules, and removes the old run even when that
150 - * failed), so two in a month is not evidence of anybody doing anything.
151 - */
152 - const WATCHDOG_ACTIVE_COUNT = 3;
153 -
154 - /**
155 - * Restorations of the same task inside WATCHDOG_REPEAT_WINDOW that mean
156 - * the same thing on a site whose administrator comes by once a week. The
157 - * watchdog only runs when an administrator opens wp-admin or from the
158 - * daily maintenance, which is itself one of the tasks being removed, so
159 - * with the week alone such a site never got past the warning however long
160 - * it went on. Found by the cross review of B77. Since the watchdog also
161 - * runs from the scheduled tasks of WordPress itself (see
162 - * run_watchdog_from_core_cron()) this is what tells apart something that
163 - * removes the tasks once a week.
164 - */
165 - const WATCHDOG_MONTH_COUNT = 4;
166 -
167 - /**
168 138 * A check older than this stops counting as a result. Something that stops
169 139 * the check (a filter, a removed hook, a cron nobody runs, files edited)
170 140 * leaves the last state frozen, and a green from three days ago read as
171 141 * "verified" would be the screen lying by omission.
@@ -245,29 +215,8 @@
245 215 */
246 216 private static $executable_names = array( '.htaccess', '.user.ini', 'php.ini' );
247 217
248 218 /**
249 - * Scheduled tasks of WordPress itself that the watchdog also runs from.
250 - *
251 - * The update checks are scheduled twice a day and put back by core on
252 - * every page load when they are missing (wp_schedule_update_checks(), on
253 - * init); the two daily cleanups are put back from every admin page
254 - * (wp-admin/admin.php). None of them depends on a task of Vigilant being
255 - * there, which is the point.
256 - *
257 - * @var array
258 - */
259 - private static $core_tasks = array( 'wp_version_check', 'wp_update_plugins', 'wp_scheduled_delete', 'delete_expired_transients' );
260 -
261 - /**
262 - * Whether the watchdog already ran from a task of WordPress in this
263 - * process: several of them fall due in the same cron run.
264 - *
265 - * @var bool
266 - */
267 - private $core_cron_pass_done = false;
268 -
269 - /**
270 219 * Constructor. Registers no hooks, see init_hooks().
271 220 *
272 221 * @param Vigilante_Settings $settings Settings instance.
273 222 * @param Vigilante_Activity_Log|null $activity_log Activity log instance.
@@ -285,15 +234,8 @@
285 234 */
286 235 public function init_hooks() {
287 236 add_action( 'admin_init', array( $this, 'maybe_detect_version_change' ), 20 );
288 237 add_action( 'admin_init', array( $this, 'maybe_run_watchdog' ), 30 );
289 - // Before the callback of WordPress itself, which runs at priority 10:
290 - // the one of wp_version_check is what starts the background updates
291 - // (wp_maybe_auto_update), and a check that ran right behind it found
292 - // the new files of Vigilant with the old code still in memory.
293 - foreach ( self::$core_tasks as $core_task ) {
294 - add_action( $core_task, array( $this, 'run_watchdog_from_core_cron' ), 5 );
295 - }
296 238 // Last of all, to see whether the two above are still there: code that
297 239 // runs inside WordPress can unhook them, and a plugin that does that
298 240 // leaves every screen showing the last result as if nothing happened.
299 241 add_action( 'admin_init', array( $this, 'verify_hooks' ), PHP_INT_MAX );
@@ -299,10 +241,9 @@
299 241 add_action( 'admin_init', array( $this, 'verify_hooks' ), PHP_INT_MAX );
300 242 }
301 243
302 244 /**
303 - * Are the entry points of the self-check still hooked? The two of
304 - * admin_init, and the one that hangs from the scheduled tasks of WordPress.
245 + * Are the two entry points of the self-check still hooked?
305 246 *
306 247 * Runs at the end of admin_init, so anything that unhooked them earlier in
307 248 * the same request is visible here. It cannot say which file called
308 249 * remove_action (WordPress fires nothing when a callback is removed, and by
@@ -326,20 +267,8 @@
326 267 if ( false === has_action( 'admin_init', array( $this, $method ) ) ) {
327 268 $missing[] = $method;
328 269 }
329 270 }
330 - // The entry point that does not depend on anybody opening wp-admin:
331 - // gone from every task of WordPress it was hooked to.
332 - $on_core = false;
333 - foreach ( self::$core_tasks as $core_task ) {
334 - if ( false !== has_action( $core_task, array( $this, 'run_watchdog_from_core_cron' ) ) ) {
335 - $on_core = true;
336 - break;
337 - }
338 - }
339 - if ( ! $on_core ) {
340 - $missing[] = 'run_watchdog_from_core_cron';
341 - }
342 271
343 272 $state = $this->get_state();
344 273 if ( empty( $missing ) ) {
345 274 if ( ! empty( $state['hooks_removed'] ) ) {
@@ -1801,96 +1730,14 @@
1801 1730 $this->detect_version_change();
1802 1731 }
1803 1732
1804 1733 /**
1805 - * Whether the WordPress updater has replaced Vigilant in this request and
1806 - * the check of the new files is still to come
1807 - *
1808 - * vigilante_on_upgrader_process_complete() leaves that check for the end
1809 - * of the request, once a failed update has had its previous copy restored.
1810 - * Until then the code in memory is the old one and the files on disk are
1811 - * the new ones, or half moved: taking that for a version change made
1812 - * outside the updater, or checking the files from here, reported the
1813 - * update WordPress had just made. It happens in one cron run, when a
1814 - * background update is followed by the daily maintenance or by a task of
1815 - * WordPress the watchdog hangs from.
1816 - *
1817 - * @since 3.0.5
1818 - *
1819 - * @return bool
1820 - */
1821 - private static function upgrade_check_pending() {
1822 - return false !== has_action( 'shutdown', 'vigilante_verify_after_upgrade' );
1823 - }
1824 -
1825 - /**
1826 - * Whether WordPress is in the middle of its background updates, in this
1827 - * process or in another one
1828 - *
1829 - * WP_Automatic_Updater::run() holds a lock in the options table while it
1830 - * works (WP_Upgrader::create_lock( 'auto_updater' )) and releases it at
1831 - * the end; core itself takes it for abandoned after an hour. A mark dated
1832 - * ahead of this server's clock counts only within five minutes, which is
1833 - * a clock slightly out of step between two servers: with a whole hour of
1834 - * margin each way, one write kept every check waiting for almost two
1835 - * (the sixth blind round).
1836 - *
1837 - * The row is read from the table, not through get_option(). Releasing
1838 - * the lock is a delete_option(), which notes the name as an option that
1839 - * does not exist; the next lock is inserted with a query of its own, and
1840 - * the update_option() that follows it writes the same value within the
1841 - * same second, changes no row and leaves that note where it was. From
1842 - * then on get_option() answers that there is no lock while there is one:
1843 - * for the rest of the process on any site, and for every process on a
1844 - * site with a persistent object cache (wp-admin/includes/class-wp-upgrader.php
1845 - * :1065, :1087 and :1103 in 7.1.3).
1846 - *
1847 - * On a network the lock is in the options of the main site of the main
1848 - * network, the only one where the updates run (class-wp-automatic-updater.php
1849 - * :647), so it is looked for there from wherever this is asked.
1850 - *
1851 - * @since 3.0.5
1852 - *
1853 - * @return bool
1854 - */
1855 - private static function core_updates_running() {
1856 - global $wpdb;
1857 -
1858 - $switched = false;
1859 - if ( is_multisite() && ! ( is_main_site() && is_main_network() ) ) {
1860 - $main_site = (int) get_main_site_id( get_main_network_id() );
1861 - if ( $main_site < 1 ) {
1862 - return false;
1863 - }
1864 - switch_to_blog( $main_site );
1865 - $switched = true;
1866 - }
1867 -
1868 - $lock = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'auto_updater.lock' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the cached answer is what cannot be trusted here: see the note above.
1869 -
1870 - if ( $switched ) {
1871 - restore_current_blog();
1872 - }
1873 -
1874 - if ( ! is_numeric( $lock ) ) {
1875 - return false;
1876 - }
1877 - $age = time() - (int) $lock;
1878 -
1879 - return $age < HOUR_IN_SECONDS && $age > -5 * MINUTE_IN_SECONDS;
1880 - }
1881 -
1882 - /**
1883 1734 * FTP/manual update and downgrade detection: the anchored version in the
1884 1735 * state differs from the running code. Also called from the daily
1885 - * maintenance cron and from the scheduled tasks of WordPress. Throttled
1886 - * while a change stays unresolved, and silent while the check that follows
1887 - * an update by WordPress is pending (see upgrade_check_pending()) or
1888 - * WordPress is running its background updates in another process (see
1889 - * core_updates_running()).
1736 + * maintenance cron. Throttled while a change stays unresolved.
1890 1737 */
1891 1738 public function detect_version_change() {
1892 - if ( ! $this->is_enabled() || self::upgrade_check_pending() ) {
1739 + if ( ! $this->is_enabled() ) {
1893 1740 return;
1894 1741 }
1895 1742
1896 1743 $state = $this->get_state();
@@ -1903,24 +1750,11 @@
1903 1750 if ( '' !== $stored_version && $running === $stored_version ) {
1904 1751 return;
1905 1752 }
1906 1753
1907 - // A version that differs while WordPress holds the lock of its
1908 - // background updates is, almost always, WordPress replacing Vigilant
1909 - // in another process: the daily maintenance that falls due in a second
1910 - // cron process, or an administrator opening the dashboard meanwhile,
1911 - // found the new files and took them for a change made outside the
1912 - // updater (the blind review of 3.0.5). The process that updates checks
1913 - // them when it ends. Asked only once the version has changed and the
1914 - // throttle allows an attempt, so a page of the dashboard does not read
1915 - // the table for it while a change stays unresolved, and before the
1916 - // throttle is set, so waiting does not use up the next attempt.
1917 1754 if ( get_transient( self::VERSION_CHECK_TRANSIENT ) ) {
1918 1755 return;
1919 1756 }
1920 - if ( self::core_updates_running() ) {
1921 - return;
1922 - }
1923 1757 set_transient( self::VERSION_CHECK_TRANSIENT, 1, self::VERSION_CHECK_THROTTLE );
1924 1758
1925 1759 if ( '' === $stored_version ) {
1926 1760 // Feature just arrived (fresh install handled by the activator,
@@ -1980,61 +1814,8 @@
1980 1814 $this->run_watchdog();
1981 1815 }
1982 1816
1983 1817 /**
1984 - * The same self-protection the daily maintenance runs, from the scheduled
1985 - * tasks of WordPress itself
1986 - *
1987 - * The two other entry points depend on what the watchdog watches: an
1988 - * administrator opening wp-admin, and the daily maintenance of Vigilant,
1989 - * which is one of the tasks that get removed. On a site where something
1990 - * removed every task, the daily maintenance and the weekly Security Check
1991 - * stayed missing until an administrator came by, and what the watchdog
1992 - * counted was those visits: coming by every ten days, it never reached
1993 - * the count that makes it critical (the blind review of 3.0.5). The update
1994 - * checks of WordPress run twice a day whether or not the tasks of Vigilant
1995 - * are there, so a missing task is put back within hours and the count
1996 - * follows what happens to the tasks, not who visits.
1997 - *
1998 - * Cron only: these actions have the same names as functions that
1999 - * WordPress calls directly on other requests, and this must not run on a
2000 - * page load. And only on the site that owns the files of the installation:
2001 - * every site of a network runs its own cron, the files are the same for
2002 - * all of them, and the main site already checks them.
2003 - *
2004 - * @since 3.0.5
2005 - */
2006 - public function run_watchdog_from_core_cron() {
2007 - if ( $this->core_cron_pass_done || ! wp_doing_cron() ) {
2008 - return;
2009 - }
2010 - $this->core_cron_pass_done = true;
2011 - if ( ! Vigilante_Settings::owns_shared_files() ) {
2012 - return;
2013 - }
2014 - // upgrade_check_pending() is only known inside the process that runs
2015 - // the update. A second cron process that starts while the first one is
2016 - // still inside the background updates of WordPress finds Vigilant
2017 - // already replaced and nothing pending, and this pass would take the
2018 - // new version for a change made outside the updater. It comes back
2019 - // with the next task of WordPress, within hours. The two checks it
2020 - // would run ask the same thing by themselves, for their other callers;
2021 - // asked here too, this entry does not schedule anything meanwhile.
2022 - if ( self::core_updates_running() ) {
2023 - return;
2024 - }
2025 -
2026 - // Same order as Vigilante_Main::daily_maintenance(), and for the same
2027 - // reasons: the version change first, and switched off is said aloud.
2028 - if ( ! $this->is_enabled() ) {
2029 - $this->audit_off_state();
2030 - return;
2031 - }
2032 - $this->detect_version_change();
2033 - $this->run_watchdog();
2034 - }
2035 -
2036 - /**
2037 1818 * Verify that Vigilant's own cron events are still scheduled and restore
2038 1819 * any that were unscheduled (a malicious wp_clear_scheduled_hook, or an
2039 1820 * overzealous cleanup plugin, silences the plugin forever otherwise).
2040 1821 *
@@ -2040,18 +1821,12 @@
2040 1821 *
2041 1822 * A hook is only reported once the watchdog has seen it scheduled: the
2042 1823 * first pass on a site, and a hook whose setting was just switched on,
2043 1824 * schedule what is missing quietly. After that, a first disappearance is
2044 - * restored with a line in the log. The SAME hook missing again within 30
2045 - * days is a warning on screen, with no email and no effect on the scores,
2046 - * and it closes on its own after a week with nothing missing: WordPress
2047 - * drops a recurring task by itself when it cannot save its next run, so
2048 - * that much proves nothing. Three times inside one week, or four inside
2049 - * the 30 days, means something is actively clearing it: critical and a
2050 - * standalone alert, one a week at most, which says tasks and not files. Until 3.0.4 the second time was already critical,
2051 - * the texts said that the files of the plugin had been modified, and the
2052 - * finding stayed for 30 days. A hook whose setting is off is forgotten, so
2053 - * switching it back on is not reported either.
1825 + * restored with a warning; the SAME hook disappearing again within 30
1826 + * days means something is actively clearing it: critical and a standalone
1827 + * alert. A hook whose setting is off is forgotten, so switching it back on
1828 + * is not reported either.
2054 1829 *
2055 1830 * On a network it only watches the site that owns the installation's
2056 1831 * shared files: the activation schedules these events once, so on the
2057 1832 * other sites "not scheduled" does not mean "removed". The events of the
@@ -2075,13 +1850,11 @@
2075 1850 // hook => array( schedule, first-run offset, required? ).
2076 1851 // The weekly analyzer scan is required unless it was switched off
2077 1852 // explicitly: its own toggle unschedules it on purpose, and an
2078 1853 // unset value means on (Vigilante_Security_Analyzer reads it the same way).
2079 - // vigilante_hourly_checks is not here since 3.0.5: its callback
2080 - // does nothing, so it missing takes nothing away, and being hourly
2081 - // it was the task WordPress dropped most often.
2082 1854 $expected = array(
2083 1855 'vigilante_daily_maintenance' => array( 'daily', 0, true ),
1856 + 'vigilante_hourly_checks' => array( 'hourly', 0, true ),
2084 1857 'vigilante_analyzer_weekly_scan' => array( 'weekly', DAY_IN_SECONDS, ! isset( $analyzer['weekly_scan_enabled'] ) || ! empty( $analyzer['weekly_scan_enabled'] ) ),
2085 1858 'vigilante_plugin_status_check' => array( 'daily', HOUR_IN_SECONDS, ! empty( $fi['check_closed_plugins'] ) ),
2086 1859 'vigilante_file_integrity_scan' => array(
2087 1860 isset( $fi['scan_frequency'] ) ? $fi['scan_frequency'] : 'daily',
@@ -2107,13 +1880,8 @@
2107 1880 $restored = array();
2108 1881 $repeated = array();
2109 1882 $now = time();
2110 1883
2111 - // What is no longer watched is forgotten, with whatever it had
2112 - // counted: the hourly task of the versions up to 3.0.4.
2113 - $seen = array_intersect_key( $seen, $expected );
2114 - $watchdog = array_intersect_key( $watchdog, $expected );
2115 -
2116 1884 foreach ( $expected as $hook => $spec ) {
2117 1885 list( $schedule, $offset, $required ) = $spec;
2118 1886
2119 1887 if ( ! $required ) {
@@ -2137,23 +1905,18 @@
2137 1905 $baselined[] = $hook;
2138 1906 continue;
2139 1907 }
2140 1908
2141 - // Every restoration of the last 30 days, with its date: how
2142 - // close together they are is what tells a task WordPress lost
2143 - // from one that something keeps removing.
2144 - $previous = ( isset( $watchdog[ $hook ] ) && is_array( $watchdog[ $hook ] ) ) ? $watchdog[ $hook ] : array();
2145 - $times = self::watchdog_times( $previous, $now, true );
2146 - $times[] = $now;
2147 - $times = array_slice( $times, -10 );
1909 + $previous = ( isset( $watchdog[ $hook ] ) && is_array( $watchdog[ $hook ] ) ) ? $watchdog[ $hook ] : array();
1910 + $recent = isset( $previous['last'] ) && ( $now - (int) $previous['last'] ) < self::WATCHDOG_REPEAT_WINDOW;
1911 + $times = $recent && isset( $previous['count'] ) ? (int) $previous['count'] + 1 : 1;
2148 1912
2149 1913 $watchdog[ $hook ] = array(
2150 - 'count' => count( $times ),
1914 + 'count' => $times,
2151 1915 'last' => $now,
2152 - 'times' => $times,
2153 1916 );
2154 1917 $restored[] = $hook;
2155 - if ( 'critical' === self::watchdog_severity( $watchdog[ $hook ], $now ) ) {
1918 + if ( $times >= 2 ) {
2156 1919 $repeated[] = $hook;
2157 1920 }
2158 1921 }
2159 1922
@@ -2189,50 +1952,25 @@
2189 1952 empty( $repeated ) ? 'warning' : 'critical'
2190 1953 );
2191 1954 }
2192 1955
2193 - // What is critical right now, and not only what this pass put
2194 - // back: an email that could not be sent is tried again by the next
2195 - // pass for as long as the finding lasts, instead of waiting for
2196 - // one more removal that may never come.
2197 - $findings = array();
2198 - foreach ( self::watchdog_findings( $state ) as $finding ) {
2199 - if ( 'critical' !== $finding['severity'] ) {
2200 - continue;
1956 + if ( ! empty( $repeated ) ) {
1957 + $findings = array();
1958 + foreach ( $repeated as $hook ) {
1959 + $findings[] = $this->finding(
1960 + 'cron_cleared_repeatedly',
1961 + $hook,
1962 + 'critical',
1963 + sprintf(
1964 + /* translators: 1: cron hook name, 2: number of restorations */
1965 + __( 'The cron event "%1$s" keeps being unscheduled (restored %2$d times in 30 days). Something on this site is actively clearing Vigilant\'s scheduled tasks.', 'vigilante' ),
1966 + $hook,
1967 + (int) $watchdog[ $hook ]['count']
1968 + )
1969 + );
2201 1970 }
2202 - $findings[] = $this->finding(
2203 - 'cron_cleared_repeatedly',
2204 - $finding['file'],
2205 - 'critical',
2206 - sprintf(
2207 - /* translators: %s: cron hook name */
2208 - __( 'The scheduled task "%s" keeps going missing, and Vigilant scheduled it again each time. Something on this site keeps removing it.', 'vigilante' ),
2209 - $finding['file']
2210 - )
2211 - );
1971 + $this->maybe_send_self_alert( $findings, 'watchdog' );
2212 1972 }
2213 -
2214 - if ( ! empty( $findings ) ) {
2215 - /*
2216 - * One email a week at most while it goes on, kept apart from
2217 - * the fingerprint of the alerts about files. That fingerprint
2218 - * is cleared by every clean check of the files, so with it
2219 - * alone this email went out again every day; and with a
2220 - * standing warning about the files it was never cleared, so a
2221 - * second episode of the same tasks went out to nobody.
2222 - */
2223 - $state = $this->get_state();
2224 - $alerted = isset( $state['watchdog_alerted'] ) ? (int) $state['watchdog_alerted'] : 0;
2225 - if ( $alerted > $now + DAY_IN_SECONDS ) {
2226 - // Written with the clock ahead: it would hold the email back until then.
2227 - $alerted = 0;
2228 - }
2229 - if ( ( $now - $alerted ) >= self::WATCHDOG_RECENT_WINDOW && $this->maybe_send_self_alert( $findings, 'watchdog', false ) ) {
2230 - $state = $this->get_state();
2231 - $state['watchdog_alerted'] = $now;
2232 - $this->save_state( $state );
2233 - }
2234 - }
2235 1973 }
2236 1974
2237 1975 // Fallback file self-check whenever nothing checked the files in the
2238 1976 // last day: the File Integrity module switched off, scheduled scans
@@ -2239,9 +1977,9 @@
2239 1977 // switched off, or a weekly schedule. Asking only for the module left a
2240 1978 // site with scheduled scans switched off with no periodic check at all.
2241 1979 $state = $this->get_state();
2242 1980 $last_check = isset( $state['last_check'] ) ? (int) $state['last_check'] : 0;
2243 - if ( ! self::upgrade_check_pending() && time() - $last_check > DAY_IN_SECONDS && ! self::core_updates_running() ) {
1981 + if ( time() - $last_check > DAY_IN_SECONDS ) {
2244 1982 $result = $this->run_check( 'watchdog' );
2245 1983 if ( 'critical' === $this->worst_severity( $result['findings'] ) ) {
2246 1984 $this->maybe_send_self_alert( $result['findings'], 'watchdog' );
2247 1985 }
@@ -2268,26 +2006,22 @@
2268 2006 * set; cleared when back to ok.
2269 2007 *
2270 2008 * @param array $findings Findings to report.
2271 2009 * @param string $context Alert context (upgrader|version_change|watchdog).
2272 - * @param bool $by_set Whether the set of findings is what keeps it from
2273 - * being sent twice. False for the watchdog, which
2274 - * keeps its own count of time (see run_watchdog()).
2275 - * @return bool Whether it was sent.
2276 2010 */
2277 - public function maybe_send_self_alert( $findings, $context, $by_set = true ) {
2011 + public function maybe_send_self_alert( $findings, $context ) {
2278 2012 if ( empty( $findings ) ) {
2279 - return false;
2013 + return;
2280 2014 }
2281 2015
2282 2016 if ( ! Vigilante_Settings::owns_shared_files() ) {
2283 - return false;
2017 + return;
2284 2018 }
2285 2019 $summary = $this->findings_summary( $findings );
2286 2020 $fingerprint = md5( wp_json_encode( $summary ) );
2287 2021 $state = $this->get_state();
2288 - if ( $by_set && isset( $state['alerted_fingerprint'] ) && $fingerprint === $state['alerted_fingerprint'] ) {
2289 - return false;
2022 + if ( isset( $state['alerted_fingerprint'] ) && $fingerprint === $state['alerted_fingerprint'] ) {
2023 + return;
2290 2024 }
2291 2025
2292 2026 if ( ! class_exists( 'Vigilante_Email_Template' ) ) {
2293 2027 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
@@ -2305,54 +2039,17 @@
2305 2039 }
2306 2040 $recipients = array_values( array_unique( $recipients ) );
2307 2041 }
2308 2042 if ( empty( $recipients ) ) {
2309 - return false;
2043 + return;
2310 2044 }
2311 2045
2312 2046 $worst = $this->worst_severity( $findings );
2313 2047 $site_name = get_bloginfo( 'name' );
2314 - // Tasks that keep being removed, the check switched off and its hooks
2315 - // removed are not files that changed, and the email said files for all
2316 - // of them until 3.0.4.
2317 - $kind = ( 'critical' === $worst ) ? self::findings_kind( $findings, 'critical' ) : '';
2318 - // Switched off by code after the last check had found changed files:
2319 - // the files travel in the alert, but they were already announced when
2320 - // they were found. What is new, and what a scan from File Integrity
2321 - // will not look at, is that the check has been switched off.
2322 - $off_after = ( 'files' === $kind ) && self::has_code( $findings, array( 'self_disabled' ) );
2323 - if ( $off_after ) {
2324 - $kind = 'off';
2325 - }
2326 - $tasks = 'tasks' === $kind;
2327 - $check_off = in_array( $kind, array( 'off', 'hooks' ), true );
2328 2048
2329 - if ( $check_off ) {
2330 - if ( 'off' === $kind ) {
2331 - /* translators: %s: Site name */
2332 - $subject_text = __( '[%s] CRITICAL: Vigilant self-protection is switched off by code', 'vigilante' );
2333 - $lead = __( 'Vigilant self-protection is switched off by code on this site.', 'vigilante' );
2334 - } else {
2335 - /* translators: %s: Site name */
2336 - $subject_text = __( '[%s] CRITICAL: the hooks of Vigilant self-protection were removed', 'vigilante' );
2337 - $lead = __( 'Something removed the hooks that run the self-check.', 'vigilante' );
2338 - }
2339 - $subject = sprintf( $subject_text, $site_name );
2340 - $title = __( 'Vigilant self-protection alert', 'vigilante' );
2341 - $intro = $lead . ' ' . ( $off_after
2342 - ? __( 'Before that, the last check that ran had found changed files in Vigilant, and they are listed below. Unless you did both things yourself, treat this as a compromise: changing the security plugin and then switching off the check that reports it is what an attacker does.', 'vigilante' )
2343 - : __( 'This alert is about the check itself, not about changed files. While this goes on, the files of Vigilant are not being checked as they should. If you did not do this yourself, treat it as a possible compromise.', 'vigilante' ) );
2344 - } elseif ( $tasks ) {
2049 + if ( 'critical' === $worst ) {
2345 2050 $subject = sprintf(
2346 2051 /* translators: %s: Site name */
2347 - __( '[%s] CRITICAL: Vigilant scheduled tasks keep being removed', 'vigilante' ),
2348 - $site_name
2349 - );
2350 - $title = __( 'Vigilant self-protection alert', 'vigilante' );
2351 - $intro = __( 'Scheduled tasks of Vigilant keep going missing, and Vigilant scheduled them again each time. This alert is about scheduled tasks, not about changed files. Something on this site keeps removing them: a cleanup plugin or a scheduled job is the usual cause, and if you find neither, treat it as a possible compromise.', 'vigilante' );
2352 - } elseif ( 'critical' === $worst ) {
2353 - $subject = sprintf(
2354 - /* translators: %s: Site name */
2355 2052 __( '[%s] CRITICAL: Vigilant plugin files were modified', 'vigilante' ),
2356 2053 $site_name
2357 2054 );
2358 2055 $title = __( 'Vigilant self-protection alert', 'vigilante' );
@@ -2368,17 +2065,11 @@
2368 2065 }
2369 2066
2370 2067 $body = Vigilante_Email_Template::p( $intro );
2371 2068 $body .= self::build_self_email_section( $findings );
2372 - if ( $tasks ) {
2373 - $how = __( 'How to see them by yourself: a plugin such as WP Crontrol lists the scheduled events of your site, and the ones of Vigilant start with vigilante_.', 'vigilante' );
2374 - } elseif ( $check_off ) {
2375 - // A scan does not check these files while the check is switched off.
2376 - $how = __( 'How to check the files of Vigilant by yourself meanwhile: over SSH, in the folder of the plugin, with "php bin/verify-manifest.php" or "sha256sum -c MANIFEST.sha256". Full instructions live in the SECURITY.md file distributed with the plugin.', 'vigilante' );
2377 - } else {
2378 - $how = __( 'How to verify by yourself: run a scan from File Integrity, or check the folder over SSH with "php bin/verify-manifest.php" or "sha256sum -c MANIFEST.sha256". Full instructions live in the SECURITY.md file distributed with the plugin.', 'vigilante' );
2379 - }
2380 - $body .= Vigilante_Email_Template::p( $how );
2069 + $body .= Vigilante_Email_Template::p(
2070 + __( 'How to verify by yourself: run a scan from File Integrity, or check the folder over SSH with "php bin/verify-manifest.php" or "sha256sum -c MANIFEST.sha256". Full instructions live in the SECURITY.md file distributed with the plugin.', 'vigilante' )
2071 + );
2381 2072 $body .= Vigilante_Email_Template::button(
2382 2073 admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-last-scan' ),
2383 2074 __( 'Review in Vigilant', 'vigilante' )
2384 2075 );
@@ -2397,17 +2088,13 @@
2397 2088 // several recipients false is not proof of anything (PHPMailer
2398 2089 // returns it when one of them is refused and the others were sent),
2399 2090 // so those count as sent, or the alert would be repeated to
2400 2091 // everybody who did get it.
2401 - return false;
2092 + return;
2402 2093 }
2403 2094
2404 - if ( $by_set ) {
2405 - $state['alerted_fingerprint'] = $fingerprint;
2406 - $this->save_state( $state );
2407 - }
2408 -
2409 - return true;
2095 + $state['alerted_fingerprint'] = $fingerprint;
2096 + $this->save_state( $state );
2410 2097 }
2411 2098
2412 2099 /**
2413 2100 * Red email sub-section for self findings, mirroring the closed-plugins
@@ -2422,28 +2109,11 @@
2422 2109 }
2423 2110
2424 2111 $html = '<div style="background:#fef1f1;border-left:4px solid #d63638;padding:12px 16px;margin:16px 0;">';
2425 2112 $html .= '<h2 style="margin:0 0 8px;font-size:16px;color:#d63638;">' . esc_html__( 'Vigilant self-protection', 'vigilante' ) . '</h2>';
2426 - // What the rows are, so the line above them and the first column say it.
2427 - $kind = self::findings_kind( $findings );
2428 - if ( 'files' === $kind && self::has_code( $findings, array( 'self_disabled', 'self_hooks_removed' ) ) ) {
2429 - // Both things: a row about the check itself is not a file of Vigilant.
2430 - $about = __( 'Findings about the self-check of Vigilant and about its own files.', 'vigilante' );
2431 - $column = __( 'File', 'vigilante' );
2432 - } elseif ( 'tasks' === $kind ) {
2433 - $about = __( 'Findings about the scheduled tasks of Vigilant, not about its files.', 'vigilante' );
2434 - $column = __( 'Task', 'vigilante' );
2435 - } elseif ( 'off' === $kind || 'hooks' === $kind ) {
2436 - $about = __( 'Findings about the self-check of Vigilant itself, not about changed files.', 'vigilante' );
2437 - // Switched off by code lists the files that hook the filter.
2438 - $column = ( 'hooks' === $kind ) ? __( 'Hook', 'vigilante' ) : __( 'File', 'vigilante' );
2439 - } else {
2440 - $about = __( 'Findings about Vigilant\'s own files. If you did not modify them yourself, treat this as a possible compromise.', 'vigilante' );
2441 - $column = __( 'File', 'vigilante' );
2442 - }
2443 - $html .= '<p style="margin:0 0 10px;color:#3c434a;">' . esc_html( $about ) . '</p>';
2113 + $html .= '<p style="margin:0 0 10px;color:#3c434a;">' . esc_html__( 'Findings about Vigilant\'s own files. If you did not modify them yourself, treat this as a possible compromise.', 'vigilante' ) . '</p>';
2444 2114 $html .= '<table style="border-collapse:collapse;width:100%;font-size:13px;">';
2445 - $html .= '<tr><th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html( $column ) . '</th>'
2115 + $html .= '<tr><th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html__( 'File', 'vigilante' ) . '</th>'
2446 2116 . '<th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Finding', 'vigilante' ) . '</th>'
2447 2117 . '<th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Severity', 'vigilante' ) . '</th></tr>';
2448 2118
2449 2119 foreach ( array_slice( $findings, 0, self::MAX_STORED_FINDINGS ) as $finding ) {
@@ -2517,117 +2187,12 @@
2517 2187 return ( $there > $here ) ? $main : $state;
2518 2188 }
2519 2189
2520 2190 /**
2521 - * The scheduled tasks the watchdog looks after
2191 + * Cron events the watchdog had to schedule again more than once inside its
2192 + * window. They are logged and emailed when they happen, but they are not
2193 + * part of last_findings, so the screens read them from the watchdog state.
2522 2194 *
2523 - * The same names as the table of run_watchdog(). A task left out of here
2524 - * is not reported even if an older version left a count for it in the
2525 - * state: the hourly task up to 3.0.4.
2526 - *
2527 - * @since 3.0.5
2528 - *
2529 - * @return string[]
2530 - */
2531 - public static function watched_hooks() {
2532 - return array(
2533 - 'vigilante_daily_maintenance',
2534 - 'vigilante_analyzer_weekly_scan',
2535 - 'vigilante_plugin_status_check',
2536 - 'vigilante_file_integrity_scan',
2537 - );
2538 - }
2539 -
2540 - /**
2541 - * When a task was restored, over the last 30 days
2542 - *
2543 - * An entry written up to 3.0.4 holds a count and the date of the last one
2544 - * only. It is read as that date twice at most: enough for the warning it
2545 - * had earned, never for the critical one, whose dates are not known.
2546 - *
2547 - * @since 3.0.5
2548 - *
2549 - * That reading is for the screens. To add a restoration to such an entry
2550 - * only the one date it really holds is kept: carrying the made-up second
2551 - * one forward turned "once three weeks ago and once three days ago" into
2552 - * three this week on the next loss, which is critical and an email.
2553 - *
2554 - * @param array $entry One entry of the watchdog state.
2555 - * @param int|null $now Current time.
2556 - * @param bool $to_keep Whether the list is going to be stored with a new date added.
2557 - * @return int[] Timestamps, oldest first.
2558 - */
2559 - public static function watchdog_times( $entry, $now = null, $to_keep = false ) {
2560 - $now = null === $now ? time() : (int) $now;
2561 - $times = array();
2562 -
2563 - if ( ! is_array( $entry ) ) {
2564 - return $times;
2565 - }
2566 -
2567 - if ( isset( $entry['times'] ) && is_array( $entry['times'] ) ) {
2568 - foreach ( $entry['times'] as $time ) {
2569 - // Whole numbers only: the state is an option, and anything
2570 - // else in it is not a date.
2571 - if ( is_int( $time ) || ( is_string( $time ) && ctype_digit( $time ) ) ) {
2572 - $times[] = (int) $time;
2573 - }
2574 - }
2575 - } elseif ( isset( $entry['last'] ) && ( is_int( $entry['last'] ) || ( is_string( $entry['last'] ) && ctype_digit( $entry['last'] ) ) ) && (int) $entry['last'] > 0 ) {
2576 - $count = ( isset( $entry['count'] ) && is_numeric( $entry['count'] ) ) ? (int) $entry['count'] : 0;
2577 - $times = array_fill( 0, $to_keep ? 1 : max( 1, min( 2, $count ) ), (int) $entry['last'] );
2578 - }
2579 -
2580 - // A date more than a day ahead was written with the clock wrong: kept,
2581 - // it would count as recent for as long as it stayed ahead.
2582 - $recent = array();
2583 - foreach ( $times as $time ) {
2584 - if ( $time > 0 && $time <= $now + DAY_IN_SECONDS && ( $now - $time ) < self::WATCHDOG_REPEAT_WINDOW ) {
2585 - $recent[] = $time;
2586 - }
2587 - }
2588 - sort( $recent, SORT_NUMERIC );
2589 -
2590 - return $recent;
2591 - }
2592 -
2593 - /**
2594 - * How serious the restorations of one task are, right now
2595 - *
2596 - * Nothing while the last one is more than a week old, so the finding
2597 - * closes on its own once the task stays put. A warning from the second one
2598 - * in 30 days. Critical from the third inside one week, or the fourth
2599 - * inside the 30 days.
2600 - *
2601 - * @since 3.0.5
2602 - *
2603 - * @param array $entry One entry of the watchdog state.
2604 - * @param int|null $now Current time.
2605 - * @return string '', warning or critical.
2606 - */
2607 - public static function watchdog_severity( $entry, $now = null ) {
2608 - $now = null === $now ? time() : (int) $now;
2609 - $times = self::watchdog_times( $entry, $now );
2610 - $week = 0;
2611 -
2612 - foreach ( $times as $time ) {
2613 - if ( ( $now - $time ) < self::WATCHDOG_RECENT_WINDOW ) {
2614 - ++$week;
2615 - }
2616 - }
2617 -
2618 - if ( $week < 1 || count( $times ) < 2 ) {
2619 - return '';
2620 - }
2621 -
2622 - return ( $week >= self::WATCHDOG_ACTIVE_COUNT || count( $times ) >= self::WATCHDOG_MONTH_COUNT ) ? 'critical' : 'warning';
2623 - }
2624 -
2625 - /**
2626 - * Scheduled tasks the watchdog had to schedule again more than once. They
2627 - * are logged when they happen, but they are not part of last_findings, so
2628 - * the screens read them from the watchdog state.
2629 - *
2630 2195 * @param array $state State.
2631 2196 * @return array Findings, in the same shape as last_findings.
2632 2197 */
2633 2198 public static function watchdog_findings( $state ) {
@@ -2632,22 +2197,21 @@
2632 2197 */
2633 2198 public static function watchdog_findings( $state ) {
2634 2199 $findings = array();
2635 2200 $watchdog = ( isset( $state['watchdog'] ) && is_array( $state['watchdog'] ) ) ? $state['watchdog'] : array();
2636 - $watched = self::watched_hooks();
2637 -
2638 2201 foreach ( $watchdog as $hook => $entry ) {
2639 - if ( ! is_array( $entry ) || ! in_array( (string) $hook, $watched, true ) ) {
2202 + if ( ! is_array( $entry ) ) {
2640 2203 continue;
2641 2204 }
2642 - $severity = self::watchdog_severity( $entry );
2643 - if ( '' === $severity ) {
2205 + $count = isset( $entry['count'] ) ? (int) $entry['count'] : 0;
2206 + $last = isset( $entry['last'] ) ? (int) $entry['last'] : 0;
2207 + if ( $count < 2 || ( time() - $last ) >= self::WATCHDOG_REPEAT_WINDOW ) {
2644 2208 continue;
2645 2209 }
2646 2210 $findings[] = array(
2647 2211 'code' => 'cron_cleared_repeatedly',
2648 2212 'file' => (string) $hook,
2649 - 'severity' => $severity,
2213 + 'severity' => 'critical',
2650 2214 'message' => '',
2651 2215 );
2652 2216 }
2653 2217 return $findings;
@@ -2653,136 +2217,8 @@
2653 2217 return $findings;
2654 2218 }
2655 2219
2656 2220 /**
2657 - * What a set of findings is about
2658 - *
2659 - * Not every critical finding of the self-check is a changed file: the
2660 - * check can be switched off through its filter, its hooks can be removed,
2661 - * and scheduled tasks can keep going missing. Until 3.0.4 the email, the
2662 - * box, the notices and the note on the scores worded all of them as
2663 - * modified files. When several kinds come together the strongest one names
2664 - * the alarm: a changed file first, then the check switched off, then its
2665 - * hooks, then the tasks.
2666 - *
2667 - * @since 3.0.5
2668 - *
2669 - * @param array $findings Findings.
2670 - * @param string $severity Only look at the findings of this severity, or '' for all of them.
2671 - * @return string files|off|hooks|tasks, or '' when there is none to look at.
2672 - */
2673 - public static function findings_kind( $findings, $severity = '' ) {
2674 - $rank = array(
2675 - 'tasks' => 1,
2676 - 'hooks' => 2,
2677 - 'off' => 3,
2678 - 'files' => 4,
2679 - );
2680 - $codes = array(
2681 - 'cron_cleared_repeatedly' => 'tasks',
2682 - 'self_hooks_removed' => 'hooks',
2683 - 'self_disabled' => 'off',
2684 - );
2685 - $kind = '';
2686 -
2687 - foreach ( (array) $findings as $finding ) {
2688 - if ( ! is_array( $finding ) ) {
2689 - continue;
2690 - }
2691 - if ( '' !== $severity && $severity !== ( isset( $finding['severity'] ) ? (string) $finding['severity'] : '' ) ) {
2692 - continue;
2693 - }
2694 - // Whatever is not one of the three is a finding about the files.
2695 - $code = isset( $finding['code'] ) ? (string) $finding['code'] : '';
2696 - $this_kind = isset( $codes[ $code ] ) ? $codes[ $code ] : 'files';
2697 - if ( '' === $kind || $rank[ $this_kind ] > $rank[ $kind ] ) {
2698 - $kind = $this_kind;
2699 - }
2700 - }
2701 -
2702 - return $kind;
2703 - }
2704 -
2705 - /**
2706 - * Whether a set of findings holds any of these codes
2707 - *
2708 - * @since 3.0.5
2709 - *
2710 - * @param array $findings Findings.
2711 - * @param array $codes Finding codes.
2712 - * @return bool
2713 - */
2714 - private static function has_code( $findings, $codes ) {
2715 - foreach ( (array) $findings as $finding ) {
2716 - if ( is_array( $finding ) && isset( $finding['code'] ) && in_array( $finding['code'], $codes, true ) ) {
2717 - return true;
2718 - }
2719 - }
2720 - return false;
2721 - }
2722 -
2723 - /**
2724 - * Whether a set of findings is only about scheduled tasks
2725 - *
2726 - * @since 3.0.5
2727 - *
2728 - * @param array $findings Findings.
2729 - * @param string $severity Only look at the findings of this severity, or '' for all of them.
2730 - * @return bool True when there is at least one and all of them are about tasks.
2731 - */
2732 - public static function findings_are_about_tasks( $findings, $severity = '' ) {
2733 - return 'tasks' === self::findings_kind( $findings, $severity );
2734 - }
2735 -
2736 - /**
2737 - * What the alarm of this site is about, for the texts that announce it
2738 - *
2739 - * @since 3.0.5
2740 - *
2741 - * @param array $state State.
2742 - * @param bool $enabled Whether the self-check runs (see is_on()).
2743 - * @return string files|off|hooks|tasks, or '' when there is no alarm.
2744 - */
2745 - public static function alarm_kind( $state, $enabled = true ) {
2746 - // The last check of the files ended in critical: files, whatever else
2747 - // there is. That includes the check being switched off afterwards,
2748 - // which is what somebody who changed a file would do next: nothing
2749 - // writes the state from then on, so it still holds what was found.
2750 - $files = 'critical' === ( isset( $state['last_status'] ) ? (string) $state['last_status'] : '' );
2751 - if ( ! $enabled ) {
2752 - return $files ? 'files' : 'off';
2753 - }
2754 - if ( 'critical' !== self::tone( $state, true ) ) {
2755 - return '';
2756 - }
2757 - if ( $files ) {
2758 - return 'files';
2759 - }
2760 -
2761 - $kind = self::findings_kind( self::state_findings( $state, true ), 'critical' );
2762 - if ( '' === $kind ) {
2763 - // Critical with no critical finding to read: the removed hooks are
2764 - // what tone() looks at before anything else.
2765 - return empty( $state['hooks_removed']['methods'] ) ? 'files' : 'hooks';
2766 - }
2767 -
2768 - return $kind;
2769 - }
2770 -
2771 - /**
2772 - * Whether the critical state of this site is about scheduled tasks and not about files
2773 - *
2774 - * @since 3.0.5
2775 - *
2776 - * @param array $state State.
2777 - * @param bool $enabled Whether the self-check runs (see is_on()).
2778 - * @return bool
2779 - */
2780 - public static function alarm_is_about_tasks( $state, $enabled = true ) {
2781 - return 'tasks' === self::alarm_kind( $state, $enabled );
2782 - }
2783 -
2784 - /**
2785 2221 * Findings of a state (stored plus watchdog), or none when the check is off.
2786 2222 *
2787 2223 * @param array $state State.
2788 2224 * @param bool $enabled Whether the self-check runs (see is_on()).
@@ -2850,36 +2286,8 @@
2850 2286 * @param bool $enabled Whether the self-check runs (see is_on()).
2851 2287 * @return string critical|warning|info|ok|off|none
2852 2288 */
2853 2289 public static function tone( $state, $enabled = true ) {
2854 - return self::tone_of( $state, $enabled, false );
2855 - }
2856 -
2857 - /**
2858 - * The tone that counts for the two scores
2859 - *
2860 - * The same as tone(), except that tasks restored more than once, while
2861 - * that is only a warning, do not count: Vigilant has already scheduled
2862 - * them again and its files are as verified as they were, which is what the
2863 - * points of this check are for. The box and the notice still show it.
2864 - *
2865 - * @since 3.0.5
2866 - *
2867 - * @param array $state State.
2868 - * @param bool $enabled Whether the self-check runs (see is_on()).
2869 - * @return string critical|warning|info|ok|off|none
2870 - */
2871 - public static function score_tone( $state, $enabled = true ) {
2872 - return self::tone_of( $state, $enabled, true );
2873 - }
2874 -
2875 - /**
2876 - * @param array $state State.
2877 - * @param bool $enabled Whether the self-check runs.
2878 - * @param bool $for_score Leave out the warnings about restored tasks.
2879 - * @return string
2880 - */
2881 - private static function tone_of( $state, $enabled, $for_score ) {
2882 2290 if ( ! $enabled ) {
2883 2291 // Switched off by code is an alarm, not a neutral state: see
2884 2292 // state_findings().
2885 2293 return 'off';
@@ -2893,11 +2301,8 @@
2893 2301 $findings = self::state_findings( $state, true );
2894 2302 $worst = 'none';
2895 2303 foreach ( $findings as $finding ) {
2896 2304 $severity = isset( $finding['severity'] ) ? (string) $finding['severity'] : '';
2897 - if ( $for_score && 'warning' === $severity && 'cron_cleared_repeatedly' === ( isset( $finding['code'] ) ? (string) $finding['code'] : '' ) ) {
2898 - continue;
2899 - }
2900 2305 if ( 'critical' === $severity ) {
2901 2306 $worst = 'critical';
2902 2307 break;
2903 2308 }
@@ -2936,26 +2341,19 @@
2936 2341 $state['off_alerted'] = time();
2937 2342 $this->save_state( $state );
2938 2343
2939 2344 $ficheros = self::disabled_by();
2940 - $findings = array(
2941 - $this->finding(
2942 - 'self_disabled',
2943 - $ficheros ? implode( ', ', $ficheros ) : '',
2944 - 'critical',
2945 - __( 'Vigilant self-protection is switched off by code on this site.', 'vigilante' )
2345 + $this->maybe_send_self_alert(
2346 + array(
2347 + $this->finding(
2348 + 'self_disabled',
2349 + $ficheros ? implode( ', ', $ficheros ) : '',
2350 + 'critical',
2351 + __( 'Vigilant self-protection is switched off by code on this site.', 'vigilante' )
2352 + ),
2946 2353 ),
2354 + 'disabled'
2947 2355 );
2948 - // Switched off after the last check had found changed files: the alert
2949 - // carries them too, so it is not read as only switched off.
2950 - if ( 'critical' === ( isset( $state['last_status'] ) ? (string) $state['last_status'] : '' ) && isset( $state['last_findings'] ) && is_array( $state['last_findings'] ) ) {
2951 - foreach ( $state['last_findings'] as $stored ) {
2952 - if ( is_array( $stored ) && isset( $stored['code'], $stored['file'], $stored['severity'] ) && 'critical' === $stored['severity'] ) {
2953 - $findings[] = $stored;
2954 - }
2955 - }
2956 - }
2957 - $this->maybe_send_self_alert( $findings, 'disabled' );
2958 2356 $this->log(
2959 2357 'self_disabled',
2960 2358 $ficheros
2961 2359 ? sprintf(
@@ -2988,9 +2386,9 @@
2988 2386 'manifest_missing' => __( 'Manifest missing', 'vigilante' ),
2989 2387 'manifest_invalid' => __( 'Manifest not valid', 'vigilante' ),
2990 2388 'manifest_unverified' => __( 'Manifest changed, unverifiable against wp.org', 'vigilante' ),
2991 2389 'no_anchors' => __( 'No verification anchors available', 'vigilante' ),
2992 - 'cron_cleared_repeatedly' => __( 'Scheduled task missing more than once', 'vigilante' ),
2390 + 'cron_cleared_repeatedly' => __( 'Cron event repeatedly cleared', 'vigilante' ),
2993 2391 'self_disabled' => __( 'Self-protection switched off by code', 'vigilante' ),
2994 2392 'self_hooks_removed' => __( 'Self-protection hooks removed', 'vigilante' ),
2995 2393 'self_stale' => __( 'Not checked recently', 'vigilante' ),
2996 2394 );