# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'singleton' cs__scoped_require 'contrast/components/interface' module Contrast module Utils module Assess # SamplingUtil has methods for tracking the number of requests per time window class SamplingUtil include Singleton include Contrast::Components::Interface access_component :sampling def initialize @requests = {} end def update self.class.reset_sampling_control @enabled = self.class.sampling_enabled? @baseline = self.class.sampling_control[:baseline] @request_frequency = self.class.sampling_control[:request_frequency] @response_frequency = self.class.sampling_control[:response_frequency] @window_ms = self.class.sampling_control[:window] true end def sample? request history = request_history(request) history.hit # if sampling isn't enabled, we record all requests and take a # default amount of responses return [true, true] unless @enabled # if we've exceeded this sample window, reset it if history.elapsed >= @window_ms history.reset_window return [true, true] end # we have to take a baseline of this request/ response combo. we only # baseline the first response, but we'll do full request analysis up # to the baseline amount return [true, history.window_hit == 1] if history.window_hit < @baseline # Once the window_hit exceeds the baseline, we limit the analysis # based on @request_frequency and @response_frequency. non_baseline = history.window_hit - @baseline analyze_request = (non_baseline % @request_frequency).zero? analyze_response = (non_baseline % @response_frequency).zero? [analyze_request, analyze_response] end private # RequestHistory tracks one requests history per time window class RequestHistory attr_reader :window_start, :window_hit def initialize @window_start = Contrast::Utils::Timer.now_ms @window_hit = 0 end def elapsed Contrast::Utils::Timer.now_ms - window_start end def hit cnt = 1 @window_hit += cnt end def reset_window @window_start = Contrast::Utils::Timer.now_ms @window_hit = 0 end end def request_history request @requests[request.hash_id] ||= RequestHistory.new end end end end end