*/ private $inventory_source; private ComponentEvidenceSanitizer $sanitizer; /** * @param callable():array|null $inventory_source Testable WordPress inventory adapter. */ public function __construct( FindingFactory $findings, ?callable $inventory_source = null, ?string $hash_key = null ) { $this->findings = $findings; $this->inventory_source = $inventory_source ?? array( $this, 'wordpress_inventory' ); $this->sanitizer = new ComponentEvidenceSanitizer( $hash_key ?? ( new LocalHashKey() )->derive() ); } public function id(): string { return 'components.inventory'; } /** * @return array */ public function execute( string $timestamp ): array { try { $inventory = call_user_func( $this->inventory_source ); } catch ( Throwable $exception ) { return $this->error_finding( $timestamp ); } if ( ! is_array( $inventory ) ) { return $this->error_finding( $timestamp ); } $objects = array(); $core_observed = false; $total = count( $inventory ); $truncated = $total > self::MAX_COMPONENTS; foreach ( array_slice( $inventory, 0, self::MAX_COMPONENTS ) as $component ) { if ( ! is_array( $component ) || ! isset( $component['type'], $component['identifier'], $component['name'], $component['version'], $component['state'] ) || ! is_string( $component['type'] ) || ! is_string( $component['identifier'] ) || ! is_string( $component['name'] ) || ! is_string( $component['version'] ) || ! is_string( $component['state'] ) ) { continue; } if ( ! in_array( $component['type'], self::TYPES, true ) || ! in_array( $component['state'], self::STATES, true ) || '' === $component['identifier'] ) { continue; } $objects[] = array( 'type' => $component['type'], 'reference' => $this->sanitizer->reference( $component['identifier'] ), 'name' => $this->sanitizer->text( $component['name'] ), 'version' => $this->sanitizer->version( $component['version'] ), 'state' => $component['state'], ); if ( 'core' === $component['type'] ) { $core_observed = true; } } usort( $objects, static function ( array $left, array $right ): int { return array( $left['type'], $left['name'], $left['reference'] ) <=> array( $right['type'], $right['name'], $right['reference'] ); } ); $observed = count( $objects ); if ( 0 === $observed ) { $status = 'not_tested'; $coverage = 'none'; } elseif ( ! $core_observed ) { $status = 'fail'; $coverage = $observed === $total ? 'complete' : 'partial'; } elseif ( $truncated || $observed < $total ) { $status = 'warning'; $coverage = 'partial'; } else { $status = 'pass'; $coverage = 'complete'; } return $this->findings->build( $this->id(), $status, array( 'objects' => $objects, 'observed_count' => $observed, 'total_count' => $total, 'truncated' => $truncated, 'limitations' => array( 'Inventory uses local WordPress APIs only. Author, license-key, URI, filesystem-path and file-content fields were not collected.' ), ), $observed, $total, $coverage, $timestamp ); } /** * @return array */ private function wordpress_inventory(): array { if ( ! function_exists( 'get_plugins' ) ) { $plugin_api = ( new WordPressPathProvider() )->core_file( 'wp-admin/includes/plugin.php' ); require_once $plugin_api; } $inventory = array( array( 'type' => 'core', 'identifier' => 'wordpress-core', 'name' => 'WordPress', 'version' => get_bloginfo( 'version' ), 'state' => 'active', ), ); $active_plugins = array_map( 'strval', (array) get_option( 'active_plugins', array() ) ); $network_active = is_multisite() ? array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) : array(); foreach ( get_plugins() as $identifier => $headers ) { $state = in_array( $identifier, $network_active, true ) ? 'network_active' : ( in_array( $identifier, $active_plugins, true ) ? 'active' : 'inactive' ); $inventory[] = $this->component( 'plugin', (string) $identifier, $headers, $state ); } foreach ( get_mu_plugins() as $identifier => $headers ) { $inventory[] = $this->component( 'mu_plugin', (string) $identifier, $headers, 'must_use' ); } foreach ( get_dropins() as $identifier => $headers ) { $inventory[] = $this->component( 'dropin', (string) $identifier, $headers, 'dropin' ); } foreach ( ( new LocalThemeAdapter() )->themes() as $theme ) { $inventory[] = array( 'type' => 'theme', 'identifier' => $theme['identifier'], 'name' => $theme['name'], 'version' => $theme['version'], 'state' => $theme['state'], ); } return $inventory; } /** * @param array $headers WordPress component headers. * @return array{type:string,identifier:string,name:string,version:string,state:string} */ private function component( string $type, string $identifier, array $headers, string $state ): array { return array( 'type' => $type, 'identifier' => $identifier, 'name' => isset( $headers['Name'] ) && is_string( $headers['Name'] ) ? $headers['Name'] : $type, 'version' => isset( $headers['Version'] ) && is_string( $headers['Version'] ) ? $headers['Version'] : 'unknown', 'state' => $state, ); } /** * @return array */ private function error_finding( string $timestamp ): array { return $this->findings->build( $this->id(), 'error', array( 'objects' => array(), 'observed_count' => 0, 'total_count' => 0, 'truncated' => false, 'limitations' => array( 'The local component adapter failed; no exception, path or component value was stored.' ), ), 0, 0, 'error', $timestamp ); } }