# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'contrast/components/interface' cs__scoped_require 'contrast/extension/module' 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 four methods # 1) name_passes? : does the constant name match a given value set # 2) value_type_passes? : does the type of the value of the constant match the # type given # 3) value_passes? : does the value of the constant match a given value set # 4) redacted_marker : the value to plug in for the obfuscated value module HardcodedValueRule include Contrast::Components::Interface access_component :analysis, :app_context, :logging, :settings def disabled? !ASSESS.enabled? || ASSESS.rule_disabled?(rule_id) end COMMON_CONSTANTS = %i[ CONTRAST_ASSESS_POLICY_STATUS VERSION ].cs__freeze def analyze clazz return if disabled? # we only want the constants explicitly defined in this class, not # those of its ancestor(s) constants = clazz.cs__constants(false) # if there are no constants, let's just leave return unless constants&.any? constants.each do |constant| next if COMMON_CONSTANTS.include?(constant) # if this class autoloads its constant, get the hell away from it # I mean it! Don't even think about it. # # Autoload means this constant (usually [always?] a class or # module) won't be required until something in the application # tries to load it. We CANNOT be that thing. We'll just have to # wait until it's loaded, at which point we'll be handed it # again. next if clazz.cs__autoload?(constant) # constant comes to us as a symbol. that sucks. we need to do # some string methods on it, so stringify it. constant_string = constant.to_s # if this is another class or a module, move on next unless constant_name?(constant_string) next unless name_passes?(constant_string) value = clazz.cs__const_get(constant, false) # if the constant isn't holding a string, skip it next unless value_type_passes?(value) # if it looks like a placeholder / pointer to a config, skip it next unless value_passes?(value) report_finding(clazz, constant_string) end end # Constants can be variable or classes defined in the given # class. We ONLY want the variables, which should be defined in # the MACRO_CASE (upper case & underscore format) CONSTANT_NAME_PATTERN = /^[A-Z_]+$/.cs__freeze def constant_name? constant constant.match?(CONSTANT_NAME_PATTERN) 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 def report_finding clazz, constant_string class_name = clazz.cs__name finding = Contrast::Api::Dtm::Finding.new finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(rule_id) finding.session_id = SETTINGS.session_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) activity = Contrast::Api::Dtm::Activity.new activity.findings << finding # If assess is enabled, we can just send the activity if APP_CONTEXT.ready? build_tags(activity) Contrast::Utils::ServiceSenderUtil.push_to_ready_queue activity # Otherwise, if the Agent isn't ready, we have to queue the messages # until we know the starting state. else Contrast::Utils::ServiceSenderUtil.add_to_assess_messages activity end rescue StandardError => e logger.error('Unable to build a finding for Hardcoded Rule', e) end # This seems silly to pull out, but we can ONLY call this in the case # where we have a configuration. Doing otherwise results in a bad error # case where we try to do other things, like logging, which behave # strangely without a config def build_tags activity activity.finding_tags = Contrast::Utils::StringUtils.force_utf8(ASSESS.tags) end end end end end end end