Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APPSEC-56187] Rework WAF span metrics collection #4291

Merged
merged 12 commits into from
Jan 16, 2025
5 changes: 5 additions & 0 deletions lib/datadog/appsec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def reconfigure_lock(&block)
appsec_component.reconfigure_lock(&block)
end

def api_security_enabled?
Datadog.configuration.appsec.api_security.enabled &&
Datadog.configuration.appsec.api_security.sample_rate.sample?
end

private

def components
Expand Down
25 changes: 19 additions & 6 deletions lib/datadog/appsec/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ module AppSec
# interface sufficient for instrumentation to perform threat detection.
class Context
ActiveContextError = Class.new(StandardError)
WAFMetrics = Struct.new(:timeouts, :duration_ns, :duration_ext_ns, keyword_init: true)

attr_reader :trace, :span

# NOTE: This is an intermediate state and will be changed
attr_reader :waf_runner
attr_reader :trace, :span, :events, :waf_metrics

class << self
def activate(context)
Expand All @@ -34,18 +32,33 @@ def active
def initialize(trace, span, security_engine)
@trace = trace
@span = span
@events = []
@security_engine = security_engine
@waf_runner = security_engine.new_context
@waf_runner = security_engine.new_runner
@waf_metrics = WAFMetrics.new(timeouts: 0, duration_ns: 0, duration_ext_ns: 0)
@mutex = Mutex.new
end

def run_waf(persistent_data, ephemeral_data, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT)
@waf_runner.run(persistent_data, ephemeral_data, timeout)
result = @waf_runner.run(persistent_data, ephemeral_data, timeout)

@mutex.synchronize do
@waf_metrics.timeouts += 1 if result.timeout?
@waf_metrics.duration_ns += result.duration_ns
@waf_metrics.duration_ext_ns += result.duration_ext_ns
end
y9v marked this conversation as resolved.
Show resolved Hide resolved

result
end

def run_rasp(_type, persistent_data, ephemeral_data, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT)
@waf_runner.run(persistent_data, ephemeral_data, timeout)
end

def extract_schema
@waf_runner.run({ 'waf.context.processor' => { 'extract-schema' => true } }, {})
end

def finalize
@waf_runner.finalize
end
Expand Down
4 changes: 2 additions & 2 deletions lib/datadog/appsec/contrib/active_record/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def detect_sql_injection(sql, adapter_name)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_rasp(Ext::RASP_SQLI, {}, ephemeral_data, waf_timeout)

if result.status == :match
if result.match?
Datadog::AppSec::Event.tag_and_keep!(context, result)

event = {
Expand All @@ -35,7 +35,7 @@ def detect_sql_injection(sql, adapter_name)
sql: sql,
actions: result.actions
}
context.waf_runner.events << event
context.events << event
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/graphql/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def watch_multiplex(gateway = Instrumentation.gateway)
}

Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end

block = GraphQL::Reactive::Multiplex.publish(engine, gateway_multiplex)
Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
12 changes: 6 additions & 6 deletions lib/datadog/appsec/contrib/rack/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def watch_request(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Rack::Reactive::Request.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -43,7 +43,7 @@ def watch_request(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand All @@ -61,7 +61,7 @@ def watch_response(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Rack::Reactive::Response.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -74,7 +74,7 @@ def watch_response(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand All @@ -92,7 +92,7 @@ def watch_request_body(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Rack::Reactive::RequestBody.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -105,7 +105,7 @@ def watch_request_body(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/rack/reactive/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/rack/reactive/request_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/rack/reactive/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
20 changes: 9 additions & 11 deletions lib/datadog/appsec/contrib/rack/request_middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ def call(env)

http_response = AppSec::Response.negotiate(env, block_actions).to_rack if block_actions

if (result = ctx.waf_runner.extract_schema)
ctx.waf_runner.events << {
if AppSec.api_security_enabled?
ctx.events << {
trace: ctx.trace,
span: ctx.span,
waf_result: result,
waf_result: ctx.extract_schema,
}
end

ctx.waf_runner.events.each do |e|
ctx.events.each do |e|
e[:response] ||= gateway_response
e[:request] ||= gateway_request
end

AppSec::Event.record(ctx.span, *ctx.waf_runner.events)
AppSec::Event.record(ctx.span, *ctx.events)

http_response
ensure
Expand Down Expand Up @@ -200,15 +200,13 @@ def add_request_tags(context, env)

def add_waf_runtime_tags(context)
span = context.span
context = context.waf_runner

return unless span && context
return unless span

span.set_tag('_dd.appsec.waf.timeouts', context.timeouts)
span.set_tag('_dd.appsec.waf.timeouts', context.waf_metrics.timeouts)

# these tags expect time in us
span.set_tag('_dd.appsec.waf.duration', context.time_ns / 1000.0)
span.set_tag('_dd.appsec.waf.duration_ext', context.time_ext_ns / 1000.0)
span.set_tag('_dd.appsec.waf.duration', context.waf_metrics.duration_ns / 1000.0)
span.set_tag('_dd.appsec.waf.duration_ext', context.waf_metrics.duration_ext_ns / 1000.0)
end

def to_rack_header(header)
Expand Down
4 changes: 2 additions & 2 deletions lib/datadog/appsec/contrib/rails/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def watch_request_action(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Rails::Reactive::Action.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -39,7 +39,7 @@ def watch_request_action(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/rails/reactive/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
8 changes: 4 additions & 4 deletions lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def watch_request_dispatch(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Rack::Reactive::RequestBody.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -41,7 +41,7 @@ def watch_request_dispatch(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand All @@ -59,7 +59,7 @@ def watch_request_routed(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Sinatra::Reactive::Routed.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -72,7 +72,7 @@ def watch_request_routed(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/contrib/sinatra/reactive/routed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
4 changes: 2 additions & 2 deletions lib/datadog/appsec/monitor/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def watch_user_id(gateway = Instrumentation.gateway)
engine = AppSec::Reactive::Engine.new

Monitor::Reactive::SetUser.subscribe(engine, context) do |result|
if result.status == :match
if result.match?
# TODO: should this hash be an Event instance instead?
event = {
waf_result: result,
Expand All @@ -37,7 +37,7 @@ def watch_user_id(gateway = Instrumentation.gateway)
# We want to keep the trace in case of security event
context.trace.keep! if context.trace
Datadog::AppSec::Event.tag_and_keep!(context, result)
context.waf_runner.events << event
context.events << event
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/datadog/appsec/monitor/reactive/set_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def self.subscribe(engine, context)
waf_timeout = Datadog.configuration.appsec.waf_timeout
result = context.run_waf(persistent_data, {}, waf_timeout)

next if result.status != :match
next unless result.match?

yield result
throw(:block, true) unless result.actions.empty?
Expand Down
7 changes: 4 additions & 3 deletions lib/datadog/appsec/processor.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# frozen_string_literal: true

require_relative 'processor/context'
require_relative 'security_engine/runner'

module Datadog
module AppSec
# Processor integrates libddwaf into datadog/appsec
# NOTE: This class will be moved under AppSec::SecurityEngine namespace
class Processor
attr_reader :diagnostics, :addresses

Expand All @@ -29,8 +30,8 @@ def finalize
@handle.finalize
end

def new_context
Context.new(@handle, telemetry: @telemetry)
def new_runner
SecurityEngine::Runner.new(@handle, telemetry: @telemetry)
end

private
Expand Down
Loading
Loading