# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Agent
    module Assess
      module Policy
        module Trigger
          # This acts a trigger to handle the special cases of the Tilt
          # library gem. Reflected XSS data may come into the trigger methods
          # from these classes.
          class ReflectedXss
            class << self
              NODE_HASH = {
                  'class_name' => 'Tilt::Template',
                  'instance_method' => true,
                  'method_name' => 'render',
                  'method_visibility' => 'public',
                  'action' => 'CUSTOM',
                  'source' => 'O,P0',
                  'target' => 'R',
                  'patch_class' => 'Contrast::Agent::Assess::Policy::Trigger::ReflectedXss',
                  'patch_method' => 'xss_tilt_trigger'
              }.cs__freeze
              TEMPLATE_PROPAGATION_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(NODE_HASH)

              def xss_tilt_trigger trigger_node, _source, object, ret, *args
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))

                scope = args[0]
                erb_template_prerender = object.instance_variable_get(:@data)
                interpolated_inputs = []
                handle_binding_variables(scope, erb_template_prerender, ret, properties, interpolated_inputs)
                handle_local_variables(args, erb_template_prerender, ret, properties, interpolated_inputs)
                event_data = Contrast::Agent::Assess::Events::EventData.new(TEMPLATE_PROPAGATION_NODE,
                                                                            ret,
                                                                            erb_template_prerender,
                                                                            ret,
                                                                            interpolated_inputs)
                properties.build_event(event_data)
                unless interpolated_inputs.empty?
                  current_event = properties.event
                  interpolated_inputs.each do |input|
                    input_properties = Contrast::Agent::Assess::Tracker.properties(input)
                    next unless input_properties&.event

                    current_event.parent_events << input_properties.event
                  end
                end

                if Contrast::Agent::Assess::Tracker.tracked?(ret)
                  Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(trigger_node,
                                                                               ret,
                                                                               erb_template_prerender,
                                                                               ret,
                                                                               interpolated_inputs)
                end

                ret
              end

              private

              def handle_binding_variables scope, erb_template_prerender, ret, properties, interpolated_inputs
                binding_variables = scope.instance_variables

                binding_variables.each do |bound_variable_sym|
                  bound_variable_value = scope.instance_variable_get(bound_variable_sym)

                  next unless Contrast::Agent::Assess::Tracker.tracked?(bound_variable_value)
                  next unless erb_template_prerender.include?(bound_variable_sym.to_s)

                  start_index = ret.index(bound_variable_value)
                  next if start_index.nil?

                  properties.copy_from(bound_variable_value, ret, start_index)
                  interpolated_inputs << bound_variable_sym
                end
              end

              def handle_local_variables args, erb_template_prerender, ret, properties, interpolated_inputs
                locals = args[1]
                locals.each do |local_name, local_value|
                  next unless Contrast::Agent::Assess::Tracker.tracked?(local_value)
                  next unless erb_template_prerender.include?(local_name.to_s)

                  start_index = ret.index(local_value)
                  next if start_index.nil?

                  properties.copy_from(local_value, ret, start_index)
                  interpolated_inputs << local_name
                end
              end
            end
          end
        end
      end
    end
  end
end