# Copyright (c) 2023 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/reporting/details/sqli_details' require 'contrast/agent/reporting/details/no_sqli_details' 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::Agent::Reporting, 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 TeamServer to tell the story of the attack # @return [Contrast::Agent::Reporting::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.details = Contrast::Agent::Reporting::Details::SqliDetails.new sqli_sample.details.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string) sqli_sample.details.start_idx = kwargs[:start_idx] sqli_sample.details.end_idx = kwargs[:end_idx] sqli_sample.details.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i sqli_sample.details.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::Agent::Reporting, 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 TeamServer to tell the story of the attack # @return [Contrast::Agent::Reporting::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.details = Contrast::Agent::Reporting::Details::NoSqliDetails.new no_sqli_sample.details.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string) no_sqli_sample.details.start_idx = kwargs[:start_idx].to_i no_sqli_sample.details.end_idx = kwargs[:end_idx].to_i no_sqli_sample.details.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i no_sqli_sample.details.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::Agent::Reporting, 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::Agent::Reporting, nil] previous attack result for this rule, if one exists, # in the case of multiple inputs being found to violate the protection criteria # @param query_string [String] the value of the input which may be an attack # @param kwargs [Hash] key - value pairs of context individual rules need to build out details to send # to TeamServer to tell the story of the attack # @return [Contrast::Agent::Reporting] the result from this attack def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs return result if mode == :NO_ACTION || mode == :PERMIT attack_string = input_analysis_result.value regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE) # extract struct result from kwargs agent_lib_struct_result = kwargs[:result_struct] 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) # if the struct actually has the needed data in it - use it if agent_lib_struct_result.cs__is_a?(Hash) record_agent_lib_match(agent_lib_struct_result, length, kwargs) else record_match(idx, length, boundary, last_boundary, kwargs) end 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 # all the agent_lib checks methods needed # @param struct[ResultingStruct] The struct including all the data from the agent_lib scan def record_agent_lib_match struct, length, kwargs kwargs[:start_idx] = struct[:start_index] kwargs[:end_idx] = if (struct[:end_index]).zero? struct[:start_index] + length else struct[:end_index] end kwargs[:boundary_overrun_idx] = struct[:boundary_overrun_index] kwargs[:input_boundary_idx] = struct[:input_boundary_index] 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