# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/protect/rule/base' require 'contrast/agent/protect/rule/base_service' module Contrast module Agent module Protect module Rule module SqlSampleBuilder # Generate a sample for the SQL injection detection rule, allowing for reporting to and rendering # by TeamServer # # @param context [Contrast::Agent::RequestContext] the context for the current request # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, # if one exists, in the case of multiple inputs being found to violate the protection criteria # @candidate_string [String] the value of the input which may be an attack # @kwargs [Hash] key - value pairs of context individual rules need to build out details # to send to the Service to tell the story of the attack # @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack module SqliSample def build_sample context, input_analysis_result, candidate_string, **kwargs sqli_sample = build_base_sample(context, input_analysis_result) sqli_sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new sqli_sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string) sqli_sample.sqli.start_idx = kwargs[:start_idx] sqli_sample.sqli.end_idx = kwargs[:end_idx] sqli_sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i sqli_sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i sqli_sample end end # Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering # by TeamServer # # @param context [Contrast::Agent::RequestContext] the context for the current request # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, # if one exists, in the case of multiple inputs being found to violate the protection criteria # @candidate_string [String] the value of the input which may be an attack # @kwargs [Hash] key - value pairs of context individual rules need to build out details # to send to the Service to tell the story of the attack # @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack module NoSqliSample def build_sample context, input_analysis_result, candidate_string, **kwargs no_sqli_sample = build_base_sample(context, input_analysis_result) no_sqli_sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new no_sqli_sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string) no_sqli_sample.no_sqli.start_idx = kwargs[:start_idx].to_i no_sqli_sample.no_sqli.end_idx = kwargs[:end_idx].to_i no_sqli_sample.no_sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i no_sqli_sample.no_sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i no_sqli_sample end end # This Module is how we apply the attack fo NoSQL and SQL Injection rule. # It includes methods for building attack with match and database scanners module AttackBuilder # Set up an attack result and assigns Database scanner for the No-SQL and SQLI injection detection rules # # @param context [Contrast::Agent::RequestContext] the context for the current request # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, # if one exists, in the case of multiple inputs being found to violate the protection criteria # @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists, # in the case of multiple inputs being found to violate the protection criteria # @query_string [string] he value of the input which may be an attack # @kwargs [Hash] key - value pairs of context individual rules need to build out details to send # to the Service to tell the story of the attack # @return [Contrast::Api::Dtm::AttackResult] the result from this attack def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION || mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT return result end attack_string = input_analysis_result.value regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE) return unless query_string.match?(regexp) database = kwargs[:database] scanner = select_scanner(database) ss = StringScanner.new(query_string) length = attack_string.length while ss.scan_until(regexp) # the pos of StringScanner is at the end of the regexp (input string), # we need the beginning idx = ss.pos - attack_string.length last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value) next unless last_boundary && boundary result ||= build_attack_result(context) record_match(idx, length, boundary, last_boundary, kwargs) append_match(context, input_analysis_result, result, query_string, **kwargs) end result end def select_scanner database @scanners ||= { Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL => Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new, Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG => Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new, Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE => Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new, Contrast::Agent::Protect::Policy::AppliesNoSqliRule::DATABASE_NOSQL => Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new }.cs__freeze @default_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new @scanners[database.to_s] || @default_scanner end def record_match idx, length, boundary, last_boundary, kwargs kwargs[:start_idx] = idx kwargs[:end_idx] = idx + length kwargs[:boundary_overrun_idx] = boundary kwargs[:input_boundary_idx] = last_boundary end def append_match context, input_analysis_result, result, query_string, **kwargs input_analysis_result.attack_count = input_analysis_result.attack_count + 1 update_successful_attack_response(context, input_analysis_result, result, query_string) append_sample(context, input_analysis_result, result, query_string, **kwargs) end end end end end end end