# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/agent/inventory/gemfile_digest_cache' require 'contrast/agent/inventory/dependencies' require 'contrast/components/interface' require 'contrast/utils/object_share' module Contrast module Agent module Inventory # Used to analyze class usage for reporting class DependencyUsageAnalysis include Singleton include Contrast::Components::Interface include Contrast::Agent::Inventory::Dependencies access_component :analysis, :config, :logging def initialize return unless enabled? @gemdigest_cache = Contrast::Agent::Inventory::GemfileDigestCache.new end # This method is invoked once, along with the rest of our catchup code # to report libraries and their associated files that have already been loaded pre-contrast def catchup return unless enabled? loaded_specs.each do |_name, spec| # Get a digest of the Gem file itself next unless (digest = Contrast::Utils::Sha256Builder.instance.build_from_spec(spec)) @gemdigest_cache.use_cache(digest) do |existing_files| loaded_files_from_gem = $LOADED_FEATURES.select { |f| f.start_with?(spec.full_gem_path) } loaded_files_from_gem.each do |file_path| logger.trace('Recording loaded file for inventory analysis', line: file_path) existing_files << adjust_path_for_reporting(file_path, spec) end end end end # This method is invoked once per TracePoint :end - to map a specific # file being required to the gem it belongs to # # @param path [String] the result of TracePoint#path from the :end # event in which the Module was defined def associate_file path return unless enabled? spec_lookup_path = adjust_path_for_spec_lookup(path) spec = Gem::Specification.find_by_path(spec_lookup_path) unless spec logger.debug('Unable to resolve gem spec for path', path: path) return end digest = Contrast::Utils::Sha256Builder.instance.build_from_spec(spec) unless digest logger.debug('Unable to resolve digest for gem spec', spec: spec.to_s) return end report_path = adjust_path_for_reporting(path, spec) @gemdigest_cache.get(digest) << report_path rescue StandardError => e logger.error('Unable to inventory file path', e, path: path) end # Populate the library_usages filed of the Activity message using the # data stored in the @gemdigest_cache # # @param activity [Contrast::Api::Dtm::Activity] the message to which # to append the usage data def generate_library_usage activity return unless enabled? return if @gemdigest_cache.empty? @gemdigest_cache.generate_usage_data(activity) end private def adjust_path_for_spec_lookup path idx = path.index('/lib/') path = path[(idx + 4)..-1] if idx path end def adjust_path_for_reporting path, gem_spec path.delete_prefix(gem_spec.full_gem_path) end # We only use this if inventory and library analysis are enabled def enabled? @_enabled = INVENTORY.enabled? && INVENTORY.analyze_libraries? if @_enabled.nil? @_enabled end end end end end