Skip to content

Commit

Permalink
Merge pull request #3965 from DataDog/vpellan/standalone-asm
Browse files Browse the repository at this point in the history
  • Loading branch information
vpellan authored Oct 21, 2024
2 parents 28b1d66 + 8e54f7d commit df97f48
Show file tree
Hide file tree
Showing 43 changed files with 998 additions and 78 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ env:
REGISTRY: ghcr.io
REPO: ghcr.io/datadog/dd-trace-rb
ST_REF: main
FORCE_TESTS: -F tests/appsec/waf/test_addresses.py::Test_GraphQL -F tests/appsec/test_blocking_addresses.py::Test_BlockingGraphqlResolvers
FORCE_TESTS_SCENARIO: GRAPHQL_APPSEC
FORCE_TESTS: -F tests/appsec/test_asm_standalone.py
FORCE_TESTS_SCENARIO: APPSEC_STANDALONE

jobs:
build-harness:
Expand Down Expand Up @@ -199,6 +199,7 @@ jobs:
- APPSEC_DISABLED
- APPSEC_BLOCKING_FULL_DENYLIST
- APPSEC_REQUEST_BLOCKING
- APPSEC_STANDALONE
include:
- library: ruby
app: rack
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

* AppSec: Add Experimental Standalone AppSec Threats billing ([#3965][])

## [2.4.0] - 2024-10-11

### Added
Expand Down
1 change: 1 addition & 0 deletions lib/datadog/appsec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative 'appsec/extensions'
require_relative 'appsec/scope'
require_relative 'appsec/ext'
require_relative 'appsec/utils'

module Datadog
# Namespace for Datadog AppSec instrumentation
Expand Down
8 changes: 8 additions & 0 deletions lib/datadog/appsec/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ def self.add_settings!(base)
o.type :bool, nilable: true
o.env 'DD_APPSEC_SCA_ENABLED'
end

settings :standalone do
option :enabled do |o|
o.type :bool
o.env 'DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED'
o.default false
end
end
end
end
end
Expand Down
6 changes: 1 addition & 5 deletions lib/datadog/appsec/contrib/graphql/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ def watch_multiplex(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end

Expand Down
24 changes: 9 additions & 15 deletions lib/datadog/appsec/contrib/rack/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,9 @@ def watch_request(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down Expand Up @@ -85,11 +83,9 @@ def watch_response(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down Expand Up @@ -129,11 +125,9 @@ def watch_request_body(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/datadog/appsec/contrib/rack/request_middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ def add_appsec_tags(processor, scope)

return unless trace && span

span.set_tag('_dd.appsec.enabled', 1)
span.set_metric(Datadog::AppSec::Ext::TAG_APPSEC_ENABLED, 1)
# We add this tag when ASM standalone is enabled to make sure we don't bill APM
span.set_metric(Datadog::AppSec::Ext::TAG_APM_ENABLED, 0) if Datadog.configuration.appsec.standalone.enabled
span.set_tag('_dd.runtime_family', 'ruby')
span.set_tag('_dd.appsec.waf.version', Datadog::AppSec::WAF::VERSION::BASE_STRING)

Expand Down
8 changes: 3 additions & 5 deletions lib/datadog/appsec/contrib/rails/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ def watch_request_action(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down
16 changes: 6 additions & 10 deletions lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,9 @@ def watch_request_dispatch(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down Expand Up @@ -84,11 +82,9 @@ def watch_request_routed(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down
24 changes: 24 additions & 0 deletions lib/datadog/appsec/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ def build_service_entry_tags(event_group)
end
# rubocop:enable Metrics/MethodLength

def tag_and_keep!(scope, waf_result)
# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if waf_result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

add_distributed_tags(scope.trace)
end

private

def compressed_and_base64_encoded(value)
Expand Down Expand Up @@ -165,6 +177,18 @@ def gzip(value)
gz.close
sio.string
end

# Propagate to downstream services the information that the current distributed trace is
# containing at least one ASM security event.
def add_distributed_tags(trace)
return unless trace

trace.set_tag(
Datadog::Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER,
Datadog::Tracing::Sampling::Ext::Decision::ASM
)
trace.set_tag(Datadog::AppSec::Ext::TAG_DISTRIBUTED_APPSEC_EVENT, '1')
end
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/datadog/appsec/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module AppSec
module Ext
INTERRUPT = :datadog_appsec_interrupt
SCOPE_KEY = 'datadog.appsec.scope'

TAG_APPSEC_ENABLED = '_dd.appsec.enabled'
TAG_APM_ENABLED = '_dd.apm.enabled'
TAG_DISTRIBUTED_APPSEC_EVENT = '_dd.p.appsec'
end
end
end
8 changes: 3 additions & 5 deletions lib/datadog/appsec/monitor/gateway/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ def watch_user_id(gateway = Instrumentation.gateway)
actions: result.actions
}

if scope.service_entry_span
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
scope.service_entry_span.set_tag('appsec.event', 'true')
end

# We want to keep the trace in case of security event
scope.trace.keep! if scope.trace
Datadog::AppSec::Event.tag_and_keep!(scope, result)
scope.processor_context.events << event
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/datadog/appsec/utils.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative 'utils/trace_operation'

module Datadog
module AppSec
# Utilities for AppSec
Expand Down
15 changes: 15 additions & 0 deletions lib/datadog/appsec/utils/trace_operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Datadog
module AppSec
module Utils
# Utility class to to AppSec-specific trace operations
class TraceOperation
def self.appsec_standalone_reject?(trace)
Datadog.configuration.appsec.standalone.enabled &&
(trace.nil? || trace.get_tag(Datadog::AppSec::Ext::TAG_DISTRIBUTED_APPSEC_EVENT) != '1')
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/datadog/core/remote/transport/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ def default_headers
# Add container ID, if present.
container_id = Datadog::Core::Environment::Container.container_id
headers[Datadog::Core::Transport::Ext::HTTP::HEADER_CONTAINER_ID] = container_id unless container_id.nil?
# Sending this header to the agent will disable metrics computation (and billing) on the agent side
# by pretending it has already been done on the library side.
if Datadog.configuration.appsec.standalone.enabled
headers[Datadog::Core::Transport::Ext::HTTP::HEADER_CLIENT_COMPUTED_STATS] = 'yes'
end
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/datadog/core/transport/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module HTTP
#
# Setting this header to any non-empty value enables this feature.
HEADER_CLIENT_COMPUTED_TOP_LEVEL = 'Datadog-Client-Computed-Top-Level'
HEADER_CLIENT_COMPUTED_STATS = 'Datadog-Client-Computed-Stats'
HEADER_META_LANG = 'Datadog-Meta-Lang'
HEADER_META_LANG_VERSION = 'Datadog-Meta-Lang-Version'
HEADER_META_LANG_INTERPRETER = 'Datadog-Meta-Lang-Interpreter'
Expand Down
13 changes: 13 additions & 0 deletions lib/datadog/tracing/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ def build_sampler(settings)
return sampler
end

# AppSec events are sent to the backend using traces.
# Standalone ASM billing means that we don't want to charge clients for APM traces,
# so we want to send the minimum amount of traces possible (idealy only traces that contains security events),
# but for features such as API Security, we need to send at least one trace per minute,
# to keep the service alive on the backend side.
if settings.appsec.standalone.enabled
post_sampler = Tracing::Sampling::RuleSampler.new(
[Tracing::Sampling::SimpleRule.new(sample_rate: 1.0)],
rate_limiter: Datadog::Core::TokenBucket.new(1.0 / 60, 1.0),
default_sample_rate: 1.0 / 60
)
end

# Sampling rules are provided
if (rules = settings.tracing.sampling.rules)
post_sampler = Tracing::Sampling::RuleSampler.parse(
Expand Down
4 changes: 4 additions & 0 deletions lib/datadog/tracing/contrib/ethon/easy_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ def datadog_before_request(continue_from: nil)

datadog_tag_request

if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(datadog_trace)
datadog_trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end

if datadog_configuration[:distributed_tracing]
@datadog_original_headers ||= {}
Contrib::HTTP.inject(datadog_trace, @datadog_original_headers)
Expand Down
3 changes: 3 additions & 0 deletions lib/datadog/tracing/contrib/excon/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def request_call(datum)
trace = Tracing.active_trace
datum[:datadog_span] = span
annotate!(span, datum)
if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(trace)
trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end
propagate!(trace, span, datum) if distributed_tracing?

span
Expand Down
3 changes: 3 additions & 0 deletions lib/datadog/tracing/contrib/faraday/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def call(env)

Tracing.trace(Ext::SPAN_REQUEST, on_error: request_options[:on_error]) do |span, trace|
annotate!(span, env, request_options)
if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(trace)
trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end
propagate!(trace, span, env) if request_options[:distributed_tracing] && Tracing.enabled?
app.call(env).on_complete { |resp| handle_response(span, resp, request_options) }
end
Expand Down
9 changes: 9 additions & 0 deletions lib/datadog/tracing/contrib/http/circuit_breaker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ def internal_request?(request)
end

def should_skip_distributed_tracing?(client_config)
if Datadog.configuration.appsec.standalone.enabled
# Skip distributed tracing so that we don't bill distributed traces in case of absence of
# upstream ASM event (_dd.p.appsec:1) and no local security event (which sets _dd.p.appsec:1 locally).
# If there is an ASM event, we still have to check if distributed tracing is enabled or not
return true unless Tracing.active_trace

return true if Tracing.active_trace.get_tag(Datadog::AppSec::Ext::TAG_DISTRIBUTED_APPSEC_EVENT) != '1'
end

return !client_config[:distributed_tracing] if client_config && client_config.key?(:distributed_tracing)

!Datadog.configuration.tracing[:http][:distributed_tracing]
Expand Down
4 changes: 4 additions & 0 deletions lib/datadog/tracing/contrib/http/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def request(req, body = nil, &block)
span.type = Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND
span.resource = req.method

if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(trace)
trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end

if Tracing.enabled? && !Contrib::HTTP.should_skip_distributed_tracing?(client_config)
Contrib::HTTP.inject(trace, req)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/datadog/tracing/contrib/httpclient/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def do_get_block(req, proxy, conn, &block)
span.service = service_name(host, request_options, client_config)
span.type = Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND

if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(trace)
trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end

if Tracing.enabled? && !should_skip_distributed_tracing?(client_config)
Contrib::HTTP.inject(trace, req.header)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/datadog/tracing/contrib/httprb/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def perform(req, options)
span.service = service_name(host, request_options, client_config)
span.type = Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND

if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(trace)
trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end

Contrib::HTTP.inject(trace, req) if Tracing.enabled? && !should_skip_distributed_tracing?(client_config)

# Add additional request specific tags to the span.
Expand Down
3 changes: 3 additions & 0 deletions lib/datadog/tracing/contrib/rest_client/request_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def execute(&block)
return super(&block) unless Tracing.enabled?

datadog_trace_request(uri) do |_span, trace|
if Datadog::AppSec::Utils::TraceOperation.appsec_standalone_reject?(trace)
trace.sampling_priority = Tracing::Sampling::Ext::Priority::AUTO_REJECT
end
Contrib::HTTP.inject(trace, processed_headers) if datadog_configuration[:distributed_tracing]

super(&block)
Expand Down
Loading

0 comments on commit df97f48

Please sign in to comment.