# Copyright (c) 2023 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 # @return [Boolean] 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) build_and_report(mod, name) rescue StandardError => e logger.error('Unable to parse AST for Hardcoded Rule analysis.', e) 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 # @param mod [Module] the module to which this AST pertains # @param name [String] the name of the hardcoded constant def build_and_report mod, name finding = build_finding(mod, name) return unless finding preflight = Contrast::Agent::Reporting::BuildPreflight.generate(finding) return unless preflight Contrast::Agent::Reporting::ReportingStorage[preflight.messages[0].data] = finding Contrast::Agent.reporter&.send_event(preflight) end # @param clazz [Class] the Class or Module containing the constant # @param constant_string [String] # @return [Contrast::Agent::Reporting::Finding] def build_finding clazz, constant_string finding = Contrast::Agent::Reporting::Finding.new(rule_id) finding.properties[SOURCE_KEY] = clazz.cs__name finding.properties[CONSTANT_NAME_KEY] = constant_string finding.properties[CODE_SOURCE_KEY] = constant_string + redacted_marker finding.hash_code = Contrast::Utils::HashDigest.generate_class_scanning_hash(finding) finding end end end end end end end