# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/assess/policy/trigger_method' require 'contrast/components/logger' require 'contrast/extension/module' require 'contrast/agent/reporting/report' module Contrast module Agent module Assess module Rule module Provider # Hardcoded rules detect if any secret value has been written # directly into the sourcecode of the application. To use this base # class, a provider must implement three methods: # 1) name_passes? : does the constant name match a given value set # 2) value_node_passes? : does the value of the constant match a # given value set # 3) redacted_marker : the value to plug in for the obfuscated value module HardcodedValueRule include Contrast::Components::Logger::InstanceMethods def disabled? !::Contrast::ASSESS.enabled? || ::Contrast::ASSESS.rule_disabled?(rule_id) end # Parse the file pertaining to the given TracePoint to walk its AST # to determine if a Constant is hardcoded. For our purposes, this # hard coding means directly set rather than as an interpolated # String or through a method call. # # Note: This is a top layer check, we make no assertions about what # the methods or interpolations do. Their presence, even if only # calling a hardcoded thing, causes this check to not report. # # @param trace_point [TracePoint] the TracePoint event created on # the :end of a Module being loaded # @param ast [RubyVM::AbstractSyntaxTree::Node] the abstract syntax # tree of the Module defined in the TracePoint end event def parse trace_point, ast return if disabled? parse_ast(trace_point.self, ast) rescue StandardError => e logger.error('Unable to parse AST for hardcoded keys', e, module: trace_point.self) end # The name of the field CONSTANT_NAME_KEY = 'name' # The code line, recreated, with the password obfuscated CODE_SOURCE_KEY = 'codeSource' # The constant name SOURCE_KEY = 'source' private # @param mod [Module] the module to which this AST pertains # @param ast [RubyVM::AbstractSyntaxTree::Node, Object] a node # within the AST, which may be a leaf, so any Object def parse_ast mod, ast return unless ast.cs__is_a?(RubyVM::AbstractSyntaxTree::Node) return unless ast.cs__respond_to?(:children) children = ast.children return unless children.any? ast.children.each do |child| parse_ast(mod, child) end # https://www.rubydoc.info/gems/ruby-internal/Node/CDECL return unless ast.type == :CDECL # The CDECL Node has two children, the first being the Constant # name as a symbol, the second as the value to assign to that # constant children = ast.children name = children[0].to_s # If that constant name doesn't pass our checks, move on. return unless name_passes?(name) value = children[1] # The assignment node could be a direct value or a call of some # sort. We leave it to each rule to properly handle these nodes. return unless value_node_passes?(value) if Contrast::Agent::Reporter.enabled? new_finding_and_reporting mod, name else # TODO: RUBY-1438 -- remove build_finding mod, name end end # Constants can be set as frozen directly. We need to account for # this change as it means the Node given to the :CDECL call will be # a :CALL, not a constant. # # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to # evaluate # @return [Boolean] is this a freeze call or not def freeze_call? value_node return false unless value_node.type == :CALL children = value_node.children return false unless children return false unless children.length >= 2 children[1] == :freeze end def build_finding clazz, constant_string class_name = clazz.cs__name finding = assign_finding class_name, constant_string Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding) rescue StandardError => e logger.error('Unable to build a finding for Hardcoded Rule', e) nil end def assign_finding class_name, constant_string finding = Contrast::Api::Dtm::Finding.new finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(rule_id) finding.version = Contrast::Agent::Assess::Policy::TriggerMethod::CURRENT_FINDING_VERSION finding.properties[SOURCE_KEY] = Contrast::Utils::StringUtils.protobuf_safe_string(class_name) finding.properties[CONSTANT_NAME_KEY] = Contrast::Utils::StringUtils.protobuf_safe_string(constant_string) finding.properties[CODE_SOURCE_KEY] = Contrast::Utils::StringUtils.protobuf_safe_string(constant_string + redacted_marker) hash = Contrast::Utils::HashDigest.generate_class_scanning_hash(finding) finding.hash_code = Contrast::Utils::StringUtils.protobuf_safe_string(hash) finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding) finding end def new_finding_and_reporting clazz, constant_string return unless Contrast::Agent::Reporter.enabled? # sent to reporter # and add logger message for the report of the preflight new_preflight = Contrast::Agent::Reporting::Preflight.new new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new new_preflight_message.hash_code = hash new_preflight_message.data = "#{ rule_id },#{ hash }" new_preflight.messages << new_preflight_message # extract to new method # here we will generate new type of finding ruby_finding = Contrast::Agent::Reporting::Finding.new rule_id ruby_finding.hash_code = hash ruby_finding.properties[SOURCE_KEY] = clazz.cs__name ruby_finding.properties[CONSTANT_NAME_KEY] = constant_string ruby_finding.properties[CODE_SOURCE_KEY] = constant_string + redacted_marker save_and_report_finding ruby_finding, new_preflight end def save_and_report_finding ruby_finding, new_preflight Contrast::Agent::Reporting::ReportingStorage[hash] = ruby_finding Contrast::Agent.reporter&.send_event_immediately(new_preflight) end end end end end end end