Skip to content

Commit

Permalink
Add AppSec::ActionHandler module
Browse files Browse the repository at this point in the history
  • Loading branch information
y9v committed Jan 16, 2025
1 parent b5cf594 commit bc7bfb2
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 169 deletions.
36 changes: 36 additions & 0 deletions lib/datadog/appsec/action_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Datadog
module AppSec
# this module encapsulates functions for handling actions that libddawf returns
module ActionHandler
module_function

def handle(type, action_params)
case type
when 'block_request' then block_request(action_params)
when 'redirect_request' then redirect_request(action_params)
when 'generate_stack' then generate_stack(action_params)
when 'generate_schema' then generate_schema(action_params)
when 'monitor' then monitor(action_params)
else
Datadog.logger.error "Unknown action type: #{type}"
end
end

def block_request(action_params)
throw(Datadog::AppSec::Ext::INTERRUPT, action_params)
end

def redirect_request(action_params)
throw(Datadog::AppSec::Ext::INTERRUPT, action_params)
end

def generate_stack(_action_params); end

def generate_schema(_action_params); end

def monitor(_action_params); end
end
end
end
4 changes: 2 additions & 2 deletions lib/datadog/appsec/contrib/rack/request_body_middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ def call(env)
# TODO: handle exceptions, except for @app.call

http_response = nil
block_actions = catch(::Datadog::AppSec::Ext::INTERRUPT) do
block_action_params = catch(::Datadog::AppSec::Ext::INTERRUPT) do
http_response, = Instrumentation.gateway.push('rack.request.body', Gateway::Request.new(env)) do
@app.call(env)
end

nil
end

return AppSec::Response.negotiate(env, block_actions).to_rack if block_actions
return AppSec::Response.build(block_action_params, env['HTTP_ACCEPT']).to_rack if block_action_params

http_response
end
Expand Down
4 changes: 2 additions & 2 deletions lib/datadog/appsec/contrib/rack/request_middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def call(env)
gateway_request = Gateway::Request.new(env)
gateway_response = nil

block_actions = catch(::Datadog::AppSec::Ext::INTERRUPT) do
block_action_params = catch(::Datadog::AppSec::Ext::INTERRUPT) do
http_response, = Instrumentation.gateway.push('rack.request', gateway_request) do
@app.call(env)
end
Expand All @@ -90,7 +90,7 @@ def call(env)
nil
end

http_response = AppSec::Response.negotiate(env, block_actions).to_rack if block_actions
http_response = AppSec::Response.build(block_action_params, env['HTTP_ACCEPT']).to_rack if block_action_params

