# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/api/decorators/architecture_component' require 'contrast/components/logger' require 'contrast/utils/object_share' require 'contrast/utils/timer' module Contrast module Agent module Inventory # Methods used for parsing database connection configurations # for getting inventory information from the application module DatabaseConfig extend Contrast::Components::Logger::InstanceMethods # TeamServer only accepts certain values for FlowMap Services. # DO NOT CHANGE THIS ADAPTER = 'adapter' HOST = 'host' PORT = 'port' DATABASE = 'database' DEFAULT = 'default' LOCALHOST = 'localhost' class << self # Append the available database connection information to the message being sent to TeamServer. This message # may be a Contrast::Api::Dtm::Activity or Contrast::Api::Dtm::ApplicationUpdate. Both report the same # Contrast::Api::Dtm::ArchitectureComponent, but have different names for their fields. # # @param activity_or_update [Contrast::Api::Dtm::Activity, Contrast::Api::Dtm::ApplicationUpdate] # @param hash_or_str [Hash, String] the database connection information def append_db_config activity_or_update, hash_or_str = active_record_config arr = build_from_db_config(hash_or_str) return unless arr&.any? arr.each do |a| next unless a if activity_or_update.is_a?(Contrast::Api::Dtm::Activity) activity_or_update.architectures << a else activity_or_update.components << a end end rescue StandardError => e logger.error('Unable to append db config', e) nil end private # We capture the active record configuration used by this application, as reported by # ActiveRecord::Base.connection_config, so that we can record it once and report it as needed. # # @return [Hash] def active_record_config return @_active_record_config if instance_variable_defined?(:@_active_record_config) @_active_record_config = ActiveRecord::Base.connection_config rescue nil # rubocop:disable Style/RescueModifier end # The classes we instrument in order to determine which, if any, database(s) an application connects to take # either a Hash or a String as a parameter. We install this one patch into all of those methods, letting our # code here, rather than at the patch level, make the determination of which path to call. We'll make that # decision and then parse the given configuration to create a Contrast::Api::Dtm::ArchitectureComponent for # reporting. # # @param hash_or_str [Hash, String] # @return [Contrast::Api::Dtm::ArchitectureComponent] def build_from_db_config hash_or_str return unless hash_or_str if hash_or_str.is_a?(Hash) build_from_db_hash(hash_or_str) else build_from_db_string(hash_or_str.to_s) end end # Convert the Hash used to create a database connection into an Contrast::Api::Dtm::ArchitectureComponent # understandable by TeamServer. # # @param hash [Hash] the information used to open a database connection # @return [Contrast::Api::Dtm::ArchitectureComponent] def build_from_db_hash hash ac = Contrast::Api::Dtm::ArchitectureComponent.build_database ac.vendor = hash[:adapter] || hash[ADAPTER] || Contrast::Utils::ObjectShare::EMPTY_STRING ac.remote_host = host_from_hash(hash) ac.remote_port = port_from_hash(hash) ac.url = hash[:database] || hash[DATABASE] || DEFAULT [ac] end # Retrieve the host from the given connection Hash # # @param hash [Hash] # @return [String] def host_from_hash hash hash[:host] || hash[HOST] || Contrast::Utils::ObjectShare::EMPTY_STRING end # Retrieve the port from the given connection Hash # # @param hash [Hash] # @return [integer] def port_from_hash hash p = hash[:port] || hash[PORT] || Contrast::Utils::ObjectShare::EMPTY_STRING p.to_i end # Examples: # mongodb://[user:pass@]host1[:port1][,host2[:port2],[,hostN[:portN]]][/[database][?options]] # postgresql://scott:tiger@localhost/mydatabase # pragma: allowlist secret # mysql+mysqlconnector://scott:tiger@localhost/foo # pragma: allowlist secret # # @param str [String] the DB connection string # @return [Contrast::Api::Dtm::ArchitectureComponent] def build_from_db_string str adapter, hosts, database = split_connection_str(str) acs = [] hosts.split(Contrast::Utils::ObjectShare::COMMA).map do |s| host, port = s.split(Contrast::Utils::ObjectShare::COLON) ac = Contrast::Api::Dtm::ArchitectureComponent.build_database ac.vendor = Contrast::Utils::StringUtils.force_utf8(adapter) ac.remote_host = Contrast::Utils::StringUtils.force_utf8(host) ac.remote_port = port.to_i ac.url = Contrast::Utils::StringUtils.force_utf8(database) acs << ac end acs end # Parse the given string used to connect to a database into its composite components, allowing for the # generation of a Contrast::Api::Dtm::ArchitectureComponent # # @param str [String] the DB connection string # @return [Array] the adapter, hosts, and database def split_connection_str str adapter, str = str.split(Contrast::Utils::ObjectShare::COLON_SLASH_SLASH) _auth, str = str.split(Contrast::Utils::ObjectShare::AT) # Not currently used # user, pass = auth.split(Contrast::Utils::ObjectShare::COLON) hosts, db_and_options = str.split(Contrast::Utils::ObjectShare::SLASH) hosts << LOCALHOST if hosts.empty? database, _options = db_and_options.split(Contrast::Utils::ObjectShare::QUESTION_MARK) [adapter, hosts, database] end end end end end end