# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'rack' require 'json' require 'contrast/agent/reporting/reporting_utilities/dtm_message' require 'contrast/agent/reporting/reporting_utilities/build_preflight' require 'contrast/utils/hash_digest' require 'contrast/utils/preflight_util' require 'contrast/utils/string_utils' module Contrast module Agent module Assess module Rule module Response # These rules check the content of the HTTP Response to determine if something was set incorrectly or # insecurely in it. class BaseRule DATA = 'data'.cs__freeze # Analyze a given application response to determine if it violates the rule # # TODO: RUBY-999999 either extract the response's body or memoize it to some degree so that it's not # generated on every call of this method # @param response [Contrast::Agent::Response] the response of the application def analyze response return unless analyze_response?(response) violation = violated?(response) return unless violation finding = build_finding(violation) return unless finding if Contrast::Agent::Reporter.enabled? report = Contrast::Agent::Reporting::DtmMessage.dtm_to_event(finding) if report.is_a?(Contrast::Agent::Reporting::Finding) request = Contrast::Agent::REQUEST_TRACKER.current&.request preflight = Contrast::Agent::Reporting::BuildPreflight.build(report, request) Contrast::Agent.reporter&.send_event(preflight) else Contrast::Agent.reporter&.send_event(report) end else Contrast::Agent::REQUEST_TRACKER.current.activity.findings << finding end end protected # Rules discern which responses they can/should analyze. # # @param response [Contrast::Agent::Response] the response of the application def analyze_response? response return false unless response return false if disabled? return false unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_response_assess? return false unless valid_response_code?(response.response_code) return false unless valid_content_type?(response.content_type) true end # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so. # # @param _response [Contrast::Agent::Response] the response of the application # @return [Hash, nil] the evidence required to prove the violation of the rule def violated? _response; end def evidence data = Contrast::Utils::ObjectShare::EMPTY_STRING data = Contrast::Utils::ObjectShare::EMPTY_STRING if data.nil? { DATA => data } end # Convert the given evidence into a finding. The rule will populate this evidence with each of the # # @param evidence [Hash] the properties required to build this finding. # @return [Contrast::Api::Dtm::Finding] def build_finding evidence finding = Contrast::Api::Dtm::Finding.new finding.rule_id = rule_id context = Contrast::Agent::REQUEST_TRACKER.current finding.routes << context.route if context&.route build_evidence(evidence, finding) hash = Contrast::Utils::HashDigest.generate_config_hash(finding) finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash) finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding) finding end # This method allows to change the evidence we attach and the way we attach it # Change it accordingly the rule you work on # # @param evidence [Hash] the properties required to build this finding. # @param finding [Contrast::Api::Dtm::Finding] finding to attach the evidence to def build_evidence evidence, finding evidence.each_pair do |key, value| finding.properties[key] = if value.cs__is_a?(Hash) Contrast::Utils::StringUtils.protobuf_format(value.to_json) elsif value.cs__is_a?(Array) value.map { Contrast::Utils::StringUtils.protobuf_format(_1) }.to_s else Contrast::Utils::StringUtils.protobuf_format(value) end end end # A rule is disabled if assess is off or it is turned off by TeamServer or by configuration. # # @return [Boolean] def disabled? !::Contrast::ASSESS.enabled? || ::Contrast::ASSESS.rule_disabled?(rule_id) end # Rules discern which response codes to which they apply. If the response is of one that the rule should not # examine, it can short circuit early. If the code is unknown, it must be examined. # # @param code [Integer,nil] the response code # @return [Boolean] def valid_response_code? code !code || [301, 302, 307, 404, 410, 500].none?(code) end # Rules discern which Content-Type to which they apply. If the response is of one that the rule should not # examine, it can short circuit early. If the type is unknown, it must be examined. # # @param type [String,nil] the value of the Content-Type header # @return [Boolean] def valid_content_type? type !type || [/json/i, /xml/i].none? { |invalid_content| type =~ invalid_content } end end end end end end end