findings = $findings; $this->scope_mode = $scope_mode; } /** * @param array $summary Source-free repository summary. * @return array */ public function analyze( string $check_id, array $summary, string $timestamp ): array { $this->validate_summary( $summary ); if ( 'code.ownership_reproducibility' === $check_id ) { return $this->ownership( $summary, $timestamp ); } if ( 'code.namespace_collisions' === $check_id ) { return $this->namespace_collisions( $summary, $timestamp ); } $status = $this->status( $check_id, $summary ); $coverage = $this->coverage( $summary ); $evidence = $this->code_evidence( $summary ); $objects = array_map( static fn ( array $sample ): array => array( 'type' => 'source_file', 'reference' => (string) $sample['file_hash'], ), array_slice( $summary['samples'], 0, 12 ) ); return $this->findings->build( $check_id, $status, $evidence, (int) $summary['observed'], (int) $summary['total'], $coverage, $timestamp, $objects ); } /** @return list */ public static function ids(): array { return array( 'code.syntax_compatibility', 'code.input_output_security', 'code.database_security', 'code.dynamic_execution', 'code.filesystem_uploads', 'code.outbound_requests', 'code.secret_signals', 'code.hot_paths', 'code.namespace_collisions', 'code.frontend_residue', 'code.ownership_reproducibility', ); } /** @param array $summary Summary. */ private function status( string $check_id, array $summary ): string { if ( 0 === $summary['total'] ) { return 'not_tested'; } if ( 0 === $summary['observed'] ) { return $summary['errors'] > 0 ? 'error' : 'not_tested'; } $counts = $summary['signal_counts']; switch ( $check_id ) { case 'code.syntax_compatibility': if ( $this->has( $counts, array( 'syntax.parse_error' ) ) ) { return 'fail'; } if ( $this->has_prefix( $counts, 'compat.' ) ) { // A syntax marker is inventory until it is compared against a // declared support matrix. The current parser cannot prove an // incompatibility by presence alone. return 'info'; } break; case 'code.input_output_security': if ( $this->has( $counts, array( 'io.input_output_cooccurrence' ) ) ) { // Co-occurrence within one file is not a data-flow result. return 'info'; } break; case 'code.dynamic_execution': if ( $this->has( $counts, array( 'dynamic.eval', 'dynamic.create_function', 'dynamic.shell_call', 'dynamic.shell_backtick' ) ) ) { return 'fail'; } if ( $this->has( $counts, array( 'dynamic.assert' ) ) ) { return 'warning'; } break; case 'code.secret_signals': if ( $this->has( $counts, array( 'secret.private_key_material' ) ) ) { return 'fail'; } if ( $this->has_prefix( $counts, 'secret.' ) ) { return 'warning'; } break; case 'code.filesystem_uploads': if ( $this->has_prefix( $counts, 'filesystem.' ) ) { // File operations are common product behaviour. Their mere // presence is useful inventory, not a confirmed security issue. return 'info'; } break; case 'code.outbound_requests': if ( $this->has_prefix( $counts, 'outbound.' ) ) { return 'info'; } break; case 'code.hot_paths': if ( $this->has_prefix( $counts, 'hot.' ) ) { return 'info'; } break; default: if ( array_sum( $counts ) > 0 ) { return 'warning'; } } return 'complete' === $this->coverage( $summary ) ? 'pass' : 'not_tested'; } /** * @param array $summary Summary. * @return array */ private function code_evidence( array $summary ): array { $file_hashes = array(); $paths = array(); $excerpts = array(); foreach ( array_slice( $summary['samples'], 0, 12 ) as $sample ) { if ( isset( $sample['file_hash'], $sample['masked_path'] ) ) { $file_hashes[] = (string) $sample['file_hash']; $paths[] = (string) $sample['masked_path']; } foreach ( array_slice( $sample['occurrences'] ?? array(), 0, 4 ) as $occurrence ) { if ( isset( $occurrence['line'], $occurrence['marker'] ) ) { $excerpts[] = (string) $sample['masked_path'] . ':L' . (int) $occurrence['line'] . ' · ' . (string) $occurrence['marker']; } } } $signal_types = array_keys( array_filter( $summary['signal_counts'], static fn ( int $count ): bool => $count > 0 ) ); return array( 'file_hashes' => array_values( array_unique( $file_hashes ) ), 'masked_paths' => array_values( array_unique( $paths ) ), 'signal_types' => array() === $signal_types ? array( 'no_matching_signals' ) : $signal_types, 'masked_excerpts' => array_slice( array_values( array_unique( $excerpts ) ), 0, 12 ), 'observed_count' => (int) $summary['observed'], 'truncated' => (bool) $summary['truncated'], 'limitations' => $this->limitations( (bool) $summary['truncated'] ), ); } /** * @param array $summary Summary. * @return array */ private function namespace_collisions( array $summary, string $timestamp ): array { $status = 0 === $summary['total'] ? 'not_tested' : ( $summary['duplicate_count'] > 0 ? 'warning' : ( 'complete' === $this->coverage( $summary ) ? 'pass' : 'not_tested' ) ); $types = array(); $notes = array(); $refs = array(); foreach ( $summary['samples'] as $sample ) { $types[] = 'duplicate_' . (string) $sample['symbol_type']; $notes[] = 'symbol-' . substr( (string) $sample['symbol_hash'], 0, 12 ) . ' · ' . (int) $sample['file_count'] . '-files'; $refs[] = array( 'type' => 'symbol', 'reference' => (string) $sample['symbol_hash'], ); } return $this->findings->build( 'code.namespace_collisions', $status, array( 'file_hashes' => array(), 'masked_paths' => array(), 'signal_types' => array() === $types ? array( 'no_duplicate_symbols' ) : array_values( array_unique( $types ) ), 'masked_excerpts' => $notes, 'observed_count' => (int) $summary['observed'], 'truncated' => (bool) $summary['truncated'], 'limitations' => array_merge( $this->limitations( (bool) $summary['truncated'] ), array( 'conditional_declarations_and_runtime_load_order_not_resolved' ) ), ), (int) $summary['observed'], (int) $summary['total'], $this->coverage( $summary ), $timestamp, $refs ); } /** * @param array $summary Summary. * @return array */ private function ownership( array $summary, string $timestamp ): array { $signals = array_keys( array_filter( $summary['signal_counts'], static fn ( int $count ): bool => $count > 0 ) ); $status = 0 === $summary['total'] ? 'not_tested' : ( array() === $signals && 'custom' === $this->scope_mode ? 'warning' : 'info' ); $hashes = array_values( array_filter( array_map( static fn ( array $sample ): string => (string) ( $sample['file_hash'] ?? '' ), $summary['samples'] ) ) ); $paths = array_values( array_filter( array_map( static fn ( array $sample ): string => (string) ( $sample['masked_path'] ?? '' ), $summary['samples'] ) ) ); return $this->findings->build( 'code.ownership_reproducibility', $status, array( 'path_hashes' => $hashes, 'masked_paths' => $paths, 'signal_types' => array() === $signals ? array( 'no_repository_or_lock_signals' ) : $signals, 'observed_count' => (int) $summary['total'], 'truncated' => (bool) $summary['truncated'], 'limitations' => array_merge( array( 'file_presence_only', 'repository_remote_ownership_and_build_reproducibility_not_proven' ), 'custom' === $this->scope_mode ? array( 'vendor_scope_excluded' ) : array( 'only_explicitly_selected_installed_components_included' ) ), ), (int) $summary['observed'], (int) $summary['total'], $this->coverage( $summary ), $timestamp ); } /** @param array $summary Summary. */ private function coverage( array $summary ): string { if ( 0 === $summary['total'] || 0 === $summary['observed'] ) { return 0 < $summary['errors'] ? 'error' : 'none'; } return $summary['observed'] === $summary['total'] && ! $summary['truncated'] ? 'complete' : 'partial'; } /** * @param array $counts Counts. * @param list $types Signals. */ private function has( array $counts, array $types ): bool { foreach ( $types as $type ) { if ( ( $counts[ $type ] ?? 0 ) > 0 ) { return true; } } return false; } /** @param array $counts Counts. */ private function has_prefix( array $counts, string $prefix ): bool { foreach ( $counts as $type => $count ) { if ( $count > 0 && 0 === strpos( $type, $prefix ) ) { return true; } } return false; } /** @return list */ private function limitations( bool $truncated ): array { $limitations = array( 'tokenizer_and_lexical_triage_not_full_dataflow_or_vulnerability_proof', 'raw_source_and_literal_values_not_retained', ); if ( 'custom' === $this->scope_mode ) { $limitations[] = 'active_and_parent_theme_mu_plugins_drop_ins_and_recognized_code_snippets_only'; $limitations[] = 'vendor_and_unselected_active_plugin_code_excluded'; $limitations[] = 'unrecognized_database_snippet_stores_not_read'; } else { $limitations[] = 'only_explicitly_selected_installed_components_included'; $limitations[] = 'node_modules_and_unselected_components_excluded'; } if ( $truncated ) { $limitations[] = 'file_item_size_or_signal_budget_limited_coverage'; } return $limitations; } /** @param array $summary Summary. */ private function validate_summary( array $summary ): void { foreach ( array( 'total', 'observed', 'skipped', 'errors' ) as $key ) { if ( ! isset( $summary[ $key ] ) || ! is_int( $summary[ $key ] ) || $summary[ $key ] < 0 ) { throw new InvalidArgumentException( 'invalid_code_summary' ); } } if ( ! isset( $summary['truncated'], $summary['signal_counts'], $summary['samples'] ) || ! is_bool( $summary['truncated'] ) || ! is_array( $summary['signal_counts'] ) || ! is_array( $summary['samples'] ) ) { throw new InvalidArgumentException( 'invalid_code_summary' ); } } }