# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'json' cs__scoped_require 'contrast' cs__scoped_require 'contrast/components/interface' cs__scoped_require 'contrast/agent/patching/policy/module_policy' cs__scoped_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(NotImplementedError, '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(NotImplementedError, 'specify disabled_globally? conditions for patching') end def node_type raise(NotImplementedError, 'specify the concrete node type for this poilcy') end access_component :logging, :analysis attr_accessor :providers, :tracked_classes attr_reader :sources, :propagators, :triggers, :patched_names SOURCES_KEY = 'sources' PROPAGATION_KEY = 'propagators' RULES_KEY = 'rules' TRIGGERS_KEY = 'triggers' TRACKED_CLASSES_KEY = 'tracked_classes' def self.policy_json File.join(policy_folder, 'policy.json').cs__freeze end def initialize @sources = [] @propagators = [] @triggers = [] @providers = {} @tracked_classes = [] @patched_names = Set.new 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(nil, 'Node was nil when adding node to policy') return end begin node.validate rescue ArgumentError => e logger.error(e, e.message) 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(nil, "Invalid node_type: #{ node_type } provided when adding node to policy") end end def module_names @_module_names ||= begin m = Set.new tracked_classes.each { |tracked| m << tracked } sources.each { |source| m << source.class_name } propagators.each { |propagator| m << propagator.class_name } triggers.each { |trigger| m << trigger.class_name } m end end def find_triggers_by_rule rule_id triggers.select { |trigger| trigger.rule_id == rule_id } 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