if AppSec.api_security_enabled?
ctx.events << {
Expand Down
7 changes: 4 additions & 3 deletions lib/datadog/appsec/contrib/rails/patcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ def process_action(*args)
if request_response
blocked_event = request_response.find { |action, _options| action == :block }
if blocked_event
@_response = AppSec::Response.negotiate(
env,
blocked_event.last[:actions]
@_response = AppSec::Response.build(
blocked_event.last[:actions].values.first,
env['HTTP_ACCEPT']
).to_action_dispatch_response

request_return = @_response.body
end
end
Expand Down
11 changes: 9 additions & 2 deletions lib/datadog/appsec/contrib/sinatra/patcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ def dispatch!
if request_response
blocked_event = request_response.find { |action, _options| action == :block }
if blocked_event
self.response = AppSec::Response.negotiate(env, blocked_event.last[:actions]).to_sinatra_response
self.response = AppSec::Response.build(
blocked_event.last[:actions].values.first,
env['HTTP_ACCEPT']
).to_sinatra_response

request_return = nil
end
end
Expand Down Expand Up @@ -111,7 +115,10 @@ def process_route(*)
if request_response
blocked_event = request_response.find { |action, _options| action == :block }
if blocked_event
self.response = AppSec::Response.negotiate(env, blocked_event.last[:actions]).to_sinatra_response
self.response = AppSec::Response.build(
blocked_event.last[:actions].values.first,
env['HTTP_ACCEPT']
).to_sinatra_response

# interrupt request and return response to dispatch! for consistency
throw(Datadog::AppSec::Contrib::Sinatra::Ext::ROUTE_INTERRUPT, response)
Expand Down
76 changes: 18 additions & 58 deletions lib/datadog/appsec/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,10 @@ def to_action_dispatch_response
end

class << self
def negotiate(env, actions)
# @type var configured_response: Response?
configured_response = nil
actions.each do |type, parameters|
# Need to use next to make steep happy :(
# I rather use break to stop the execution
next if configured_response

configured_response = case type
when 'block_request'
block_response(env, parameters)
when 'redirect_request'
redirect_response(env, parameters)
end
end
def build(action_params, http_accept_header)
return redirect_response(action_params) if action_params['location']

configured_response || default_response(env)
block_response(action_params, http_accept_header)
end

def graphql_response(gateway_multiplex)
Expand All @@ -63,56 +50,29 @@ def graphql_response(gateway_multiplex)

private

def default_response(env)
content_type = content_type(env)

body = []
body << content(content_type)
def block_response(action_params, http_accept_header)
content_type = case action_params['type']
when nil, 'auto' then content_type(http_accept_header)
else FORMAT_TO_CONTENT_TYPE.fetch(action_params['type'], DEFAULT_CONTENT_TYPE)
end

Response.new(
status: 403,
status: action_params['status_code']&.to_i || 403,
headers: { 'Content-Type' => content_type },
body: body,
body: [content(content_type)],
)
end

def block_response(env, options)
content_type = if options['type'] == 'auto'
content_type(env)
else
FORMAT_TO_CONTENT_TYPE[options['type']]
end

body = []
body << content(content_type)
def redirect_response(action_params)
status_code = action_params['status_code'].to_i

Response.new(
status: options['status_code']&.to_i || 403,
headers: { 'Content-Type' => content_type },
body: body,
status: (status_code >= 300 && status_code < 400 ? status_code : 303),
headers: { 'Location' => action_params.fetch('location') },
body: [],
)
end

def redirect_response(env, options)
if options['location'] && !options['location'].empty?
content_type = content_type(env)

headers = {
'Content-Type' => content_type,
'Location' => options['location']
}

status_code = options['status_code'].to_i
Response.new(
status: (status_code >= 300 && status_code < 400 ? status_code : 303),
headers: headers,
body: [],
)
else
default_response(env)
end
end

CONTENT_TYPE_TO_FORMAT = {
'application/json' => :json,
'text/html' => :html,
Expand All @@ -126,10 +86,10 @@ def redirect_response(env, options)

DEFAULT_CONTENT_TYPE = 'application/json'

def content_type(env)
return DEFAULT_CONTENT_TYPE unless env.key?('HTTP_ACCEPT')
def content_type(http_accept_header)
return DEFAULT_CONTENT_TYPE if http_accept_header.nil?

accept_types = env['HTTP_ACCEPT'].split(',').map(&:strip)
accept_types = http_accept_header.split(',').map(&:strip)

accepted = accept_types.map { |m| Utils::HTTP::MediaRange.new(m) }.sort!.reverse!

Expand Down
9 changes: 4 additions & 5 deletions sig/datadog/appsec/response.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module Datadog
def to_sinatra_response: () -> ::Sinatra::Response
def to_action_dispatch_response: () -> ::ActionDispatch::Response

def self.negotiate: (::Hash[untyped, untyped] env, ::Hash[String, untyped] actions) -> Response
def self.build: (::Hash[::String, ::String] action_params, ::String http_accept_header) -> Response
def self.graphql_response: (Datadog::AppSec::Contrib::GraphQL::Gateway::Multiplex gateway_multiplex) -> Array[::GraphQL::Query::Result]

private
Expand All @@ -20,11 +20,10 @@ module Datadog
FORMAT_TO_CONTENT_TYPE: ::Hash[::String, ::String]
DEFAULT_CONTENT_TYPE: ::String

def self.default_response: (::Hash[untyped, untyped] env) -> Response
def self.block_response: (::Hash[untyped, untyped] env, ::Hash[String, untyped] options) -> Response
def self.redirect_response: (::Hash[untyped, untyped] env, ::Hash[String, untyped] options) -> Response
def self.block_response: (::Hash[::String, ::String] action_params, ::String http_accept_header) -> Response
def self.redirect_response: (::Hash[::String, ::String] action_params) -> Response

def self.content_type: (::Hash[untyped, untyped] env) -> ::String
def self.content_type: (::String) -> ::String
def self.content: (::String) -> ::String
end
end
Expand Down
Loading

0 comments on commit bc7bfb2

Please sign in to comment.