diff --git a/.circleci/config.yml b/.circleci/config.yml index efa1999ac47..d92d81d456d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -138,10 +138,6 @@ step_rubocop: &step_rubocop # The workaround is to use `cpu.shares / 1024`: # https://discuss.circleci.com/t/environment-variable-set-to-the-number-of-available-cpus/32670/4 command: PARALLEL_PROCESSOR_COUNT=$((`cat /sys/fs/cgroup/cpu/cpu.shares` / 1024)) bundle exec rake rubocop -step_sorbet_type_checker: &step_sorbet_type_checker - run: - name: Run sorbet type checker - command: bundle exec rake typecheck step_appraisal_install: &step_appraisal_install run: name: Install Appraisal gems @@ -348,16 +344,6 @@ orbs: keys: - bundle-{{ .Environment.CIRCLE_CACHE_VERSION }}-{{ checksum ".circleci/images/primary/binary_version" }}-<>-{{ checksum "lib/ddtrace/version.rb" }}-{{ .Branch }}-{{ checksum ".circleci/bundle_checksum" }} - *step_rubocop - sorbet_type_checker: - <<: *test_job_default - steps: - - restore_cache: - keys: - - '{{ .Environment.CIRCLE_CACHE_VERSION }}-bundled-repo-<>-{{ .Environment.CIRCLE_SHA1 }}' - - restore_cache: - keys: - - bundle-{{ .Environment.CIRCLE_CACHE_VERSION }}-{{ checksum ".circleci/images/primary/binary_version" }}-<>-{{ checksum "lib/ddtrace/version.rb" }}-{{ .Branch }}-{{ checksum ".circleci/bundle_checksum" }} - - *step_sorbet_type_checker coverage: <<: *test_job_default steps: @@ -564,11 +550,6 @@ workflows: name: lint requires: - build-2.7 - - orb/sorbet_type_checker: - <<: *config-2_7-small - name: sorbet_type_checker - requires: - - build-2.7 - orb/coverage: <<: *config-2_7-small name: coverage @@ -766,7 +747,6 @@ workflows: <<: *filters_all_branches_and_tags requires: - lint - - sorbet_type_checker - test-2.1 - test-2.2 - test-2.3 @@ -786,7 +766,6 @@ workflows: <<: *filters_only_release_tags requires: - lint - - sorbet_type_checker - test-2.1 - test-2.2 - test-2.3 diff --git a/Steepfile b/Steepfile index e6613ec5db6..98981b55e77 100644 --- a/Steepfile +++ b/Steepfile @@ -7,6 +7,8 @@ target :appsec do # check 'lib/datadog/kit' ignore 'lib/datadog/appsec/contrib' + ignore 'lib/datadog/appsec/monitor' + ignore 'lib/datadog/appsec/component.rb' library 'pathname', 'set' library 'cgi' diff --git a/lib/datadog/appsec/contrib/rack/gateway/watcher.rb b/lib/datadog/appsec/contrib/rack/gateway/watcher.rb index 14765e7357b..0c7ecab7c60 100644 --- a/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +++ b/lib/datadog/appsec/contrib/rack/gateway/watcher.rb @@ -14,140 +14,144 @@ module Rack module Gateway # Watcher for Rack gateway events module Watcher - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def self.watch - Instrumentation.gateway.watch('rack.request', :appsec) do |stack, request| - block = false - event = nil - waf_context = request.env['datadog.waf.context'] - - AppSec::Reactive::Operation.new('rack.request') do |op| - trace = active_trace - span = active_span - - Rack::Reactive::Request.subscribe(op, waf_context) do |result, _block| - if result.status == :match - # TODO: should this hash be an Event instance instead? - event = { - waf_result: result, - trace: trace, - span: span, - request: request, - actions: result.actions - } - - span.set_tag('appsec.event', 'true') if span - - waf_context.events << event + class << self + def watch + gateway = Instrumentation.gateway + + watch_request(gateway) + watch_response(gateway) + watch_request_body(gateway) + end + + def watch_request(gateway = Instrumentation.gateway) + gateway.watch('rack.request', :appsec) do |stack, request| + block = false + event = nil + waf_context = request.env['datadog.waf.context'] + + AppSec::Reactive::Operation.new('rack.request') do |op| + trace = active_trace + span = active_span + + Rack::Reactive::Request.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + request: request, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end end + + _result, block = Rack::Reactive::Request.publish(op, request) end - _result, block = Rack::Reactive::Request.publish(op, request) - end + next [nil, [[:block, event]]] if block - next [nil, [[:block, event]]] if block + ret, res = stack.call(request) - ret, res = stack.call(request) + if event + res ||= [] + res << [:monitor, event] + end - if event - res ||= [] - res << [:monitor, event] + [ret, res] end - - [ret, res] end - Instrumentation.gateway.watch('rack.response', :appsec) do |stack, response| - block = false - event = nil - waf_context = response.instance_eval { @waf_context } - - AppSec::Reactive::Operation.new('rack.response') do |op| - trace = active_trace - span = active_span - - Rack::Reactive::Response.subscribe(op, waf_context) do |result, _block| - if result.status == :match - # TODO: should this hash be an Event instance instead? - event = { - waf_result: result, - trace: trace, - span: span, - response: response, - actions: result.actions - } - - span.set_tag('appsec.event', 'true') if span - - waf_context.events << event + def watch_response(gateway = Instrumentation.gateway) + gateway.watch('rack.response', :appsec) do |stack, response| + block = false + event = nil + waf_context = response.instance_eval { @waf_context } + + AppSec::Reactive::Operation.new('rack.response') do |op| + trace = active_trace + span = active_span + + Rack::Reactive::Response.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + response: response, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end end + + _result, block = Rack::Reactive::Response.publish(op, response) end - _result, block = Rack::Reactive::Response.publish(op, response) - end + next [nil, [[:block, event]]] if block - next [nil, [[:block, event]]] if block + ret, res = stack.call(response) - ret, res = stack.call(response) + if event + res ||= [] + res << [:monitor, event] + end - if event - res ||= [] - res << [:monitor, event] + [ret, res] end - - [ret, res] end - Instrumentation.gateway.watch('rack.request.body', :appsec) do |stack, request| - block = false - event = nil - waf_context = request.env['datadog.waf.context'] - - AppSec::Reactive::Operation.new('rack.request.body') do |op| - trace = active_trace - span = active_span - - Rack::Reactive::RequestBody.subscribe(op, waf_context) do |result, _block| - if result.status == :match - # TODO: should this hash be an Event instance instead? - event = { - waf_result: result, - trace: trace, - span: span, - request: request, - actions: result.actions - } - - span.set_tag('appsec.event', 'true') if span - - waf_context.events << event + def watch_request_body(gateway = Instrumentation.gateway) + gateway.watch('rack.request.body', :appsec) do |stack, request| + block = false + event = nil + waf_context = request.env['datadog.waf.context'] + + AppSec::Reactive::Operation.new('rack.request.body') do |op| + trace = active_trace + span = active_span + + Rack::Reactive::RequestBody.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + request: request, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end end + + _result, block = Rack::Reactive::RequestBody.publish(op, request) end - _result, block = Rack::Reactive::RequestBody.publish(op, request) - end + next [nil, [[:block, event]]] if block - next [nil, [[:block, event]]] if block + ret, res = stack.call(request) - ret, res = stack.call(request) + if event + res ||= [] + res << [:monitor, event] + end - if event - res ||= [] - res << [:monitor, event] + [ret, res] end - - [ret, res] end - end - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/MethodLength - # rubocop:enable Metrics/AbcSize - class << self private def active_trace diff --git a/lib/datadog/appsec/contrib/rack/patcher.rb b/lib/datadog/appsec/contrib/rack/patcher.rb index 6797385d27c..d439d53973b 100644 --- a/lib/datadog/appsec/contrib/rack/patcher.rb +++ b/lib/datadog/appsec/contrib/rack/patcher.rb @@ -1,6 +1,7 @@ # typed: ignore require_relative '../patcher' +require_relative '../../monitor' require_relative 'gateway/watcher' module Datadog @@ -22,6 +23,7 @@ def target_version end def patch + Monitor::Gateway::Watcher.watch Gateway::Watcher.watch Patcher.instance_variable_set(:@patched, true) end diff --git a/lib/datadog/appsec/contrib/rack/request_middleware.rb b/lib/datadog/appsec/contrib/rack/request_middleware.rb index 7d200378bc4..e49c9cd48b6 100644 --- a/lib/datadog/appsec/contrib/rack/request_middleware.rb +++ b/lib/datadog/appsec/contrib/rack/request_middleware.rb @@ -31,7 +31,7 @@ def call(env) # TODO: handle exceptions, except for @app.call - context = processor.new_context + context = processor.activate_context env['datadog.waf.context'] = context request = ::Rack::Request.new(env) @@ -68,7 +68,7 @@ def call(env) ensure if context add_waf_runtime_tags(active_trace, context) - context.finalize + processor.deactivate_context end end diff --git a/lib/datadog/appsec/contrib/rails/gateway/watcher.rb b/lib/datadog/appsec/contrib/rails/gateway/watcher.rb index a43e8709e80..7248a70bb58 100644 --- a/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +++ b/lib/datadog/appsec/contrib/rails/gateway/watcher.rb @@ -12,50 +12,56 @@ module Rails module Gateway # Watcher for Rails gateway events module Watcher - def self.watch - Instrumentation.gateway.watch('rails.request.action', :appsec) do |stack, request| - block = false - event = nil - waf_context = request.env['datadog.waf.context'] - - AppSec::Reactive::Operation.new('rails.request.action') do |op| - trace = active_trace - span = active_span - - Rails::Reactive::Action.subscribe(op, waf_context) do |result, _block| - if result.status == :match - # TODO: should this hash be an Event instance instead? - event = { - waf_result: result, - trace: trace, - span: span, - request: request, - actions: result.actions - } - - span.set_tag('appsec.event', 'true') if span - - waf_context.events << event + class << self + def watch + gateway = Instrumentation.gateway + + watch_request_action(gateway) + end + + def watch_request_action(gateway = Instrumentation.gateway) + gateway.watch('rails.request.action', :appsec) do |stack, request| + block = false + event = nil + waf_context = request.env['datadog.waf.context'] + + AppSec::Reactive::Operation.new('rails.request.action') do |op| + trace = active_trace + span = active_span + + Rails::Reactive::Action.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + request: request, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end end + + _result, block = Rails::Reactive::Action.publish(op, request) end - _result, block = Rails::Reactive::Action.publish(op, request) - end + next [nil, [[:block, event]]] if block - next [nil, [[:block, event]]] if block + ret, res = stack.call(request) - ret, res = stack.call(request) + if event + res ||= [] + res << [:monitor, event] + end - if event - res ||= [] - res << [:monitor, event] + [ret, res] end - - [ret, res] end - end - class << self private def active_trace diff --git a/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb b/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb index 87af97c8073..eb8266373f8 100644 --- a/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +++ b/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb @@ -13,93 +13,100 @@ module Sinatra module Gateway # Watcher for Sinatra gateway events module Watcher - # rubocop:disable Metrics/MethodLength - def self.watch - Instrumentation.gateway.watch('sinatra.request.dispatch', :appsec) do |stack, request| - block = false - event = nil - waf_context = request.env['datadog.waf.context'] - - AppSec::Reactive::Operation.new('sinatra.request.dispatch') do |op| - trace = active_trace - span = active_span - - Rack::Reactive::RequestBody.subscribe(op, waf_context) do |result, _block| - if result.status == :match - # TODO: should this hash be an Event instance instead? - event = { - waf_result: result, - trace: trace, - span: span, - request: request, - actions: result.actions - } - - span.set_tag('appsec.event', 'true') if span - - waf_context.events << event + class << self + def watch + gateway = Instrumentation.gateway + + watch_request_dispatch(gateway) + watch_request_routed(gateway) + end + + def watch_request_dispatch(gateway = Instrumentation.gateway) + gateway.watch('sinatra.request.dispatch', :appsec) do |stack, request| + block = false + event = nil + waf_context = request.env['datadog.waf.context'] + + AppSec::Reactive::Operation.new('sinatra.request.dispatch') do |op| + trace = active_trace + span = active_span + + Rack::Reactive::RequestBody.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + request: request, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end end + + _result, block = Rack::Reactive::RequestBody.publish(op, request) end - _result, block = Rack::Reactive::RequestBody.publish(op, request) - end + next [nil, [[:block, event]]] if block - next [nil, [[:block, event]]] if block + ret, res = stack.call(request) - ret, res = stack.call(request) + if event + res ||= [] + res << [:monitor, event] + end - if event - res ||= [] - res << [:monitor, event] + [ret, res] end - - [ret, res] end - Instrumentation.gateway.watch('sinatra.request.routed', :appsec) do |stack, (request, route_params)| - block = false - event = nil - waf_context = request.env['datadog.waf.context'] - - AppSec::Reactive::Operation.new('sinatra.request.routed') do |op| - trace = active_trace - span = active_span - - Sinatra::Reactive::Routed.subscribe(op, waf_context) do |result, _block| - if result.status == :match - # TODO: should this hash be an Event instance instead? - event = { - waf_result: result, - trace: trace, - span: span, - request: request, - actions: result.actions - } - - span.set_tag('appsec.event', 'true') if span - - waf_context.events << event + def watch_request_routed(gateway = Instrumentation.gateway) + gateway.watch('sinatra.request.routed', :appsec) do |stack, (request, route_params)| + block = false + event = nil + waf_context = request.env['datadog.waf.context'] + + AppSec::Reactive::Operation.new('sinatra.request.routed') do |op| + trace = active_trace + span = active_span + + Sinatra::Reactive::Routed.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + request: request, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end end + + _result, block = Sinatra::Reactive::Routed.publish(op, [request, route_params]) end - _result, block = Sinatra::Reactive::Routed.publish(op, [request, route_params]) - end + next [nil, [[:block, event]]] if block - next [nil, [[:block, event]]] if block + ret, res = stack.call(request) - ret, res = stack.call(request) + if event + res ||= [] + res << [:monitor, event] + end - if event - res ||= [] - res << [:monitor, event] + [ret, res] end - - [ret, res] end - end - # rubocop:enable Metrics/MethodLength - class << self private def active_trace diff --git a/lib/datadog/appsec/monitor.rb b/lib/datadog/appsec/monitor.rb new file mode 100644 index 00000000000..f02c188a77d --- /dev/null +++ b/lib/datadog/appsec/monitor.rb @@ -0,0 +1,12 @@ +# typed: false +# frozen_string_literal: true + +require_relative 'monitor/gateway/watcher' + +module Datadog + module AppSec + # Monitor for internal AppSec Events + module Monitor + end + end +end diff --git a/lib/datadog/appsec/monitor/gateway/watcher.rb b/lib/datadog/appsec/monitor/gateway/watcher.rb new file mode 100644 index 00000000000..9282ef21d27 --- /dev/null +++ b/lib/datadog/appsec/monitor/gateway/watcher.rb @@ -0,0 +1,86 @@ +# typed: false +# frozen_string_literal: true + +require_relative '../../instrumentation/gateway' +require_relative '../../reactive/operation' +require_relative '../reactive/set_user' + +module Datadog + module AppSec + module Monitor + module Gateway + # Watcher for Apssec internal events + module Watcher + class << self + def watch + gateway = Instrumentation.gateway + + watch_user_id(gateway) + end + + def watch_user_id(gateway = Instrumentation.gateway) + gateway.watch('identity.set_user', :appsec) do |stack, user| + block = false + event = nil + waf_context = Datadog::AppSec::Processor.active_context + + AppSec::Reactive::Operation.new('identity.set_user') do |op| + trace = active_trace + span = active_span + + Monitor::Reactive::SetUser.subscribe(op, waf_context) do |result, _block| + if result.status == :match + # TODO: should this hash be an Event instance instead? + event = { + waf_result: result, + trace: trace, + span: span, + user: user, + actions: result.actions + } + + span.set_tag('appsec.event', 'true') if span + + waf_context.events << event + end + end + + _result, block = Monitor::Reactive::SetUser.publish(op, user) + end + + next [nil, [[:block, event]]] if block + + ret, res = stack.call(user) + + if event + res ||= [] + res << [:monitor, event] + end + + [ret, res] + end + end + + private + + def active_trace + # TODO: factor out tracing availability detection + + return unless defined?(Datadog::Tracing) + + Datadog::Tracing.active_trace + end + + def active_span + # TODO: factor out tracing availability detection + + return unless defined?(Datadog::Tracing) + + Datadog::Tracing.active_span + end + end + end + end + end + end +end diff --git a/lib/datadog/appsec/monitor/reactive/set_user.rb b/lib/datadog/appsec/monitor/reactive/set_user.rb new file mode 100644 index 00000000000..50daa496c50 --- /dev/null +++ b/lib/datadog/appsec/monitor/reactive/set_user.rb @@ -0,0 +1,62 @@ +# typed: false +# frozen_string_literal: true + +module Datadog + module AppSec + module Monitor + module Reactive + # Dispatch data from Datadog::Kit::Identity.set_user to the WAF context + module SetUser + ADDRESSES = [ + 'usr.id', + ].freeze + private_constant :ADDRESSES + + def self.publish(op, user) + catch(:block) do + op.publish('usr.id', user.id) + + nil + end + end + + def self.subscribe(op, waf_context) + op.subscribe(*ADDRESSES) do |*values| + Datadog.logger.debug { "reacted to #{ADDRESSES.inspect}: #{values.inspect}" } + + user_id = values[0] + + waf_args = { + 'usr.id' => user_id, + } + + waf_timeout = Datadog::AppSec.settings.waf_timeout + result = waf_context.run(waf_args, waf_timeout) + + Datadog.logger.debug { "WAF TIMEOUT: #{result.inspect}" } if result.timeout + + case result.status + when :match + Datadog.logger.debug { "WAF: #{result.inspect}" } + + block = result.actions.include?('block') + + yield [result, block] + + throw(:block, [result, true]) if block + when :ok + Datadog.logger.debug { "WAF OK: #{result.inspect}" } + when :invalid_call + Datadog.logger.debug { "WAF CALL ERROR: #{result.inspect}" } + when :invalid_rule, :invalid_flow, :no_rule + Datadog.logger.debug { "WAF RULE ERROR: #{result.inspect}" } + else + Datadog.logger.debug { "WAF UNKNOWN: #{result.status.inspect} #{result.inspect}" } + end + end + end + end + end + end + end +end diff --git a/lib/datadog/appsec/processor.rb b/lib/datadog/appsec/processor.rb index b2051b07a86..1d868ca312c 100644 --- a/lib/datadog/appsec/processor.rb +++ b/lib/datadog/appsec/processor.rb @@ -39,6 +39,30 @@ def finalize end end + class << self + def active_context + Thread.current[:datadog_current_waf_context] + end + + private + + def active_context=(context) + unless context.instance_of?(Context) + raise ArgumentError, + "The context provide: #{context.inspect} is not a Datadog::AppSec::Processor::Context" + end + + Thread.current[:datadog_current_waf_context] = context + end + + def reset_active_context + Thread.current[:datadog_current_waf_context] = nil + end + end + + class NoActiveContextError < StandardError; end + class AlreadyActiveContextError < StandardError; end + attr_reader :ruleset_info, :addresses def initialize @@ -63,6 +87,23 @@ def new_context Context.new(self) end + def activate_context + existing_active_context = Processor.active_context + raise AlreadyActiveContextError if existing_active_context + + context = new_context + Processor.send(:active_context=, context) + context + end + + def deactivate_context + context = Processor.active_context + raise NoActiveContextError unless context + + Processor.send(:reset_active_context) + context.finalize + end + def update_rule_data(data) @handle.update_rule_data(data) end diff --git a/sig/datadog/appsec/contrib/rack/gateway/watcher.rbs b/sig/datadog/appsec/contrib/rack/gateway/watcher.rbs index 8c7e0a9c5ac..5a830812e3e 100644 --- a/sig/datadog/appsec/contrib/rack/gateway/watcher.rbs +++ b/sig/datadog/appsec/contrib/rack/gateway/watcher.rbs @@ -6,6 +6,12 @@ module Datadog module Watcher def self.watch: () -> untyped + def self.watch_request: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + + def self.watch_response: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + + def self.watch_request_body: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + private def self.active_trace: () -> (nil | untyped) diff --git a/sig/datadog/appsec/contrib/rails/gateway/watcher.rbs b/sig/datadog/appsec/contrib/rails/gateway/watcher.rbs index 38a1c6701b9..11c37538354 100644 --- a/sig/datadog/appsec/contrib/rails/gateway/watcher.rbs +++ b/sig/datadog/appsec/contrib/rails/gateway/watcher.rbs @@ -6,6 +6,8 @@ module Datadog module Watcher def self.watch: () -> untyped + def self.watch_request_action: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + private def self.active_trace: () -> (nil | untyped) diff --git a/sig/datadog/appsec/contrib/sinatra/gateway/watcher.rbs b/sig/datadog/appsec/contrib/sinatra/gateway/watcher.rbs index 164f9de6543..9441f63c7e6 100644 --- a/sig/datadog/appsec/contrib/sinatra/gateway/watcher.rbs +++ b/sig/datadog/appsec/contrib/sinatra/gateway/watcher.rbs @@ -6,6 +6,10 @@ module Datadog module Watcher def self.watch: () -> untyped + def self.watch_request_dispatch: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + + def self.watch_request_routed: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + private def self.active_trace: () -> (nil | untyped) diff --git a/sig/datadog/appsec/monitor/gateway/watcher.rbs b/sig/datadog/appsec/monitor/gateway/watcher.rbs new file mode 100644 index 00000000000..8a3f0937960 --- /dev/null +++ b/sig/datadog/appsec/monitor/gateway/watcher.rbs @@ -0,0 +1,19 @@ +module Datadog + module AppSec + module Monitor + module Gateway + module Watcher + def self.watch: () -> untyped + + def self.watch_user_id: (?Datadog::AppSec::Instrumentation::Gateway gateway) -> untyped + + private + + def self.active_trace: () -> (nil | untyped) + + def self.active_span: () -> (nil | untyped) + end + end + end + end +end diff --git a/sig/datadog/appsec/monitor/reactive/set_user.rbs b/sig/datadog/appsec/monitor/reactive/set_user.rbs new file mode 100644 index 00000000000..529d31fe2d2 --- /dev/null +++ b/sig/datadog/appsec/monitor/reactive/set_user.rbs @@ -0,0 +1,15 @@ +module Datadog + module AppSec + module Monitor + module Reactive + module SetUser + ADDRESSES: ::Array[::String] + + def self.publish: (untyped op, untyped user) -> untyped + + def self.subscribe: (untyped op, untyped waf_context) { (untyped) -> untyped } -> untyped + end + end + end + end +end diff --git a/sig/datadog/appsec/processor.rbs b/sig/datadog/appsec/processor.rbs index d226476b322..9b5a4537a4b 100644 --- a/sig/datadog/appsec/processor.rbs +++ b/sig/datadog/appsec/processor.rbs @@ -17,6 +17,19 @@ module Datadog def finalize: () -> void end + def self.active_context: () -> Context + + private + + def self.active_context=: (untyped context) -> untyped + def self.reset_active_context: () -> untyped + + class NoActiveContextError < StandardError + end + + class AlreadyActiveContextError < StandardError + end + attr_reader ruleset_info: untyped attr_reader addresses: untyped @@ -27,6 +40,8 @@ module Datadog def initialize: () -> void def ready?: () -> bool def new_context: () -> Context + def activate_context: () -> Context + def deactivate_context: () -> void def update_rule_data: (untyped data) -> untyped def toggle_rules: (untyped map) -> untyped def finalize: () -> void diff --git a/spec/datadog/appsec/monitor/reactive/set_user_spec.rb b/spec/datadog/appsec/monitor/reactive/set_user_spec.rb new file mode 100644 index 00000000000..11e5381fcb2 --- /dev/null +++ b/spec/datadog/appsec/monitor/reactive/set_user_spec.rb @@ -0,0 +1,149 @@ +# typed: ignore +# frozen_string_literal: true + +require 'datadog/appsec/spec_helper' +require 'datadog/appsec/reactive/operation' +require 'datadog/appsec/monitor/reactive/set_user' + +RSpec.describe Datadog::AppSec::Monitor::Reactive::SetUser do + let(:operation) { Datadog::AppSec::Reactive::Operation.new('test') } + let(:user) { double(:user, id: 1) } + + describe '.publish' do + it 'propagates request body attributes to the operation' do + expect(operation).to receive(:publish).with('usr.id', 1) + + described_class.publish(operation, user) + end + end + + describe '.subscribe' do + let(:waf_context) { double(:waf_context) } + + context 'not all addresses have been published' do + it 'does not call the waf context' do + expect(operation).to receive(:subscribe).with('usr.id').and_call_original + expect(waf_context).to_not receive(:run) + described_class.subscribe(operation, waf_context) + end + end + + context 'all addresses have been published' do + it 'does call the waf context with the right arguments' do + expect(operation).to receive(:subscribe).and_call_original + + expected_waf_arguments = { 'usr.id' => 1 } + + waf_result = double(:waf_result, status: :ok, timeout: false) + expect(waf_context).to receive(:run).with( + expected_waf_arguments, + Datadog::AppSec.settings.waf_timeout + ).and_return(waf_result) + described_class.subscribe(operation, waf_context) + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + + context 'waf result is a match' do + it 'yields result and no blocking action' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :match, timeout: false, actions: ['']) + expect(waf_context).to receive(:run).and_return(waf_result) + described_class.subscribe(operation, waf_context) do |result, block| + expect(result).to eq(waf_result) + expect(block).to eq(false) + end + result = described_class.publish(operation, user) + expect(result).to be_nil + end + + it 'yields result and blocking action. The publish method catches the resul as well' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :match, timeout: false, actions: ['block']) + expect(waf_context).to receive(:run).and_return(waf_result) + described_class.subscribe(operation, waf_context) do |result, block| + expect(result).to eq(waf_result) + expect(block).to eq(true) + end + publish_result, publish_block = described_class.publish(operation, user) + expect(publish_result).to eq(waf_result) + expect(publish_block).to eq(true) + end + end + + context 'waf result is ok' do + it 'does not yield' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :ok, timeout: false) + expect(waf_context).to receive(:run).and_return(waf_result) + expect { |b| described_class.subscribe(operation, waf_context, &b) }.not_to yield_control + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + + context 'waf result is invalid_call' do + it 'does not yield' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :invalid_call, timeout: false) + expect(waf_context).to receive(:run).and_return(waf_result) + expect { |b| described_class.subscribe(operation, waf_context, &b) }.not_to yield_control + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + + context 'waf result is invalid_rule' do + it 'does not yield' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :invalid_rule, timeout: false) + expect(waf_context).to receive(:run).and_return(waf_result) + expect { |b| described_class.subscribe(operation, waf_context, &b) }.not_to yield_control + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + + context 'waf result is invalid_flow' do + it 'does not yield' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :invalid_flow, timeout: false) + expect(waf_context).to receive(:run).and_return(waf_result) + expect { |b| described_class.subscribe(operation, waf_context, &b) }.not_to yield_control + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + + context 'waf result is no_rule' do + it 'does not yield' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :no_rule, timeout: false) + expect(waf_context).to receive(:run).and_return(waf_result) + expect { |b| described_class.subscribe(operation, waf_context, &b) }.not_to yield_control + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + + context 'waf result is unknown' do + it 'does not yield' do + expect(operation).to receive(:subscribe).and_call_original + + waf_result = double(:waf_result, status: :foo, timeout: false) + expect(waf_context).to receive(:run).and_return(waf_result) + expect { |b| described_class.subscribe(operation, waf_context, &b) }.not_to yield_control + result = described_class.publish(operation, user) + expect(result).to be_nil + end + end + end +end diff --git a/spec/datadog/appsec/processor_spec.rb b/spec/datadog/appsec/processor_spec.rb index 03991bd1fba..083f8f7aee4 100644 --- a/spec/datadog/appsec/processor_spec.rb +++ b/spec/datadog/appsec/processor_spec.rb @@ -20,6 +20,10 @@ allow(Datadog).to receive(:logger).and_return(logger) end + after do + described_class.send(:reset_active_context) + end + context 'self' do it 'detects if the WAF is unavailable' do hide_const('Datadog::AppSec::WAF') @@ -50,6 +54,50 @@ expect(described_class.require_libddwaf).to be true end + + describe '.active_context' do + it 'return nil if not set earlier' do + expect(described_class.active_context).to be_nil + end + + it 'return the previously set current context' do + processor = described_class.new + context = processor.new_context + + described_class.send(:active_context=, context) + + expect(described_class.active_context).to eq(context) + + context.finalize + processor.finalize + described_class.send(:reset_active_context) + end + + describe '.active_context=' do + it 'raises ArgumentError when trying to setup current context to a non Context instance' do + expect do + described_class.send(:active_context=, 'foo') + end.to raise_error(ArgumentError) + end + end + + describe '.reset_active_context' do + it 'sets active_context to nil' do + processor = described_class.new + context = processor.new_context + + described_class.send(:active_context=, context) + + expect(described_class.active_context).to eq(context) + + described_class.send(:reset_active_context) + expect(described_class.active_context).to be_nil + + context.finalize + processor.finalize + end + end + end end describe '#load_libddwaf' do @@ -476,4 +524,35 @@ end end end + + describe '#active_context' do + it 'creates a new context and store in the class .active_context variable' do + context = described_class.new.activate_context + expect(context).to eq(described_class.active_context) + end + + context 'when an active context already exists' do + it 'raises AlreadyActiveContextError' do + described_class.new.activate_context + expect { described_class.new.activate_context }.to raise_error(described_class::AlreadyActiveContextError) + end + end + end + + describe '#deactivate_context' do + it 'finalize the active context and reset the class .active_context variable' do + handler = described_class.new + context = handler.activate_context + + expect(context).to receive(:finalize) + handler.deactivate_context + expect(described_class.active_context).to be_nil + end + + context 'without an active_context' do + it 'raises NoActiveContextError' do + expect { described_class.new.deactivate_context }.to raise_error(described_class::NoActiveContextError) + end + end + end end