store = $store ? $store : new EventStore(); } /** * {@inheritDoc} */ public function group(): string { return 'security'; } /** * {@inheritDoc} */ public function run(): array { // Accounts that existed before Keepwatch was installed are not "new" (no alarm on a fresh install). $since = max( time() - self::LOOKBACK, self::installed_at() ); $events = $this->store->since( $since ); $admins = $this->new_administrators( $since ); return self::evaluate( $events, $admins ); } /** * Pure evaluation. * * @param array, time: int}> $events Recent events. * @param string[] $admins New admin logins. * @return Result[] */ public static function evaluate( array $events, array $admins ): array { $results = array(); if ( array() !== $admins ) { $list = implode( ', ', array_slice( $admins, 0, 5 ) ); $results[] = new Result( 'security.new_admins', Result::CRITICAL, /* translators: %d: number of new administrators */ sprintf( _n( '%d new administrator in the last 7 days', '%d new administrators in the last 7 days', count( $admins ), 'keepwatch' ), count( $admins ) ), /* translators: %s: comma-separated logins */ sprintf( __( 'Accounts: %s. Confirm each change was intentional.', 'keepwatch' ), $list ), admin_url( 'users.php?role=administrator' ), __( 'Review users', 'keepwatch' ) ); } else { $results[] = new Result( 'security.new_admins', Result::GOOD, __( 'No new administrators in the last 7 days', 'keepwatch' ) ); } $plugin_events = array_values( array_filter( $events, static function ( array $event ): bool { return in_array( $event['type'], array( 'plugin_activated', 'plugin_deactivated' ), true ); } ) ); if ( array() !== $plugin_events ) { $last = $plugin_events[ count( $plugin_events ) - 1 ]; $results[] = new Result( 'security.plugins', Result::RECOMMENDED, /* translators: %d: number of plugin activation/deactivation events */ sprintf( _n( '%d plugin change in the last 7 days', '%d plugin changes in the last 7 days', count( $plugin_events ), 'keepwatch' ), count( $plugin_events ) ), $last['message'], admin_url( 'plugins.php' ), __( 'Review plugins', 'keepwatch' ) ); } else { $results[] = new Result( 'security.plugins', Result::GOOD, __( 'No plugin activations or deactivations in the last 7 days', 'keepwatch' ) ); } $role_events = array_values( array_filter( $events, static function ( array $event ) use ( $admins ): bool { $login = isset( $event['meta']['login'] ) ? (string) $event['meta']['login'] : ''; return 'admin_role' === $event['type'] && ! in_array( $login, $admins, true ); } ) ); if ( array() !== $role_events ) { $last = $role_events[ count( $role_events ) - 1 ]; $results[] = new Result( 'security.roles', Result::CRITICAL, /* translators: %d: number of role grants */ sprintf( _n( '%d administrator role grant recorded', '%d administrator role grants recorded', count( $role_events ), 'keepwatch' ), count( $role_events ) ), $last['message'], admin_url( 'users.php?role=administrator' ), __( 'Review users', 'keepwatch' ) ); } return $results; } /** * Administrator accounts registered in the lookback window. * * @param int $since Unix timestamp. * @return string[] Logins. */ private function new_administrators( int $since ): array { $users = get_users( array( 'role' => 'administrator', 'fields' => array( 'user_login', 'user_registered' ), 'orderby' => 'registered', 'order' => 'DESC', 'number' => 20, ) ); $logins = array(); foreach ( $users as $user ) { $registered = isset( $user->user_registered ) ? strtotime( $user->user_registered ) : 0; if ( $registered >= $since ) { $logins[] = (string) $user->user_login; } } return $logins; } /** * When Keepwatch was first activated. Older installs without the option get "now". */ private static function installed_at(): int { $installed = (int) get_option( Lifecycle::OPTION_INSTALLED_AT, 0 ); if ( $installed <= 0 ) { $installed = time(); add_option( Lifecycle::OPTION_INSTALLED_AT, $installed, '', false ); } return $installed; } }