load_checks(); } /** * Require the check class files and instantiate them. */ protected function load_checks() { $dir = AARDWOLF_SC_PATH . 'includes/checks/'; $files = array( 'class-check-updates.php', 'class-check-users.php', 'class-check-config.php', 'class-check-exposure.php', 'class-check-headers.php', 'class-check-environment.php', 'class-check-permissions.php', 'class-check-vulnerabilities.php', ); foreach ( $files as $file ) { if ( file_exists( $dir . $file ) ) { require_once $dir . $file; } } // The concrete check classes shipped with the plugin. $classes = array( 'Aardwolf_Check_Core_Update', 'Aardwolf_Check_Plugin_Updates', 'Aardwolf_Check_Theme_Updates', 'Aardwolf_Check_Unused_Extensions', 'Aardwolf_Check_Admin_Username', 'Aardwolf_Check_User_Enumeration', 'Aardwolf_Check_Registration', 'Aardwolf_Check_Login_Protection', 'Aardwolf_Check_File_Editor', 'Aardwolf_Check_Debug_Mode', 'Aardwolf_Check_Security_Keys', 'Aardwolf_Check_Table_Prefix', 'Aardwolf_Check_Force_Ssl_Admin', 'Aardwolf_Check_File_Permissions', 'Aardwolf_Check_Xmlrpc', 'Aardwolf_Check_Readme', 'Aardwolf_Check_Version_Disclosure', 'Aardwolf_Check_Directory_Listing', 'Aardwolf_Check_Sensitive_Files', 'Aardwolf_Check_Https', 'Aardwolf_Check_Php_Version', 'Aardwolf_Check_Security_Headers', 'Aardwolf_Check_Vulnerable_Plugins', ); foreach ( $classes as $class ) { if ( class_exists( $class ) ) { $this->checks[] = new $class(); } } /** * Allow third parties to register additional checks. * * @param Aardwolf_Check[] $checks Currently registered checks. */ $this->checks = apply_filters( 'aardwolf_sc_checks', $this->checks ); } /** * Run every registered check. * * @return array { * @type array $results Array of Aardwolf_Check_Result::to_array(). * @type array $summary Counts and computed score. * @type int $timestamp Unix time the scan finished. * } */ public function run() { // A scan makes several loopback HTTP requests and update checks, which // can take a little while on slow hosts. Best-effort only: many hosts // disable set_time_limit(), and we ignore any failure. if ( function_exists( 'set_time_limit' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged @set_time_limit( 120 ); } $results = array(); foreach ( $this->checks as $check ) { try { $result = $check->run(); if ( $result instanceof Aardwolf_Check_Result ) { $results[] = $result; } } catch ( Exception $e ) { // A single failing check must never abort the whole scan. $results[] = new Aardwolf_Check_Result( array( 'id' => $check->id(), 'label' => $check->label(), 'category' => $check->category(), 'status' => Aardwolf_Check_Result::STATUS_INFO, 'severity' => Aardwolf_Check_Result::SEVERITY_INFO, 'summary' => sprintf( /* translators: %s: error message. */ __( 'This check could not complete: %s', 'aardwolf-security-scanner' ), $e->getMessage() ), ) ); } } $summary = $this->summarise( $results ); $payload = array( 'results' => array_map( function ( Aardwolf_Check_Result $r ) { return $r->to_array(); }, $results ), 'summary' => $summary, 'timestamp' => time(), 'version' => AARDWOLF_SC_VERSION, ); update_option( self::RESULTS_OPTION, $payload, false ); return $payload; } /** * Compute counts and an overall 0-100 security score. * * The score starts at 100 and subtracts weighted penalties for every * warning/failure, scaled against the worst-case total so the number stays * meaningful regardless of how many checks run. * * @param Aardwolf_Check_Result[] $results Results to summarise. * @return array */ protected function summarise( array $results ) { $counts = array( 'pass' => 0, 'warning' => 0, 'fail' => 0, 'info' => 0, 'total' => count( $results ), ); $penalty = 0; $max_penalty = 0; $high_problems = 0; foreach ( $results as $result ) { if ( isset( $counts[ $result->status ] ) ) { $counts[ $result->status ]++; } // Every non-info check contributes its worst-case weight to the // denominator so the score reflects the whole battery. if ( Aardwolf_Check_Result::STATUS_INFO !== $result->status ) { // Worst case for this check is a high-severity failure (5), // but we scale by the check's actual severity band when it has // one so passes for high-impact items count for more. $max_penalty += 5; } if ( $result->is_problem() ) { $weight = $result->weight(); $penalty += Aardwolf_Check_Result::STATUS_FAIL === $result->status ? $weight : max( 1, (int) round( $weight / 2 ) ); if ( Aardwolf_Check_Result::SEVERITY_HIGH === $result->severity && Aardwolf_Check_Result::STATUS_FAIL === $result->status ) { $high_problems++; } } } $score = 100; if ( $max_penalty > 0 ) { $score = (int) round( 100 * ( 1 - ( $penalty / $max_penalty ) ) ); } $score = max( 0, min( 100, $score ) ); // Letter grade for a quick read. if ( $score >= 90 ) { $grade = 'A'; } elseif ( $score >= 80 ) { $grade = 'B'; } elseif ( $score >= 65 ) { $grade = 'C'; } elseif ( $score >= 50 ) { $grade = 'D'; } else { $grade = 'F'; } $counts['score'] = $score; $counts['grade'] = $grade; $counts['high_problems'] = $high_problems; $counts['problems_total'] = $counts['warning'] + $counts['fail']; return $counts; } /** * Retrieve the stored results from the last scan, if any. * * @return array|null */ public static function get_last_scan() { $data = get_option( self::RESULTS_OPTION, null ); return is_array( $data ) ? $data : null; } /** * Number of registered checks. * * @return int */ public function count_checks() { return count( $this->checks ); } }