# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/scope' require 'contrast/utils/object_share' module Contrast module Agent module Patching module Policy # This class functions to translate our policy.json into an actionable # Ruby object, allowing for dynamic patching over hardcoded patching. # # @abstract class PolicyNode include Contrast::Components::Scope::InstanceMethods JSON_METHOD_VISIBILITY = 'method_visibility' JSON_PROPERTIES = 'properties' JSON_METHOD_NAME = 'method_name' JSON_METHOD_SCOPE = 'scope' # The keys used to read from policy.json to create the individual # policy nodes. These are common across node types JSON_CLASS_NAME = 'class_name' JSON_INSTANCE_METHOD = 'instance_method' def initialize policy_hash = {} @class_name = policy_hash[JSON_CLASS_NAME] @instance_method = policy_hash[JSON_INSTANCE_METHOD] @method_name = policy_hash[JSON_METHOD_NAME] @method_scope = policy_hash[JSON_METHOD_SCOPE] @method_visibility = policy_hash[JSON_METHOD_VISIBILITY] @properties = policy_hash[JSON_PROPERTIES] symbolize end # Name of the class in which the method is being invoked. attr_accessor :class_name # Check for instance method. # # @return true | false attr_accessor :instance_method # The symbol representation of the invoked method. attr_accessor :method_name # Visibility of the invoked method [Private, Public, Protected] attr_accessor :method_visibility # Properties parsed from our JSON policy. attr_reader :properties # Scope of the method parsed from our JSON policy. attr_reader :method_scope # @raise [NoMethodError] This is being raised if any of the implementing subclasses does not have # that method implemented, but is being called on. def node_class raise(NoMethodError, 'specify the type of the feature for which this node patches') end # @raise [NoMethodError] This is being raised if any of the implementing subclasses does not have # that method implemented, but is being called on. def feature raise(NoMethodError, 'specify the name of the feature for which this node patches') end def id @_id ||= "#{ feature }:#{ node_class }:#{ class_name }#{ instance_method? ? '#' : '.' }#{ method_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. # # @raise [ArgumentError] Validates if the created nodes have everything that we'll need now or later on. def validate unless class_name raise(ArgumentError, "#{ node_class } #{ id } did not have a proper class name. Unable to create.") end unless method_name raise(ArgumentError, "#{ node_class } #{ id } did not have a proper method name. Unable to create.") end unless method_name.is_a?(Symbol) raise(ArgumentError, "#{ node_class } #{ id } has a non symbol @method_name value. Unable to create.") end unless method_visibility.is_a?(Symbol) raise(ArgumentError, "#{ node_class } #{ id } has a non symbol @method_visibility value. Unable to create.") end unless method_scope.nil? || Contrast::Agent::Scope.valid_scope?(method_scope) raise(ArgumentError, "#{ node_class } #{ id } requires an undefined scope. Unable to create.") end nil end # just turns this into a ruby-ism def instance_method? instance_method end private # Convert strings to symbols here, once, to avoid doing so on every # comparison at runtime def symbolize @method_name = @method_name.to_sym if @method_name @method_visibility = @method_visibility.to_sym if @method_visibility @method_scope = @method_scope.to_sym if @method_scope end end end end end end