# Copyright (c) 2022 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/agent/reporting/reporting_events/finding_event_taint_range_tags' require 'contrast/utils/object_share' require 'contrast/agent/assess/policy/policy_node_utils' require 'contrast/components/logger' 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 include Contrast::Components::Logger::InstanceMethods include PolicyNodeUtils JSON_TAGS = 'tags' JSON_DATAFLOW = 'dataflow' # The keys used to read from policy.json to create the individual # policy nodes. These are common across node types JSON_SOURCE = 'source' ALL_TYPE = 'A' JSON_TARGET = 'target' TO_MARKER = '2' attr_accessor :tags, :type attr_reader :sources, :targets, :source_string, :target_string # Here are all methods that can use original objects without mutation the source. # For methods with REMOVE action, their (!) bang alternative is not listed, since # this methods tends to mutate the original (shortens it's length, remove special # symbols..) and causes mismatch in tags range representation of the target. In # short preshift is needed. ORIGINAL_OBJECT_METHODS = %i[ capitalize capitalize! chomp to_s downcase downcase! lstrip strip upcase! upcase ].cs__freeze TO_S = 'to_s' 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) @_use_original_object = ORIGINAL_OBJECT_METHODS.include?(@method_name) @_use_original_on_bang_method = assign_on_bang_check(policy_hash) end def assign_on_bang_check policy_hash # If we have KEEP action on String, and the method is to_s, that method would return self: # String#to_s => self or string. This method is included here to cover the situations such as # String.to_s.html_safe, where normally the dynamic sources properties get lost. To solve this # we will simply return the original object here. return true if @_use_original_object && policy_hash[JSON_METHOD_NAME] == TO_S @_use_original_object && # Check if method name ends with a (!) bang unless is the to_s method: policy_hash[JSON_METHOD_NAME].end_with?(Contrast::Utils::ObjectShare::BANG) end def feature 'Assess' end def node_class 'Node' end def node_type :TYPE_METHOD 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 rescue ArgumentError => e logger.debug('Validation of policy node failed with: ', e) nil 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. # @raise[ArgumentError] raises if any of the tags is invalid def validate_tags return unless tags tags.each do |tag| next if Contrast::Agent::Reporting::FindingEventTaintRangeTags::VALID_TAGS.include?(tag) || Contrast::Agent::Reporting::FindingEventTaintRangeTags::VALID_SOURCE_TAGS.include?(tag) raise(ArgumentError, "#{ node_class } #{ id } had an invalid tag. #{ tag } is not a known value.") end end # 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 ||= 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 = all_type?(source_string) ? ALL_TYPE : source_string target = all_type?(target_string) ? ALL_TYPE : target_string str = source[0] + TO_MARKER + target[0] str.to_sym end end @event_action end # This method will check if a method is fit to use it's original object and # that the method is without bang - it does not change the source, but rather # creates a copy of it. # # @return true | false def use_original_object? @_use_original_object && Contrast::ASSESS.track_original_object? end # This method will check if a method is fit to use it's original object and # that the target return is the same as object - a bang method modifying the # source. # # @return true | false def use_original_on_bang_method? @_use_original_on_bang_method && Contrast::ASSESS.track_original_object? end end end end end end