*/ public function run(): array { if ( ! function_exists( 'wp_get_connectors' ) ) { return array(); } $findings = array(); foreach ( wp_get_connectors() as $connector ) { if ( ! is_array( $connector ) ) { continue; } $auth = isset( $connector['authentication'] ) && is_array( $connector['authentication'] ) ? $connector['authentication'] : array(); $method = isset( $auth['method'] ) ? (string) $auth['method'] : ''; if ( 'api_key' !== $method && 'application_password' !== $method ) { continue; } $setting = isset( $auth['setting_name'] ) ? (string) $auth['setting_name'] : ''; if ( '' === $setting ) { continue; } // Held outside the database: nothing is stored here to report. if ( $this->held_outside_the_database( $auth ) ) { continue; } if ( ! $this->has_stored_credential( $setting, $method ) ) { continue; } $name = isset( $connector['name'] ) ? (string) $connector['name'] : $setting; $findings[] = new Finding( $this->id(), Severity::REVIEW, __( 'A provider credential is stored on this site', 'agentic-daisy-ai-agent-firewall' ), __( 'WordPress keeps connector credentials in the options table without encrypting them, so every administrator, every plugin running here, and every database backup carries this one. If the site is shared or inherited, treat the credential as known and rotate it. Moving it to an environment variable or a PHP constant keeps it out of the database entirely.', 'agentic-daisy-ai-agent-firewall' ), $name, null, $this->id() . ':stored:' . $setting ); } return $findings; } /** * Whether core would resolve this credential before reaching the option. * * Mirrors the order in `_wp_connectors_get_api_key_source()`: environment * variable, then constant, then the database. That helper is private, so * the order is reproduced rather than called. * * @param array $auth Connector authentication block. */ private function held_outside_the_database( array $auth ): bool { $env_name = isset( $auth['env_var_name'] ) ? (string) $auth['env_var_name'] : ''; if ( '' !== $env_name ) { $value = getenv( $env_name ); if ( is_string( $value ) && '' !== $value ) { return true; } } $constant = isset( $auth['constant_name'] ) ? (string) $auth['constant_name'] : ''; if ( '' !== $constant && defined( $constant ) ) { $value = constant( $constant ); if ( is_string( $value ) && '' !== $value ) { return true; } } return false; } /** * Whether the option actually holds a credential. * * An api_key connector stores a string; an application_password connector * stores `array( 'username' => ..., 'password' => ... )`, and core writes * that shape with blank values as its default - so "the row exists" is * the wrong question for it. The stored value is never returned, only * asked whether it is empty. * * @param string $setting Option name. * @param string $method api_key or application_password. */ private function has_stored_credential( string $setting, string $method ): bool { $stored = get_option( $setting, '' ); if ( 'application_password' === $method ) { return is_array( $stored ) && '' !== (string) ( $stored['password'] ?? '' ); } return is_string( $stored ) && '' !== $stored; } }