# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'contrast/agent/settings_state' cs__scoped_require 'contrast/core_extensions/module' cs__scoped_require 'contrast/utils/boolean_util' module Contrast module Agent # This class functions as a way to query the Agent for its current feature # set without having to expose other sections of code to the decision tree # needed to make that determination. class FeatureState < SettingsState include Singleton # FeatureState methods are grouped in modules, # and these modules are organized roughly in ascending order # according to their complexity - where more complex # methods call simpler ones. module ConfigWrappers # Wrapper to provide convenience for boolean values, allowing for the # handling of nil defaults that behave like true and other strange # cases. module Booleans include Contrast::Components::Interface access_component :config # Ruby 2.4 does not nicely compare to nil, so we have to include # these wrapper methods. RUBY-179 has the task to update this on # EOL of 2.4 support def false? config Contrast::Utils::BooleanUtil.false?(config) end def true? config Contrast::Utils::BooleanUtil.true?(config) end def inventory_enabled? !false?(CONFIG.root.inventory.enable) end def protect_forcibly_enabled? true?(CONFIG.root.protect.enable) end def protect_forcibly_disabled? false?(CONFIG.root.protect.enable) end def assess_forcibly_enabled? true?(CONFIG.root.assess.enable) end def assess_forcibly_disabled? false?(CONFIG.root.assess.enable) end def assess_track_frozen_sources? !false?(CONFIG.root.agent.ruby.track_frozen_sources) end def scan_response? true?(CONFIG.root.assess.enable_scan_response) end def omit_body? true?(CONFIG.root.agent.omit_body) end def require_scanning_enabled? !false?(CONFIG.root.agent.ruby.require_scan) end def heap_dump_enabled? true?(CONFIG.root.agent.heap_dump.enable) end def service_forcibly_disabled? false?(CONFIG.root.agent.start_bundled_service) end def report_any_command_execution? ctrl = protect_rule_config[Contrast::Agent::Protect::Rule::CmdInjection::NAME] true?(ctrl.disable_system_commands) end def report_custom_code_sysfile_access? ctrl = protect_rule_config[Contrast::Agent::Protect::Rule::PathTraversal::NAME] true?(ctrl.detect_custom_code_accessing_system_files) end # Determines if the Process we're currently in matches that of the # Process in which the FeatureState instance was created. # If it doesn't, that indicates the running context is in a new # Process. # @return [Boolean] if we're in the original Process in which the # FeaturesState instance was initialized. def in_new_process? current_pid = Process.pid original_pid = pid current_pid != original_pid end end # Wrapper for configurations regarding assess & protect rules module Parameters include Contrast::Components::Interface access_component :settings, :config def protect_rule_config CONFIG.root.protect.rules || [] end def assess_rule_disabled? name assess_disabled_rules.include? name end def protect_rule_enabled? rule_id protect_rule_mode(rule_id).to_sym != :NO_ACTION end def assess_tags CONFIG.root.assess&.tags end def assess_disabled_rules CONFIG.root.assess&.rules&.disabled_rules || SETTINGS.disabled_assess_rules || [] end def current_session_id Contrast::Utils::StringUtils.force_utf8(SETTINGS.session_id) end def disabled_agent_rake_tasks CONFIG.root.agent.ruby.disabled_agent_rake_tasks end def exclusions SETTINGS.exclusion_matchers end def url_exclusions exclusions.select(&:url?) end def input_exclusions exclusions.select(&:input?) end def code_exclusions exclusions.select(&:code?) end end end # Wrapper for the Assess & Protect states module SettingWrappers include Contrast::Components::Interface access_component :settings, :config def protect_setting_enabled? SETTINGS.protect_state[:enabled] end def assess_setting_enabled? SETTINGS.assess_state[:enabled] end end # whether it's connected to the Contrast Service, # whether config succeeded, # whether writable paths are actually writeable, etc. module AgentState # Indicates the status of the Agent - initialized & ready - as well as # the mode(s) in which it is functioning module Operation include Contrast::Components::Interface access_component :agent # Return true if the agent is ready to protect the application def agent_ready? AGENT.enabled? && connection_established? && update_received? end def protect_enabled? return false unless AGENT.enabled? # config overrides if forcibly set return false if protect_forcibly_disabled? return true if protect_forcibly_enabled? !!protect_setting_enabled? end def assess_enabled? return false unless AGENT.enabled? # config overrides if forcibly set return false if assess_forcibly_disabled? return true if assess_forcibly_enabled? !!assess_setting_enabled? end end # Wrapper for the state of our Service settings and a way to access the # Contrast::Agent::SocketClient responsible for interactions between # the Agent & Service. module Service include Contrast::Components::Interface access_component :agent def service_enabled? AGENT.enabled? && !service_forcibly_disabled? end def client @_client ||= Contrast::Agent::SocketClient.new end # Return true if the agent has connected with the service def connection_established? client.connection_established? end # Return true if the agent has processed a response from the service def update_received? !@last_update.nil? end end end # 'Controls' synthesize all of the above into parameter sets. # These can have more complex logic. module Controls include Contrast::Components::Interface access_component :config def heap_dump_control { path: File.absolute_path(CONFIG.root.agent.heap_dump.path), count: CONFIG.root.agent.heap_dump.count.to_i, window: CONFIG.root.agent.heap_dump.window_ms.to_f / 1000, delay: CONFIG.root.agent.heap_dump.delay_ms.to_f / 1000, clean: true?(CONFIG.root.agent.heap_dump.clean) } end DEFAULT_SAMPLING_ENABLED = false DEFAULT_SAMPLING_BASELINE = 5 DEFAULT_SAMPLING_REQUEST_FREQUENCY = 5 DEFAULT_SAMPLING_RESPONSE_FREQUENCY = 25 DEFAULT_SAMPLING_WINDOW_MS = 180_000 # 3 minutes, arbitrary value from Java agent def sampling_control settings = @sampling_features cas = CONFIG.root.assess&.sampling { enabled: [cas&.enable, settings&.enabled, DEFAULT_SAMPLING_ENABLED] .reject(&:nil?).first, baseline: [cas&.baseline, settings&.baseline, DEFAULT_SAMPLING_BASELINE] .map(&:to_i).find(&:positive?), request_frequency: [cas&.request_frequency, settings&.request_frequency, DEFAULT_SAMPLING_REQUEST_FREQUENCY] .map(&:to_i).find(&:positive?), response_frequency: [cas&.response_frequency, settings&.response_frequency, DEFAULT_SAMPLING_RESPONSE_FREQUENCY].map(&:to_i).find(&:positive?), window: [cas&.window_ms, settings&.window_ms, DEFAULT_SAMPLING_WINDOW_MS] .map(&:to_i).find(&:positive?) } end DEFAULT_LOG_FILENAME = 'contrast_service.log' def service_control { host: CONFIG.root.agent.service.host || Contrast::Configuration::DEFAULT_HOST, port: CONFIG.root.agent.service.port || Contrast::Configuration::DEFAULT_PORT, socket_path: CONFIG.root.agent.service.socket, logger_path: CONFIG.root.agent.service.logger.path || DEFAULT_LOG_FILENAME } end def exception_control { enable: true?(CONFIG.root.agent.ruby.exceptions.capture), status: CONFIG.root.agent.ruby.exceptions.override_status || 403, message: CONFIG.root.agent.ruby.exceptions.override_message || Contrast::Utils::ObjectShare::OVERRIDE_MESSAGE } end end # Wrapper for the state of our diagnostics settings module Diagnostic include Contrast::Components::Interface access_component :config def uninstrument_namespaces tmp = CONFIG.root.agent.ruby.uninstrument_namespace.map(&:to_sym) tmp.select! { |sym| Object.cs__const_defined?(sym) } tmp.map! { |sym| Object.cs__const_get(sym) } tmp end end # Wrapper for the state of our logging settings and holder for utility # methods for logging state. module Logging include Contrast::Components::Interface access_component :config FALLBACK = STDOUT LOG_INFO_RULE_SETTINGS = 'Current rule settings:' # Utility method to log the current mode of all the rules def log_rule_modes return unless logger&.info? logger.info(LOG_INFO_RULE_SETTINGS) PROTECT.rules.each { |k, v| logger.info(".. protect:\t#{ k } (#{ v.mode })") } ASSESS.rules.each { |k, v| logger.info(".. assess: \t#{ k } (#{ v.enabled? })") } end ENV_KEYS = %w[HOME PWD RACK_ENV RAILS_ENV RUBY_VERSION GEM_HOME GEM_PATH].cs__freeze # Utility method to log some current ruby and rails information from environment def log_environment return unless logger.info? logger.info "AGENT_VERSION=#{ Contrast::Agent::VERSION }" ENV.each do |env_key, env_value| env_key = env_key.to_s next unless ENV_KEYS.include?(env_key) || (env_key.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER) && !env_key.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER + 'API')) logger.info "#{ env_key }=#{ env_value }" end logger.info "PARENT_PROCESS_ID=#{ Process.ppid }" logger.info "PROCESS_ID=#{ Process.pid }" end def log_configuration return unless logger.info? loggable = CONFIG.raw.send(:load_config) loggable.delete('api') logger.info "LOCAL CONFIGURATION: #{ JSON.pretty_generate(loggable) }" end FRAMEWORKS = %w[rails sinatra grape].cs__freeze WEB_SERVERS = %w[agoo falcon hoof iodine mongrel mongrel2 passenger puma rack skinny thin trinidad unicorn webrick yarn].cs__freeze LIBRARIES = %w[excon json mongo moped mysql nokogiri oga ox pg psych sqlite3 typhoeus yaml].cs__freeze def log_specific_libraries FRAMEWORKS.each(&cs__method(:log_gem_data)) WEB_SERVERS.each(&cs__method(:log_gem_data)) LIBRARIES.each(&cs__method(:log_gem_data)) end def log_all_libraries return unless logger.debug? Gem.loaded_specs.each_pair do |_name, gem_spec| logger.debug { "GEM LOADED: #{ gem_spec.name }=#{ gem_spec.version }" } end end def log_gem_data gem_name gem_spec = Gem.loaded_specs[gem_name] return unless gem_spec logger.info { "GEM LOADED: #{ gem_spec.name }=#{ gem_spec.version }" } end end # Methods that query the config. include ConfigWrappers::Booleans # These check boolean config options. include ConfigWrappers::Parameters # These check against the config and return parameters. include SettingWrappers # Methods that query settings from TeamServer. include AgentState::Operation # Enable/disable the agent, ask if the agent can run. include AgentState::Service # Agent's client that talks to the service. include Controls # Syntheses of the above. include Diagnostic # Misc. include Logging # Logging. end end end