# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/api/settings.pb' require 'contrast/agent/reporting/settings/sensitive_data_masking' module Contrast module Components # This component encapsulates the statefulness of settings. # When we say 'settings', we're referring specifically to external # directives (likely provided by TeamServer) about product operation. # 'Settings' is not a generic term for 'configurable stuff'. module Settings APPLICATION_STATE_BASE = Struct.new(:modes_by_id, :exclusion_matchers). new(Hash.new(Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION), []) PROTECT_STATE_BASE = Struct.new(:enabled, :rules).new(false, {}) ASSESS_STATE_BASE = Struct.new(:enabled, :sampling_settings, :disabled_assess_rules, :session_id).new(false, nil, [], nil) do def sampling_settings= new_val @sampling_settings = new_val Contrast::Utils::Assess::SamplingUtil.instance.update end end SENSITIVE_DATA_MASKING_BASE = Contrast::Agent::Reporting::Settings::SensitiveDataMasking.new # This is a class. class Interface extend Contrast::Components::Config # tainted_columns are database columns that receive unsanitized input. attr_reader :tainted_columns # This can probably go into assess_state? # Current state for Assess. # enabled [Boolean] Indicate if the assess feature set is enabled for this server or not. # # sampling [Hash] Hash of AssessSampling Used to control the sampling feature in the agent: { # baseline [Integer] The number of baseline requests to take before switching to sampling # for the window. # enabled [Boolean] If the sampling feature should be used or not. # frequency [Integer] The number of requests to skip before observing during the sampling # window after the baseline. # responseFrequency [Integer] # window [Integer] # } # # disabled_assess_rules [array] Assess rules to disable for this application. attr_reader :assess_state # Current State for Protect. # enabled [Boolean] Indicate if the protect feature set is enabled for this server or not. # # Protection rules are returned as: # rules [Hash MODE>, nil] Hash with rule_id as key and mode as value attr_reader :protect_state # Current Application State. # # modes_by_id [Hash Mode] Returns Hash with rules and their current mode. # exclusion_matchers [Array] Array of all the exclusions. # code_exclusions [Array] Array of CodeExclusion: { # name [String] The name of the exclusion as defined by the user in TS. # modes [String] If this exclusion applies to assess or protect. [assess, defend] # assess_rules [Array] Array of assess rules to which this exclusion applies. AssessRuleID [String] # denylist [String] The call, if in the stack, should result in the agent not taking action. # input_exclusions [Array] Array of InputExclusions: { # name [String] The name of the input. # modes [String] If this exclusion applies to assess or protect. [assess, defend] # assess_rules [Array] Array of assess rules to which this exclusion applies. AssessRuleID [String] # protect_rules [Array] Array of ProtectRuleID [String] The protect rules to which this exclusion applies. # urls [Array] Array of URLs to which the exclusions apply. URL [String] # match_strategy [String] If this exclusion applies to all URLs or only those specified. [ALL, ONLY] # type [String] The type of the input [COOKIE, PARAMETER, HEADER, BODY, QUERYSTRING] # } # url_exclusions [Array] Array of UrlExclusions: { # name [String] The name of the input. # modes [String] If this exclusion applies to assess or protect. [assess, defend] # assess_rules [Array] Array of assess rules to which this exclusion applies. AssessRuleID [String] # protect_rules [Array] Array of ProtectRuleID [String] The protect rules to which this exclusion applies. # urls [Array] Array of URLs to which the exclusions apply. URL [String] # match_strategy [String] If this exclusion applies to all URLs or only those specified. [ALL, ONLY] # type [String] The type of the input [COOKIE, PARAMETER, HEADER, BODY, QUERYSTRING] # } attr_reader :application_state # This the structure that will hold the masking rules send from TS. # # @return [Contrast::Agent::Reporting::Settings::SensitiveDataMasking]: # mask_http_body [Boolean] Policy flag to enable the use of masking on request body. # rules [Array] # Rules to follow when using the masking. Each rules contains Id [String] # and Keywords [Array]. attr_reader :sensitive_data_masking # @return [Integer] the time, in ms, that application settings last changed attr_reader :last_app_update_ms # @return [Integer] the time, in ms, that server settings last changed attr_reader :last_server_update_ms def initialize reset_state end def code_exclusions @application_state.exclusion_matchers.select(&:code?) end # @param features_response [Contrast::Api::Settings::ServerFeatures, Contrast::Agent::Reporting::Response] def update_from_server_features features_response if features_response.cs__is_a?(Contrast::Agent::Reporting::Response) update_from_response(features_response) else @protect_state.enabled = features_response.protect_enabled? @assess_state.enabled = features_response.assess_enabled? @assess_state.sampling_settings = features_response.assess.sampling @last_server_update_ms = Contrast::Utils::Timer.now_ms end @last_server_update_ms = Contrast::Utils::Timer.now_ms end # @param settings_response [Contrast::Api::Settings::ApplicationSettings, Contrast::Agent::Reporting::Response] def update_from_application_settings settings_response if settings_response&.cs__class == Contrast::Agent::Reporting::Response update_from_response(settings_response) else new_vals = settings_response.application_state_translation @application_state.modes_by_id = new_vals[:modes_by_id] @application_state.exclusion_matchers = new_vals[:exclusion_matchers] @assess_state.disabled_assess_rules = new_vals[:disabled_assess_rules] @last_app_update_ms = Contrast::Utils::Timer.now_ms end @last_app_update_ms = Contrast::Utils::Timer.now_ms end # Wipe state to zero. def reset_state @protect_state = PROTECT_STATE_BASE.dup @assess_state = ASSESS_STATE_BASE.dup @application_state = APPLICATION_STATE_BASE.dup @tainted_columns = {} @sensitive_data_masking = SENSITIVE_DATA_MASKING_BASE.dup end def build_protect_rules @protect_state.rules = {} # Rules. They add themselves on initialize. Contrast::Agent::Protect::Rule::CmdInjection.new Contrast::Agent::Protect::Rule::Deserialization.new Contrast::Agent::Protect::Rule::HttpMethodTampering.new Contrast::Agent::Protect::Rule::NoSqli.new Contrast::Agent::Protect::Rule::PathTraversal.new Contrast::Agent::Protect::Rule::Sqli.new Contrast::Agent::Protect::Rule::UnsafeFileUpload.new Contrast::Agent::Protect::Rule::Xss.new Contrast::Agent::Protect::Rule::Xxe.new end # Update the sensitive data masking policy from settings, # received from TS. In case the settings are empty, # keep current ones. # # @param sensitive_data_masking [Contrast::Agent::Reporting::Settings::SensitiveDataMasking] # Ts Response settings for sensitive data masking policy def update_sensitive_data_policy sensitive_data_masking @sensitive_data_masking.mask_http_body = sensitive_data_masking.mask_http_body? unless settings_empty?(sensitive_data_masking.mask_http_body?) @sensitive_data_masking.mask_attack_vector = sensitive_data_masking.mask_attack_vector? unless settings_empty?(sensitive_data_masking.mask_attack_vector?) return if settings_empty?(sensitive_data_masking.rules) @sensitive_data_masking.rules = sensitive_data_masking.rules # update with the newly received rules. Contrast::Agent::Reporting::Masker.send(:update_dictionary) end private # @param response [Contrast::Agent::Reporting::Response] def update_from_response response if (server_features = response.server_features) update_server_features(server_features) end return unless (app_settings = response.application_settings) update_application_settings(app_settings) end # @param server_features [Contrast::Agent::Reporting::Settings::FeatureSettings] def update_server_features server_features return unless server_features log_file = server_features.log_file log_level = server_features.log_level Contrast::Logger::Log.instance.update(log_file, log_level) if log_file || log_level @protect_state.enabled = server_features.protect.enabled? @assess_state.enabled = server_features.assess.enabled? @assess_state.sampling_settings = server_features.assess.sampling @last_server_update_ms = Contrast::Utils::Timer.now_ms end # @param app_settings [Contrast::Agent::Reporting::Settings::ApplicationSettings] def update_application_settings app_settings return unless app_settings @application_state.modes_by_id = app_settings.protect.protection_rules_to_settings_hash # TODO: RUBY-1438 this needs to be translated # @application_state.exclusion_matchers = new_vals[:exclusion_matchers] update_sensitive_data_policy(app_settings.sensitive_data_masking) @assess_state.disabled_assess_rules = app_settings.assess.disabled_rules if app_settings.assess.session_id && !app_settings.assess.session_id.blank? @assess_state.session_id = app_settings.assess.session_id end @last_app_update_ms = Contrast::Utils::Timer.now_ms end # check if settings are empty and return true if so. # # @param settings [String, Boolean, Array, Hash] # @return true | false def settings_empty? settings return false if !!settings == settings return true if settings.nil? || settings.empty? false end end end end end