# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'json' require 'singleton' require 'contrast' require 'contrast/components/interface' require 'contrast/agent/patching/policy/module_policy' require 'contrast/agent/patching/policy/method_policy' module Contrast module Agent module Patching module Policy # This is just a holder for our policy. Takes the policy JSON and converts it into hashes that we can access # nicely. # # @abstract class Policy include Singleton include Contrast::Components::Interface # Indicates the folder in `resources` where this policy lives. def self.policy_folder raise(NoMethodError, 'specify policy_folder for patching') end # Indicates is this feature has been disabled by the configuration, read at startup, and therefore can never # be enabled. def disabled_globally? raise(NoMethodError, 'specify disabled_globally? conditions for patching') end def node_type raise(NoMethodError, 'specify the concrete node type for this poilcy') end access_component :analysis, :logging attr_reader :sources, :propagators, :triggers, :providers SOURCES_KEY = 'sources' PROPAGATION_KEY = 'propagators' RULES_KEY = 'rules' TRIGGERS_KEY = 'triggers' def self.policy_json File.join(policy_folder, 'policy.json').cs__freeze end def initialize @sources = [] @propagators = [] @triggers = [] @providers = {} json = Contrast::Utils::ResourceLoader.load(cs__class.policy_json) from_hash_string(json) end # Our policy for patching rules is a 'dope ass' JSON file. Rather than hard code in a bunch of things to # monkey patch, we let the JSON file define the conditions in which modifications are applied. This let's us # be flexible and extensible. def from_hash_string string # The default behavior of the agent is to load the policy on startup, as at this point we do not know in # which mode we'll be run. # # If the configuration file explicitly disables a feature, we know that we will not ever be able to enable # it, so in that case, we can skip policy loading. return if disabled_globally? policy_data = JSON.parse(string) policy_data[RULES_KEY].each do |rule_hash| rule_hash[TRIGGERS_KEY].each do |trigger_hash| trigger_node = node_type.new(trigger_hash, rule_hash) add_node(trigger_node) end end end def add_node node, node_type = :trigger unless node logger.error('Node was nil when adding node to policy') return end begin node.validate rescue ArgumentError => e logger.error('Node failed validation', e) return end case node_type when :source @sources << node when :propagator @propagators << node when :trigger @triggers << node when :dynamic_source module_names << node.class_name @sources << node else logger.error('Invalid node_type provided when adding node to policy', node_type: node_type) end end def module_names @_module_names ||= Set.new([sources, propagators, triggers].flatten.map!(&:class_name)) end def find_triggers_by_rule rule_id triggers.select { |trigger| trigger.rule_id == rule_id } end def find_source_node class_name, method_name, instance_method sources.find do |source| source.class_name == class_name && source.method_name == method_name && source.instance_method == instance_method end end def find_propagator_node class_name, method_name, instance_method propagators.find do |propagator| propagator.class_name == class_name && propagator.method_name == method_name && propagator.instance_method == instance_method end end def find_node rule_id, class_name, method_name, instance_method find_triggers_by_rule(rule_id).find do |node| node.class_name == class_name && node.method_name == method_name && node.instance_method == instance_method end end end end end end end