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

require 'contrast/agent/patching/policy/policy_node'
require 'contrast/api/decorators/trace_taint_range_tags'

module Contrast
  module Agent
    module Assess
      module Policy
        # This class functions to translate our policy.json into an actionable
        # Ruby object, allowing for dynamic patching over hardcoded patching.
        class PolicyNode < Contrast::Agent::Patching::Policy::PolicyNode
          attr_accessor :tags, :type
          attr_reader   :sources, :targets, :source_string, :target_string

          def initialize policy_hash = {}
            super(policy_hash)
            @source_string = policy_hash[JSON_SOURCE]
            @target_string = policy_hash[JSON_TARGET]
            @tags = Set.new(policy_hash[JSON_TAGS])
            @sources = convert_policy_markers(source_string)
            @targets = convert_policy_markers(target_string)
          end

          def feature
            'Assess'
          end

          def node_class
            'Node'
          end

          def node_type
            :TYPE_METHOD
          end

          def target
            @_target ||= begin
              if targets&.any?
                targets[0]
              elsif sources&.any?
                sources[0]
              end
            end
          end

          def target_string= value
            @target_string = value
            @targets = convert_policy_markers(value)
          end

          # Sometimes we need to tie information to an event. We'll add a
          # properties section to the patch node, which can pass them along to
          # the pre-dtm event
          def add_property name, value
            return unless name && value

            @properties ||= {}
            @properties[name] = value
          end

          def get_property name
            return unless @properties

            @properties[name]
          end

          # Don't let nodes be created that will be missing things we need
          # later on. Really, if they don't have these things, they couldn't have
          # done their jobs anyway.
          def validate
            super
            validate_tags
          end

          # TeamServer is picky. The tags here match to ENUMs there. If there
          # isn't a matching ENUM in TS land, the database gets got. We really
          # don't want to get them, so we're going to prevent the node from being
          # made.
          def validate_tags
            return unless tags

            tags.each do |tag|
              next if Contrast::Api::Decorators::TraceTaintRangeTags::VALID_TAGS.include?(tag) ||
                  Contrast::Api::Decorators::TraceTaintRangeTags::VALID_SOURCE_TAGS.include?(tag)

              raise(ArgumentError,
                    "#{ node_class } #{ id } had an invalid tag. #{ tag } is not a known value.")
            end
          end

          ALL_TYPE = 'A'
          TO_MARKER = '2'
          # Convert our action, built from our source and target, into
          # the TS appropriate action. That's a single source to single
          # target marker (A,O,P,R)
          #
          # Creation (source nodes) don't have sources (they are the source)
          # Trigger (trigger nodes) don't have targets (they are the target)
          # Everything else (propagation nodes) are Source2Target
          def build_action
            @event_action ||= begin
              case node_class
              when Contrast::Agent::Assess::Policy::SourceNode::SOURCE
                :CREATION
              when Contrast::Agent::Assess::Policy::TriggerNode::TRIGGER
                :TRIGGER
              else
                if source_string.nil?
                  :CREATION
                elsif target_string.nil?
                  :TRIGGER
                else
                  # TeamServer can't handle the multi-source or multi-target
                  # values. Give it some help by changing them to 'A'
                  source = source_string.include?(Contrast::Utils::ObjectShare::COMMA) ? ALL_TYPE : source_string
                  target = target_string.include?(Contrast::Utils::ObjectShare::COMMA) ? ALL_TYPE : target_string
                  str = source[0] + TO_MARKER + target[0]
                  str.to_sym
                end
              end
            end
            @event_action
          end

          # The keys used to read from policy.json to create the individual
          # policy nodes. These are common across node types
          JSON_SOURCE = 'source'
          JSON_TARGET = 'target'
          JSON_TAGS = 'tags'
          JSON_DATAFLOW = 'dataflow'

          private

          # Given a policy string in the format A,B,C, populate the given array
          # 1) Split on ','
          # 2) If 'O' or 'R', add the array, else it's P and needs to be
          #    converted. P type will either be P# where # is the index
          #    of the parameter. Drop the P and store the # as an int.
          #
          # @param markers [String] the String from the policy to parse
          # @return [Array] the array generated by converting the marker string
          def convert_policy_markers markers
            return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless markers
            return Contrast::Utils::ObjectShare::EMPTY_ARRAY if markers.empty?

            converted = []
            markers.split(Contrast::Utils::ObjectShare::COMMA).each do |t|
              case t
              when Contrast::Utils::ObjectShare::OBJECT_KEY,
                   Contrast::Utils::ObjectShare::RETURN_KEY

                converted << t
              else
                converted << Integer(t[1..-1])
              end
            end
            converted
          end
        end
      end
    end
  end
end