*/ private const CHECK_IDS = array( 'server.runtime_services', 'server.privileges_processes', 'server.filesystem_resources', 'server.database', 'server.scheduled_work', 'server.tls_http', 'server.backup_logging', 'server.integrity', 'server.coverage_profile', ); /** @var list */ private const STATES = array( 'available', 'partial', 'permission_denied', 'not_applicable', 'not_tested', 'error' ); /** @var list */ private const PERMISSIONS = array( 'standard', 'root_required', 'denied', 'not_applicable' ); /** * Exact scalar fields which may cross the server observation boundary. * * @var array> */ private const VALUE_TYPES = array( 'server.runtime_services' => array( 'os_family' => 'enum:windows|linux|bsd|darwin|solaris|unknown', 'architecture' => 'enum:x86_64|aarch64|x86|arm|other|unknown', 'php_version' => 'version', 'php_sapi' => 'enum:cli|cli-server|fpm-fcgi|apache2handler|cgi-fcgi|litespeed|other', 'extensions_loaded' => 'count', 'ini_state' => 'enum:loaded|unavailable', 'profile' => 'enum:shared|vps|container|unclassified', 'profile_source' => 'enum:user|detected|undetermined', 'container_state' => 'enum:detected|not_detected|not_tested', ), 'server.privileges_processes' => array( 'identity' => 'enum:root|nonroot|not_tested', 'process_probe' => 'enum:available|permission_denied|not_tested|error', 'listener_probe' => 'enum:available|permission_denied|not_tested|error', 'process_family_count' => 'count', 'common_listener_count' => 'count', 'other_listener_count' => 'count', ), 'server.filesystem_resources' => array( 'wordpress_root_readable' => 'bool', 'wordpress_root_writable' => 'bool', 'content_root_readable' => 'bool', 'content_root_writable' => 'bool', 'uploads_root_readable' => 'bool', 'uploads_root_writable' => 'bool', 'disk_free_percent' => 'percent', 'disk_free_band' => 'enum:healthy|watch|low|unknown', 'inode_state' => 'enum:not_tested', ), 'server.database' => array( 'family' => 'enum:mysql|mariadb|sqlite|postgresql|unknown', 'version' => 'version', 'connection_state' => 'enum:available|unavailable', ), 'server.scheduled_work' => array( 'wp_cron_state' => 'enum:enabled|disabled|not_tested', 'scheduled_event_count' => 'count', 'system_timer_state' => 'enum:available|permission_denied|not_tested|error', 'system_timer_count' => 'count', ), 'server.tls_http' => array( 'home_scheme' => 'enum:https|http|other|not_tested', 'web_server_family' => 'enum:apache|nginx|litespeed|iis|caddy|other|unknown', 'local_certificate_state' => 'enum:not_tested', 'header_state' => 'enum:not_tested', ), 'server.backup_logging' => array( 'backup_evidence' => 'enum:not_tested', 'debug_log_state' => 'enum:enabled|disabled|not_tested', 'log_rotation_state' => 'enum:not_tested', 'restore_test_state' => 'enum:not_tested', ), 'server.integrity' => array( 'core_checksum_state' => 'enum:not_tested', 'package_manifest_state' => 'enum:not_tested', ), 'server.coverage_profile' => array( 'profile' => 'enum:shared|vps|container|unclassified', 'available_sections' => 'count', 'partial_sections' => 'count', 'unavailable_sections' => 'count', 'total_sections' => 'count', 'host_probe' => 'enum:available|not_tested', ), ); /** @var array> */ private array $sections; /** * @param array> $sections Normalized observations by check ID. */ public function __construct( array $sections ) { if ( self::CHECK_IDS !== array_keys( $sections ) ) { throw new InvalidArgumentException( 'invalid_server_observation_order' ); } foreach ( $sections as $check_id => $section ) { $this->validate_section( $check_id, $section ); } $this->sections = $sections; } /** * @return list */ public static function check_ids(): array { return self::CHECK_IDS; } /** * @return array */ public function section( string $check_id ): array { if ( ! isset( $this->sections[ $check_id ] ) ) { throw new InvalidArgumentException( 'unknown_server_observation' ); } return $this->sections[ $check_id ]; } /** * @return array> */ public function all(): array { return $this->sections; } /** * @param array $section Observation section. */ private function validate_section( string $check_id, array $section ): void { $keys = array_keys( $section ); sort( $keys ); if ( array( 'capability', 'limitations', 'masked_paths', 'observed', 'permission', 'state', 'total', 'values' ) !== $keys ) { throw new InvalidArgumentException( 'invalid_server_observation_fields' ); } if ( $check_id !== $section['capability'] || ! in_array( $section['state'], self::STATES, true ) || ! in_array( $section['permission'], self::PERMISSIONS, true ) ) { throw new InvalidArgumentException( 'invalid_server_observation_state' ); } if ( ! is_int( $section['observed'] ) || ! is_int( $section['total'] ) || $section['observed'] < 0 || $section['total'] < 0 || $section['observed'] > $section['total'] ) { throw new InvalidArgumentException( 'invalid_server_observation_coverage' ); } if ( ! is_array( $section['limitations'] ) || ! is_array( $section['masked_paths'] ) || ! is_array( $section['values'] ) ) { throw new InvalidArgumentException( 'invalid_server_observation_collections' ); } if ( count( $section['limitations'] ) > 20 || count( $section['masked_paths'] ) > 10 || count( $section['values'] ) > 20 ) { throw new InvalidArgumentException( 'server_observation_budget_exceeded' ); } foreach ( $section['limitations'] as $limitation ) { if ( ! is_string( $limitation ) || 1 !== preg_match( '/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/D', $limitation ) ) { throw new InvalidArgumentException( 'invalid_server_limitation' ); } } foreach ( $section['masked_paths'] as $masked_path ) { if ( ! is_string( $masked_path ) || 1 !== preg_match( '/^\{[a-z][a-z0-9-]{0,31}\}$/D', $masked_path ) ) { throw new InvalidArgumentException( 'invalid_server_masked_path' ); } } $types = self::VALUE_TYPES[ $check_id ]; if ( array() !== array_diff( array_keys( $section['values'] ), array_keys( $types ) ) ) { throw new InvalidArgumentException( 'unknown_server_observation_value' ); } foreach ( $section['values'] as $key => $value ) { $this->validate_value( $types[ $key ], $value ); } } /** * @param mixed $value Observation scalar. */ private function validate_value( string $type, $value ): void { if ( 'bool' === $type && is_bool( $value ) ) { return; } if ( 'count' === $type && is_int( $value ) && $value >= 0 && $value <= 1000000 ) { return; } if ( 'percent' === $type && is_int( $value ) && $value >= 0 && $value <= 100 ) { return; } if ( 'version' === $type && is_string( $value ) && 1 === preg_match( '/^(?:[0-9]{1,3}\.[0-9]{1,3}|unknown)$/D', $value ) ) { return; } if ( 0 === strpos( $type, 'enum:' ) && is_string( $value ) && in_array( $value, explode( '|', substr( $type, 5 ) ), true ) ) { return; } throw new InvalidArgumentException( 'invalid_server_observation_value' ); } }