# Copyright (c) 2023 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/build_preflight' require 'contrast/utils/hash_digest' 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 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 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::Agent::Reporting::Finding] def build_finding evidence finding = Contrast::Agent::Reporting::Finding.new(rule_id) context = Contrast::Agent::REQUEST_TRACKER.current finding.routes << context.discovered_route if context&.discovered_route build_evidence(evidence, finding) finding.request = Contrast::Agent::Reporting::FindingRequest.convert(context.request) if context&.request # Hash must be built last so that finding has full context. hash = Contrast::Utils::HashDigest.generate_response_hash(finding, context&.request) finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash) 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::Agent::Reporting::Finding] finding to attach the evidence to def build_evidence evidence, finding evidence.each_pair do |key, value| finding.properties[key] = value 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