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