lib/contrast/agent/assess/policy/trigger_method.rb in contrast-agent-3.8.5 vs lib/contrast/agent/assess/policy/trigger_method.rb in contrast-agent-3.9.0

- old
+ new

@@ -21,187 +21,272 @@ access_component :logging, :analysis # The level of TeamServer compliance our traces meet CURRENT_FINDING_VERSION = 2 - def self.settings - Contrast::Agent::FeatureState.instance - end + class << self + # This is called from within our woven proc. It will be called as if it + # were inline in the Rack application. + # + # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] + # the node that applies to the method being called + # @param object [Object] the Object on which the method was invoked + # @param ret [Object] the Return of the invoked method + # @param args [Array<Object>] the Arguments with which the method + # was invoked + def apply_trigger_rule trigger_node, object, ret, args + return if trigger_node.nil? - def self.apply_trigger_rule trigger_node, object, ret, args - return if trigger_node.nil? + current_context = Contrast::Agent::REQUEST_TRACKER.current + return unless current_context&.analyze_request? && ASSESS.enabled? - current_context = Contrast::Agent::REQUEST_TRACKER.current - return unless current_context&.analyze_request? && ASSESS.enabled? - - if trigger_node.sources&.any? - trigger_node.sources.each do |marker| - source = determine_source(marker, object, ret, args) - TriggerMethod.cs__apply_trigger(current_context, - trigger_node, - source, - object, - ret, - 1, - *args) + if trigger_node.sources&.any? + trigger_node.sources.each do |marker| + source = determine_source(marker, object, ret, args) + apply_trigger(current_context, + trigger_node, + source, + object, + ret, + 1, + *args) + end + else + apply_trigger(current_context, + trigger_node, + nil, + object, + ret, + 1, + *args) end - else - TriggerMethod.cs__apply_trigger(current_context, - trigger_node, - nil, - object, - ret, - 1, - *args) end - end - def self.cs__apply_trigger context, trigger_node, source, object, ret, invoked, *args - return unless context && trigger_node - return if trigger_node.rule_disabled? - return if trigger_node.dataflow? && source.nil? + def apply_eval_trigger context, trigger_node, source, object, ret, invoked, *args + apply_trigger(context, trigger_node, source, object, ret, invoked, *args) + end - invoked += 1 - if trigger_node.regexp_rule? - apply_regexp_rule(context, trigger_node, source, object, ret, invoked, *args) - elsif trigger_node.custom_trigger? - trigger_node.apply_custom_trigger(context, trigger_node, source, object, ret, invoked, *args) - elsif trigger_node.dataflow? - apply_dataflow_rule(context, trigger_node, source, object, ret, invoked, *args) - else # trigger rule - just calling the method is dangerous - build_finding(context, trigger_node, source, object, ret, invoked, *args) + # This converts the source of the finding, and the events leading + # up to it into a Finding + # + # @param context [Contrast::Utils::ThreadTracker] the current request + # context + # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] + # the node to direct applying this trigger event + # @param source [Object] the source of the Trigger Event + # @param object [Object] the Object on which the method was invoked + # @param ret [Object] the Return of the invoked method + # @param invoked [Integer] the depth of this invocation from + # application code; often a lie. + # @param args [Array<Object>] the Arguments with which the method + # was invoked + # @return [Contrast::Api::Dtm::Finding, nil] the + # Contrast::Api::Dtm::Finding to send to TeamServer or nil if + # conditions were not met + def build_finding context, trigger_node, source, object, ret, invoked, *args + return unless Contrast::Agent::Assess::Policy::TriggerValidation.valid?(trigger_node, object, ret, args) + + request = context.request + env = request.env + return if defined?(ActionController::Live) && + env && + env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live) + + finding = Contrast::Api::Dtm::Finding.new + finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id) + finding.session_id = Contrast::Agent::FeatureState.instance.current_session_id + finding.version = CURRENT_FINDING_VERSION + + build_from_source(finding, source) + trigger_event = Contrast::Agent::Assess::ContrastEvent.new(trigger_node, source, object, ret, args, invoked + 1).to_dtm_event + finding.events << trigger_event + build_hash(finding, source) + build_tags(context) + finding.routes << context.route if context.route + context.activity.findings << finding + logger.debug(nil, "Trigger #{ trigger_node.id } detected: #{ source.__id__ } triggered #{ trigger_node.rule_id }") + rescue StandardError => e + logger.error(e, "Unable to build a finding for #{ trigger_node.id }") end - rescue StandardError => e - logger.warn(e, 'Unable to apply trigger.') - end - # Given the marker from the trigger_node (the pointer indicating the entity - # from which the taint originated), return the entity on which this - # trigger needs to operate. - # - # In an effort to speed up this lookup, we've changed the marker for - # parameters to be implicit - if it is not a return or an object, it - # must be a parameter, which we can reference either by index or by - # name. - def self.determine_source marker, object, ret, args - case marker - when Contrast::Utils::ObjectShare::RETURN_KEY - ret - when Contrast::Utils::ObjectShare::OBJECT_KEY - object - else # 'P' - if marker.is_a?(Integer) - args[marker] - else - arg = nil - args.each do |search| - next unless search.is_a?(Hash) + private - arg = search[marker] - break if arg - end - arg - end + def settings + Contrast::Agent::FeatureState.instance end - end - def self.apply_regexp_rule context, trigger_node, source, object, ret, invoked, *args - return unless source.is_a?(String) - return if trigger_node.good_value && source.match?(trigger_node.good_value) - return if trigger_node.bad_value && source !~ trigger_node.bad_value + # This is our method that actually checks the taint on the object + # our trigger_node targets. + # + # @param context [Contrast::Utils::ThreadTracker] the current request + # context + # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] + # the node to direct applying this trigger event + # @param source [Object] the source of the Trigger Event + # @param object [Object] the Object on which the method was invoked + # @param ret [Object] the Return of the invoked method + # @param invoked [Integer] the depth of this invocation from + # application code; often a lie. + # @param args [Array<Object>] the Arguments with which the method + # was invoked + def apply_trigger context, trigger_node, source, object, ret, invoked, *args + return unless context && trigger_node + return if trigger_node.rule_disabled? + return if trigger_node.dataflow? && source.nil? - invoked += 1 - build_finding(context, trigger_node, source, object, ret, invoked, *args) - end + invoked += 1 + if trigger_node.regexp_rule? + apply_regexp_rule(context, trigger_node, source, object, ret, invoked, *args) + elsif trigger_node.custom_trigger? + trigger_node.apply_custom_trigger(context, trigger_node, source, object, ret, invoked, *args) + elsif trigger_node.dataflow? + apply_dataflow_rule(context, trigger_node, source, object, ret, invoked, *args) + else # trigger rule - just calling the method is dangerous + build_finding(context, trigger_node, source, object, ret, invoked, *args) + end + rescue StandardError => e + logger.warn(e, 'Unable to apply trigger.') + end - def self.apply_dataflow_rule context, trigger_node, source, object, ret, invoked, *args - return unless source + # Given the marker from the trigger_node (the pointer indicating + # the entity from which the taint originated), return the entity on + # which this trigger needs to operate. + # + # In an effort to speed up this lookup, we've changed the marker + # for parameters to be implicit - if it is not a return or an + # object, it must be a parameter, which we can reference either by + # index or by name. + # + # @param marker [String] the source marker that indicates which + # Object + # @param object [Object] the Object on which the method was invoked + # @param ret [Object] the Return of the invoked method + # @param args [Array<Object>] the Arguments with which the method + # was invoked + # @return [Object] the literal object that this Trigger Event + # verifies + def determine_source marker, object, ret, args + case marker + when Contrast::Utils::ObjectShare::RETURN_KEY + ret + when Contrast::Utils::ObjectShare::OBJECT_KEY + object + else # 'P' + if marker.is_a?(Integer) + args[marker] + else + arg = nil + args.each do |search| + next unless search.is_a?(Hash) - invoked += 1 - if Contrast::Utils::DuckUtils.quacks_to?(source, :cs__properties) - return unless source.cs__tracked? - return unless trigger_node.violated?(source) + arg = search[marker] + break if arg + end + arg + end + end + end + # This is our method that actually checks the taint on the object + # our trigger_node targets for our Regexp based rules. + # + # @param context [Contrast::Utils::ThreadTracker] the current request + # context + # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] + # the node to direct applying this trigger event + # @param source [Object] the source of the Trigger Event + # @param object [Object] the Object on which the method was invoked + # @param ret [Object] the Return of the invoked method + # @param invoked [Integer] the depth of this invocation from + # application code; often a lie. + # @param args [Array<Object>] the Arguments with which the method + # was invoked + def apply_regexp_rule context, trigger_node, source, object, ret, invoked, *args + return unless source.is_a?(String) + return if trigger_node.good_value && source.match?(trigger_node.good_value) + return if trigger_node.bad_value && source !~ trigger_node.bad_value + + invoked += 1 build_finding(context, trigger_node, source, object, ret, invoked, *args) - elsif Contrast::Utils::DuckUtils.quacks_like_tracked_hash?(source) - invoked += 2 # the each & the block - source.each_pair do |key, value| - apply_dataflow_rule(context, trigger_node, key, object, ret, invoked, *args) - apply_dataflow_rule(context, trigger_node, value, object, ret, invoked, *args) - end - elsif Contrast::Utils::DuckUtils.quacks_like_tracked_enumerable?(source) - invoked += 2 # the each & the block - source.each do |value| - apply_dataflow_rule(context, trigger_node, value, object, ret, invoked, *args) - end - else - logger.warn( - nil, - "Target is a #{ source.cs__class.name } -- not sure how to inspect: #{ trigger_node.inspect }") - logger.debug(nil, source.to_s[0..99]) end - end - def self.build_finding context, trigger_node, source, object, ret, invoked, *args - return unless Contrast::Agent::Assess::Policy::TriggerValidation.valid?(trigger_node, object, ret, args) + # This is our method that actually checks the taint on the object + # our trigger_node targets for our Dataflow based rules. + # + # @param context [Contrast::Utils::ThreadTracker] the current request + # context + # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] + # the node to direct applying this trigger event + # @param source [Object] the source of the Trigger Event + # @param object [Object] the Object on which the method was invoked + # @param ret [Object] the Return of the invoked method + # @param invoked [Integer] the depth of this invocation from + # application code; often a lie. + # @param args [Array<Object>] the Arguments with which the method + # was invoked + def apply_dataflow_rule context, trigger_node, source, object, ret, invoked, *args + return unless source - request = context.request - env = request.env - return if defined?(ActionController::Live) && - env && - env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live) + invoked += 1 + if Contrast::Utils::DuckUtils.quacks_to?(source, :cs__properties) + return unless source.cs__tracked? + return unless trigger_node.violated?(source) - finding = Contrast::Api::Dtm::Finding.new - finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id) - finding.session_id = Contrast::Agent::FeatureState.instance.current_session_id - finding.version = CURRENT_FINDING_VERSION + build_finding(context, trigger_node, source, object, ret, invoked, *args) + elsif Contrast::Utils::DuckUtils.iterable_hash?(source) + invoked += 2 # the each & the block + source.each_pair do |key, value| + apply_dataflow_rule(context, trigger_node, key, object, ret, invoked, *args) + apply_dataflow_rule(context, trigger_node, value, object, ret, invoked, *args) + end + elsif Contrast::Utils::DuckUtils.iterable_enumerable?(source) + invoked += 2 # the each & the block + source.each do |value| + apply_dataflow_rule(context, trigger_node, value, object, ret, invoked, *args) + end + else + logger.warn( + nil, + "Target is a #{ source.cs__class.name } -- not sure how to inspect: #{ trigger_node.inspect }") + logger.debug(nil, source.to_s[0..99]) + end + end - build_from_source(finding, source) - trigger_event = Contrast::Agent::Assess::ContrastEvent.new(trigger_node, source, object, ret, args, invoked + 1).to_dtm_event - finding.events << trigger_event - build_hash(finding, source) - build_tags(context) - finding.routes << context.route if context.route - context.activity.findings << finding - logger.debug(nil, "Trigger #{ trigger_node.id } detected: #{ source.__id__ } triggered #{ trigger_node.rule_id }") - rescue StandardError => e - logger.error(e, "Unable to build a finding for #{ trigger_node.id }") - end + def build_from_source finding, source + return unless source + return unless Contrast::Utils::DuckUtils.quacks_to?( + source, + :cs__properties) + return unless source.cs__properties - def self.build_from_source finding, source - return unless source - return unless Contrast::Utils::DuckUtils.quacks_to?( - source, - :cs__properties) - return unless source.cs__properties + # events could technically be nil, but we would have failed + # the rule check before getting here. not worth the nil check + source.cs__properties.events.each do |event| + finding.events << event.to_dtm_event + end - # events could technically be nil, but we would have failed - # the rule check before getting here. not worth the nil check - source.cs__properties.events.each do |event| - finding.events << event.to_dtm_event + # Google::Protobuf::Map doesn't support merge!, so we have to do this + # long form + source_props = source.cs__properties.properties + return unless source_props + + source_props.each_pair do |key, value| + key = Contrast::Utils::StringUtils.force_utf8(key) + finding.properties[key] = Contrast::Utils::StringUtils.force_utf8(value) + end end - # Google::Protobuf::Map doesn't support merge!, so we have to do this - # long form - source_props = source.cs__properties.properties - return unless source_props - - source_props.each_pair do |key, value| - key = Contrast::Utils::StringUtils.force_utf8(key) - finding.properties[key] = Contrast::Utils::StringUtils.force_utf8(value) + def build_hash finding, source + hash_code = Contrast::Utils::HashDigest.generate_event_hash(finding, source) + finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash_code) + finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding) end - end - def self.build_hash finding, source - hash_code = Contrast::Utils::HashDigest.generate_event_hash(finding, source) - finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash_code) - finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding) - end + def build_tags context + return unless ASSESS.tags - def self.build_tags context - return unless ASSESS.tags - - context.activity.finding_tags = Contrast::Utils::StringUtils.force_utf8(ASSESS.tags) + context.activity.finding_tags = Contrast::Utils::StringUtils.force_utf8(ASSESS.tags) + end end end end end end