# 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/utils/object_share' cs__scoped_require 'contrast/api' module Contrast module Utils # Utilities for converting ruby stack trace into DTMs class StackTraceUtils # need lib here to separate from specs AGENT_CLASS_MARKER = '/lib/contrast/' class << self # Determine if this method is being invoked by application code or not # based on the immediate caller of the code after Contrast. # # @return [Boolean] true if this code is called with application # (non-gem) code in the caller def custom_code_context? root_dir = Contrast::Agent.framework_manager.app_root stack = Kernel.caller(0, 15) i = 0 while i < stack.length stack_element = stack[i] i += 1 next if stack_element.include?('/contrast/') return stack_element.start_with?(root_dir) end end # Call and translate a caller_locations array to an array of # StackTraceElement for TeamServer to display, excluding any Contrast # code found. # # @return [Array<Contrast::Api::Dtm::StackTraceElement] def build_protect_stack_array stack = caller(2, 20) return [] unless stack stack = reject_caller_entries(stack) i = 0 stack.map! do |entry| element = Contrast::Api::Dtm::StackTraceElement.new element = fill_protect_element(element, entry, i) i += 1 element end stack.compact! stack end # Translate a caller array to an array of TraceStacks for TeamServer to # display, excluding any Contrast code found. # # @param stack [Array<String>] the output of Kernel.caller # @return [Array<Contrast::Api::Dtm::TraceStack] def build_assess_stack_array stack converted = [] return converted unless stack i = 0 while i < stack.length caller_location = stack[i] i += 1 next if caller_location.include?(AGENT_CLASS_MARKER) # To play nice with the way that TeamServer is rendering these # values, we only populate the file_name field with exactly what we # want them to display element = Contrast::Api::Dtm::TraceStack.new element.file_name = caller_location converted << element end converted end private def reject_caller_entries stack stack.reject do |entry| entry = entry.to_s entry.include?(AGENT_CLASS_MARKER) end end # In Contrast UI - there are many ways to render the stacktraces. For # Protect, there are two ways we're concerned about, one for the first # element and then all others. # 1 for protect first frame - We need the method specifically for this rendering # 1 for protect Nth frames def fill_protect_element element, caller_location, index return element unless caller_location if index.zero? element.method_name = caller_location.split('`')[-1].delete_suffix!('\'') element.file_name = caller_location.split(':')[0] else element.file_name = caller_location end element rescue StandardError # NOOP nil end end end end end