# frozen_string_literal: true require 'socket' require 'json' require 'dotpath' require 'cased/version' require 'cased/config' require 'cased/context' require 'cased/model' require 'cased/error' require 'cased/clients' require 'cased/policy' require 'cased/rack_middleware' require 'cased/sensitive' require 'cased/publishers' require 'cased/test_helper' require 'cased/instrumentation/log_subscriber' require 'cased/cli' # Integrations begin require 'cased/integrations/sidekiq' rescue LoadError # rubocop:disable Lint/SuppressedException # Sidekiq is not installed in host application end module Cased def self.sensitive(label, handler) Cased::Sensitive::Handler.register(label, handler) end # # @example # Cased.policies[:organization] # # @return [Hash{Symbol => Cased::Policy, nil}] def self.policies @policies ||= Hash.new do |hash, name| key = name.to_sym api_key = Cased.config.policy_key(key) hash[key] = Policy.new(api_key: api_key) end end # Helper method for accessing the applications default policy. # # @example # Cased.configure do |config| # config.policy_key = 'policy_test_1dQpY5JliYgHSkEntAbMVzuOROh' # end # # policy = Cased.policy # policy.events.each do |event| # puts event['action'] # => user.login # end # # @return [Cased::Policy, nil] def self.policy policies[:default] end # @return [Cased::Config] def self.config @config ||= Cased::Config.new end # @example # Cased.configure do |config| # config.policy_key = 'policy_test_1dQpY5JliYgHSkEntAbMVzuOROh' # end # # @return [void] def self.configure(&block) block.call(config) end class << self attr_writer :publishers end # The list of publishers that will receive the processed audit event when calling Cased.publish. # # The desired behavior for Cased.publish should not change based on the order # of the publishers. # # @example Adding a publisher to the stack # Cased.publishers << Cased::Publishers::KafkaPublisher.new # # @example Setting the list of publishers # Cased.publishers = [ # Cased::Publishers::KafkaPublisher.new, # ] # # @return [Array] def self.publishers @publishers ||= [ Cased::Publishers::HTTPPublisher.new, Cased::Publishers::ActiveSupportPublisher.new, ] end def self.clients @clients ||= Cased::Clients.new end # @param audit_event [Hash] the audit event. # # @example # Cased.publish( # action: "user.login", # actor: "garrett@cased.com", # actor_id: "user_1dQpY5JliYgHSkEntAbMVzuOROh", # http_user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36", # http_url: "https://app.cased.com/", # http_method: "GET", # language: "en-US" # ) # # @example With User object that includes Cased::Model # Cased.publish( # action: "user.login", # actor: User.find(1), # ) # # @return [Array] of responses from Cased.publishers # @return [false] if Cased has been silenced. # @raise [ArgumentError] if a publisher does not implement the #publish method def self.publish(audit_event) return false if config.silence? processed_audit_event = process(audit_event) publishers.each do |publisher| unless publisher.respond_to?(:publish) raise ArgumentError, "#{publisher.class} must implement #{publisher.class}#publish" end publisher.publish(processed_audit_event.dup) rescue StandardError => e handle_exception(e) end end # @return [Cased::Context] def self.context Context.current end # The main entry point to handling any exceptions encountered in the event creation lifecycle. # # @param exception [Exception] the exception to be raised # # @return [void] # @raise [Exception] if Cased is configured to raise on errors def self.handle_exception(exception) raise exception if config.raise_on_errors? if exception_handler.nil? warn exception.message return end exception_handler.call(exception) end # Applications can determine where they want exceptions generated by Cased to be sent. # # @return [Proc, nil] def self.exception_handler @exception_handler end # Sets the system user to be used for Cased events that do not contain an actor. # # @param handler [Proc] - The Proc or lambda that takes a single exception argument. # # @example # Cased.exception_handler = Proc.new do |exception| # Raven.capture_exception(exception) # end # # @return [void] # @raise [ArgumentError] if the provided handler does not respond to call or # accept an argument. def self.exception_handler=(handler) if handler.nil? @exception_handler = nil return elsif !handler.respond_to?(:call) @exception_handler = nil raise ArgumentError, "#{handler.class} does not respond to #call" elsif handler.arity != 1 raise ArgumentError, 'handler does not accept any arguments' end @exception_handler = handler end # Configures a default context for console sessions. # # When a console session is started you don't have the context generated from # a typical web request lifecycle where authentication will happen and an IP # address is present. This uses the server's hostname as a standard location # for migrations or data transitions. # # @param options [Hash] # # @return [void] def self.console(options = {}) context.merge({ location: Socket.gethostname, }.merge(options)) end # Generates Cased compatible resource identifier. # # @param model [Object] an object that responds to #cased_id # # @return [String] the Cased::Model#cased_id # @raise [Cased::Error::MissingIdentifier] when the provided model does not # respond to #cased_id def self.id(model) raise Cased::Error::MissingIdentifier unless model.respond_to?(:cased_id) model.cased_id end # Don't send any events to Cased that are created within the lifecycle of the block. # # @example # Cased.silence do # user.save # end def self.silence original_silence = config.silence? config.silence = true yield ensure config.silence = original_silence end # Publish the Cased event so all subscribers can handle storing the Cased event. # # @param payload [Hash] payload to publish. # # @return [Hash] the processed audit event payload. private_class_method def self.process(payload) expanded_audit_event = Cased::Context::Expander.expand(payload) event = expanded_audit_event.merge( cased_id: SecureRandom.hex, timestamp: Time.now.utc.iso8601(6), ) safe_context = context.context.dup audit_event = safe_context.merge(event) Cased::Sensitive::Processor.process!(audit_event) audit_event end end