PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.5
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.5
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 All 93 releases
← All changes | includes/class-self-integrity.php +664 -62 3.0.4 → 3.0.5 View file →
@@ -134,8 +134,38 @@
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 + /**
138 168 * A check older than this stops counting as a result. Something that stops
139 169 * the check (a filter, a removed hook, a cron nobody runs, files edited)
140 170 * leaves the last state frozen, and a green from three days ago read as
141 171 * "verified" would be the screen lying by omission.
@@ -215,8 +245,29 @@
215 245 */
216 246 private static $executable_names = array( '.htaccess', '.user.ini', 'php.ini' );
217 247
218 248 /**
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 + /**
219 270 * Constructor. Registers no hooks, see init_hooks().
220 271 *
221 272 * @param Vigilante_Settings $settings Settings instance.
222 273 * @param Vigilante_Activity_Log|null $activity_log Activity log instance.
@@ -234,8 +285,15 @@
234 285 */
235 286 public function init_hooks() {
236 287 add_action( 'admin_init', array( $this, 'maybe_detect_version_change' ), 20 );
237 288 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 + }
238 296 // Last of all, to see whether the two above are still there: code that
239 297 // runs inside WordPress can unhook them, and a plugin that does that
240 298 // leaves every screen showing the last result as if nothing happened.
241 299 add_action( 'admin_init', array( $this, 'verify_hooks' ), PHP_INT_MAX );
@@ -241,9 +299,10 @@
241 299 add_action( 'admin_init', array( $this, 'verify_hooks' ), PHP_INT_MAX );
242 300 }
243 301
244 302 /**
245 - * Are the two entry points of the self-check still hooked?
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.
246 305 *
247 306 * Runs at the end of admin_init, so anything that unhooked them earlier in
248 307 * the same request is visible here. It cannot say which file called
249 308 * remove_action (WordPress fires nothing when a callback is removed, and by
@@ -267,8 +326,20 @@
267 326 if ( false === has_action( 'admin_init', array( $this, $method ) ) ) {
268 327 $missing[] = $method;
269 328 }
270 329 }
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 + }
271 342
272 343 $state = $this->get_state();
273 344 if ( empty( $missing ) ) {
274 345 if ( ! empty( $state['hooks_removed'] ) ) {
@@ -1730,14 +1801,96 @@
1730 1801 $this->detect_version_change();
1731 1802 }
1732 1803
1733 1804 /**
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 + /**
1734 1883 * FTP/manual update and downgrade detection: the anchored version in the
1735 1884 * state differs from the running code. Also called from the daily
1736 - * maintenance cron. Throttled while a change stays unresolved.
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()).
1737 1890 */
1738 1891 public function detect_version_change() {
1739 - if ( ! $this->is_enabled() ) {
1892 + if ( ! $this->is_enabled() || self::upgrade_check_pending() ) {
1740 1893 return;
1741 1894 }
1742 1895
1743 1896 $state = $this->get_state();
@@ -1750,11 +1903,24 @@
1750 1903 if ( '' !== $stored_version && $running === $stored_version ) {
1751 1904 return;
1752 1905 }
1753 1906
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.
1754 1917 if ( get_transient( self::VERSION_CHECK_TRANSIENT ) ) {
1755 1918 return;
1756 1919 }
1920 + if ( self::core_updates_running() ) {
1921 + return;
1922 + }
1757 1923 set_transient( self::VERSION_CHECK_TRANSIENT, 1, self::VERSION_CHECK_THROTTLE );
1758 1924
1759 1925 if ( '' === $stored_version ) {
1760 1926 // Feature just arrived (fresh install handled by the activator,
@@ -1814,8 +1980,61 @@
1814 1980 $this->run_watchdog();
1815 1981 }
1816 1982
1817 1983 /**
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 + /**
1818 2037 * Verify that Vigilant's own cron events are still scheduled and restore
1819 2038 * any that were unscheduled (a malicious wp_clear_scheduled_hook, or an
1820 2039 * overzealous cleanup plugin, silences the plugin forever otherwise).
1821 2040 *
@@ -1821,12 +2040,18 @@
1821 2040 *
1822 2041 * A hook is only reported once the watchdog has seen it scheduled: the
1823 2042 * first pass on a site, and a hook whose setting was just switched on,
1824 2043 * schedule what is missing quietly. After that, a first disappearance is
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.
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.
1829 2054 *
1830 2055 * On a network it only watches the site that owns the installation's
1831 2056 * shared files: the activation schedules these events once, so on the
1832 2057 * other sites "not scheduled" does not mean "removed". The events of the
@@ -1850,11 +2075,13 @@
1850 2075 // hook => array( schedule, first-run offset, required? ).
1851 2076 // The weekly analyzer scan is required unless it was switched off
1852 2077 // explicitly: its own toggle unschedules it on purpose, and an
1853 2078 // 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.
1854 2082 $expected = array(
1855 2083 'vigilante_daily_maintenance' => array( 'daily', 0, true ),
1856 - 'vigilante_hourly_checks' => array( 'hourly', 0, true ),
1857 2084 'vigilante_analyzer_weekly_scan' => array( 'weekly', DAY_IN_SECONDS, ! isset( $analyzer['weekly_scan_enabled'] ) || ! empty( $analyzer['weekly_scan_enabled'] ) ),
1858 2085 'vigilante_plugin_status_check' => array( 'daily', HOUR_IN_SECONDS, ! empty( $fi['check_closed_plugins'] ) ),
1859 2086 'vigilante_file_integrity_scan' => array(
1860 2087 isset( $fi['scan_frequency'] ) ? $fi['scan_frequency'] : 'daily',
@@ -1880,8 +2107,13 @@
1880 2107 $restored = array();
1881 2108 $repeated = array();
1882 2109 $now = time();
1883 2110
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 +
1884 2116 foreach ( $expected as $hook => $spec ) {
1885 2117 list( $schedule, $offset, $required ) = $spec;
1886 2118
1887 2119 if ( ! $required ) {
@@ -1905,18 +2137,23 @@
1905 2137 $baselined[] = $hook;
1906 2138 continue;
1907 2139 }
1908 2140
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;
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 );
1912 2148
1913 2149 $watchdog[ $hook ] = array(
1914 - 'count' => $times,
2150 + 'count' => count( $times ),
1915 2151 'last' => $now,
2152 + 'times' => $times,
1916 2153 );
1917 2154 $restored[] = $hook;
1918 - if ( $times >= 2 ) {
2155 + if ( 'critical' === self::watchdog_severity( $watchdog[ $hook ], $now ) ) {
1919 2156 $repeated[] = $hook;
1920 2157 }
1921 2158 }
1922 2159
@@ -1952,25 +2189,50 @@
1952 2189 empty( $repeated ) ? 'warning' : 'critical'
1953 2190 );
1954 2191 }
1955 2192
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 - );
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;
1970 2201 }
1971 - $this->maybe_send_self_alert( $findings, 'watchdog' );
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 + );
1972 2212 }
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 + }
1973 2235 }
1974 2236
1975 2237 // Fallback file self-check whenever nothing checked the files in the
1976 2238 // last day: the File Integrity module switched off, scheduled scans
@@ -1977,9 +2239,9 @@
1977 2239 // switched off, or a weekly schedule. Asking only for the module left a
1978 2240 // site with scheduled scans switched off with no periodic check at all.
1979 2241 $state = $this->get_state();
1980 2242 $last_check = isset( $state['last_check'] ) ? (int) $state['last_check'] : 0;
1981 - if ( time() - $last_check > DAY_IN_SECONDS ) {
2243 + if ( ! self::upgrade_check_pending() && time() - $last_check > DAY_IN_SECONDS && ! self::core_updates_running() ) {
1982 2244 $result = $this->run_check( 'watchdog' );
1983 2245 if ( 'critical' === $this->worst_severity( $result['findings'] ) ) {
1984 2246 $this->maybe_send_self_alert( $result['findings'], 'watchdog' );
1985 2247 }
@@ -2006,22 +2268,26 @@
2006 2268 * set; cleared when back to ok.
2007 2269 *
2008 2270 * @param array $findings Findings to report.
2009 2271 * @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.
2010 2276 */
2011 - public function maybe_send_self_alert( $findings, $context ) {
2277 + public function maybe_send_self_alert( $findings, $context, $by_set = true ) {
2012 2278 if ( empty( $findings ) ) {
2013 - return;
2279 + return false;
2014 2280 }
2015 2281
2016 2282 if ( ! Vigilante_Settings::owns_shared_files() ) {
2017 - return;
2283 + return false;
2018 2284 }
2019 2285 $summary = $this->findings_summary( $findings );
2020 2286 $fingerprint = md5( wp_json_encode( $summary ) );
2021 2287 $state = $this->get_state();
2022 - if ( isset( $state['alerted_fingerprint'] ) && $fingerprint === $state['alerted_fingerprint'] ) {
2023 - return;
2288 + if ( $by_set && isset( $state['alerted_fingerprint'] ) && $fingerprint === $state['alerted_fingerprint'] ) {
2289 + return false;
2024 2290 }
2025 2291
2026 2292 if ( ! class_exists( 'Vigilante_Email_Template' ) ) {
2027 2293 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
@@ -2039,17 +2305,54 @@
2039 2305 }
2040 2306 $recipients = array_values( array_unique( $recipients ) );
2041 2307 }
2042 2308 if ( empty( $recipients ) ) {
2043 - return;
2309 + return false;
2044 2310 }
2045 2311
2046 2312 $worst = $this->worst_severity( $findings );
2047 2313 $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 );
2048 2328
2049 - if ( 'critical' === $worst ) {
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 ) {
2050 2345 $subject = sprintf(
2051 2346 /* 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 */
2052 2355 __( '[%s] CRITICAL: Vigilant plugin files were modified', 'vigilante' ),
2053 2356 $site_name
2054 2357 );
2055 2358 $title = __( 'Vigilant self-protection alert', 'vigilante' );
@@ -2065,11 +2368,17 @@
2065 2368 }
2066 2369
2067 2370 $body = Vigilante_Email_Template::p( $intro );
2068 2371 $body .= self::build_self_email_section( $findings );
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 - );
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 );
2072 2381 $body .= Vigilante_Email_Template::button(
2073 2382 admin_url( 'admin.php?page=vigilante&tab=file-integrity#vigilante-section-fi-last-scan' ),
2074 2383 __( 'Review in Vigilant', 'vigilante' )
2075 2384 );
@@ -2088,13 +2397,17 @@
2088 2397 // several recipients false is not proof of anything (PHPMailer
2089 2398 // returns it when one of them is refused and the others were sent),
2090 2399 // so those count as sent, or the alert would be repeated to
2091 2400 // everybody who did get it.
2092 - return;
2401 + return false;
2093 2402 }
2094 2403
2095 - $state['alerted_fingerprint'] = $fingerprint;
2096 - $this->save_state( $state );
2404 + if ( $by_set ) {
2405 + $state['alerted_fingerprint'] = $fingerprint;
2406 + $this->save_state( $state );
2407 + }
2408 +
2409 + return true;
2097 2410 }
2098 2411
2099 2412 /**
2100 2413 * Red email sub-section for self findings, mirroring the closed-plugins
@@ -2109,11 +2422,28 @@
2109 2422 }
2110 2423
2111 2424 $html = '<div style="background:#fef1f1;border-left:4px solid #d63638;padding:12px 16px;margin:16px 0;">';
2112 2425 $html .= '<h2 style="margin:0 0 8px;font-size:16px;color:#d63638;">' . esc_html__( 'Vigilant self-protection', 'vigilante' ) . '</h2>';
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>';
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>';
2114 2444 $html .= '<table style="border-collapse:collapse;width:100%;font-size:13px;">';
2115 - $html .= '<tr><th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html__( 'File', 'vigilante' ) . '</th>'
2445 + $html .= '<tr><th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html( $column ) . '</th>'
2116 2446 . '<th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Finding', 'vigilante' ) . '</th>'
2117 2447 . '<th style="text-align:left;padding:4px 8px;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Severity', 'vigilante' ) . '</th></tr>';
2118 2448
2119 2449 foreach ( array_slice( $findings, 0, self::MAX_STORED_FINDINGS ) as $finding ) {
@@ -2187,12 +2517,117 @@
2187 2517 return ( $there > $here ) ? $main : $state;
2188 2518 }
2189 2519
2190 2520 /**
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.
2521 + * The scheduled tasks the watchdog looks after
2194 2522 *
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 + *
2195 2630 * @param array $state State.
2196 2631 * @return array Findings, in the same shape as last_findings.
2197 2632 */
2198 2633 public static function watchdog_findings( $state ) {
@@ -2197,21 +2632,22 @@
2197 2632 */
2198 2633 public static function watchdog_findings( $state ) {
2199 2634 $findings = array();
2200 2635 $watchdog = ( isset( $state['watchdog'] ) && is_array( $state['watchdog'] ) ) ? $state['watchdog'] : array();
2636 + $watched = self::watched_hooks();
2637 +
2201 2638 foreach ( $watchdog as $hook => $entry ) {
2202 - if ( ! is_array( $entry ) ) {
2639 + if ( ! is_array( $entry ) || ! in_array( (string) $hook, $watched, true ) ) {
2203 2640 continue;
2204 2641 }
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 ) {
2642 + $severity = self::watchdog_severity( $entry );
2643 + if ( '' === $severity ) {
2208 2644 continue;
2209 2645 }
2210 2646 $findings[] = array(
2211 2647 'code' => 'cron_cleared_repeatedly',
2212 2648 'file' => (string) $hook,
2213 - 'severity' => 'critical',
2649 + 'severity' => $severity,
2214 2650 'message' => '',
2215 2651 );
2216 2652 }
2217 2653 return $findings;
@@ -2217,8 +2653,136 @@
2217 2653 return $findings;
2218 2654 }
2219 2655
2220 2656 /**
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 + /**
2221 2785 * Findings of a state (stored plus watchdog), or none when the check is off.
2222 2786 *
2223 2787 * @param array $state State.
2224 2788 * @param bool $enabled Whether the self-check runs (see is_on()).
@@ -2286,8 +2850,36 @@
2286 2850 * @param bool $enabled Whether the self-check runs (see is_on()).
2287 2851 * @return string critical|warning|info|ok|off|none
2288 2852 */
2289 2853 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 ) {
2290 2882 if ( ! $enabled ) {
2291 2883 // Switched off by code is an alarm, not a neutral state: see
2292 2884 // state_findings().
2293 2885 return 'off';
@@ -2301,8 +2893,11 @@
2301 2893 $findings = self::state_findings( $state, true );
2302 2894 $worst = 'none';
2303 2895 foreach ( $findings as $finding ) {
2304 2896 $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 + }
2305 2900 if ( 'critical' === $severity ) {
2306 2901 $worst = 'critical';
2307 2902 break;
2308 2903 }
@@ -2341,19 +2936,26 @@
2341 2936 $state['off_alerted'] = time();
2342 2937 $this->save_state( $state );
2343 2938
2344 2939 $ficheros = self::disabled_by();
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 - ),
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' )
2353 2946 ),
2354 - 'disabled'
2355 2947 );
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' );
2356 2958 $this->log(
2357 2959 'self_disabled',
2358 2960 $ficheros
2359 2961 ? sprintf(
@@ -2386,9 +2988,9 @@
2386 2988 'manifest_missing' => __( 'Manifest missing', 'vigilante' ),
2387 2989 'manifest_invalid' => __( 'Manifest not valid', 'vigilante' ),
2388 2990 'manifest_unverified' => __( 'Manifest changed, unverifiable against wp.org', 'vigilante' ),
2389 2991 'no_anchors' => __( 'No verification anchors available', 'vigilante' ),
2390 - 'cron_cleared_repeatedly' => __( 'Cron event repeatedly cleared', 'vigilante' ),
2992 + 'cron_cleared_repeatedly' => __( 'Scheduled task missing more than once', 'vigilante' ),
2391 2993 'self_disabled' => __( 'Self-protection switched off by code', 'vigilante' ),
2392 2994 'self_hooks_removed' => __( 'Self-protection hooks removed', 'vigilante' ),
2393 2995 'self_stale' => __( 'Not checked recently', 'vigilante' ),
2394 2996 );