# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/utils/os' require 'digest' require 'socket' module Contrast module Agent # Tools for supporting the Telemetry feature module Telemetry # Gets info about the instrumented application required to build unique identifiers, # used in the agent's Telemetry. module Identifier MAC_REGEXP = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.cs__freeze LINUX_OS_REG = /hwaddr=.*?(([A-F0-9]{2}:){5}[A-F0-9]{2})/im.cs__freeze MAC_OS_PRIMARY = 'en0'.cs__freeze LINUX_PRIMARY = 'enp'.cs__freeze # Sinatra and Grape both use similar approach to identify the app_name. # Rails has a different way of doing it, but to unify this we'll use this one. # If app_name is changed/renamed during production it would still get the # new folder's name. # # @ return [String] name of the application from the current working directory def self.app_name @_app_name ||= File.basename(Dir.pwd) end # Returns the MAC address of the primary network interface, depending on the used OS. # If the primary is unknown it finds the first available network interface and gets it's # MAC address instead. # # @return [String, nil] MAC address of the primary network interface or # the first available one, or nil if nothing found def self.mac primary = Contrast::Utils::OS.mac? ? MAC_OS_PRIMARY : LINUX_PRIMARY @_mac = find_mac(primary) || find_mac end # Set and return a Sha256 hash representing this application, based on the mac identifier of this machine and # the application name or a Secure UUID if one of them cannot be determined. # # @return [String] def self.application_id @_application_id ||= begin id = nil mac = Contrast::Agent::Telemetry::Identifier.mac app_name = Contrast::Agent::Telemetry::Identifier.app_name id = mac + app_name if mac && app_name Digest::SHA2.new(256).hexdigest(id || "_#{ SecureRandom.uuid }") end end # Set and return a Sha256 hash representing this agent run, based on the mac identifier of this machine or a # Secure UUID if one cannot be determined. # # @return [String] def self.instance_id @_instance_id ||= Digest::SHA2.new(256).hexdigest(Contrast::Agent::Telemetry::Identifier.mac || "_#{ SecureRandom.uuid }") end class << self private # Finds the primary MAC address of all listed network adapters. # If primary is not set or unknown, use the first MAC address found # from the listed adapters. # # @param primary [nil, String] optional param if set look only for primary # network adapter's name # @return [String, nil] MAC address of the first listed network adapter or # nil if not found def find_mac primary = nil result = nil idx = 0 return if interfaces.empty? while idx < interfaces.length addr = interfaces[idx].addr name = interfaces[idx].name # rubocop:disable Security/Module/Name idx += 1 next if primary && !name.include?(primary) # retrieving MAC address from primary network interface or first available mac = retrieve_mac(addr) next unless mac result = mac if mac.match?(MAC_REGEXP) break if result end result end # Retrieves MAC address for primary or any network interface. # This is OS dependent search. # # @param addr [String] address info # example: # # @return mac [nil, String] MAC address of primary network interface, # any network interface, or nil if no interface is found. def retrieve_mac addr # Mac OS allow us to use getnameinfo(sockaddr [, flags]) => [hostname, servicename] # # returned address: # return addr.getnameinfo[0] if Contrast::Utils::OS.mac? # In Linux using Socket::addr#getnameinfo results in ai_family not supported exception. # In this case we are relying on match filtering of addresses. # # returned address: # # return Regexp.last_match(1) if addr.inspect =~ LINUX_OS_REG nil end # Returns array of network interfaces belonging to the expected pfamily of this OS. # # @return interfaces [Array] def interfaces @_interfaces ||= Socket.getifaddrs.select { |interface| interface.addr&.pfamily == check_family } end # We need only network adapters MACs. Checking for pfamily of every socket address: # 18 for Mac OS and 17 for Linux. Family should be an address family such as: :INET, :INET6, :UNIX, etc. # It corresponds to the Addrinfo.pfamily value. # # @return [Integer] def check_family @_check_family ||= Contrast::Utils::OS.mac? ? 18 : 17 end end end end end end