diff --git a/lib/datadog/appsec/contrib/rack/request_middleware.rb b/lib/datadog/appsec/contrib/rack/request_middleware.rb index 971d0e34f8f..d229e4ea389 100644 --- a/lib/datadog/appsec/contrib/rack/request_middleware.rb +++ b/lib/datadog/appsec/contrib/rack/request_middleware.rb @@ -75,6 +75,16 @@ def call(env) _response_return, response_response = Instrumentation.gateway.push('rack.response', gateway_response) + result = scope.processor_context.extract_schema + + if result + scope.processor_context.events << { + trace: scope.trace, + span: scope.service_entry_span, + waf_result: result, + } + end + scope.processor_context.events.each do |e| e[:response] ||= gateway_response e[:request] ||= gateway_request diff --git a/lib/datadog/appsec/event.rb b/lib/datadog/appsec/event.rb index e882514d434..4462d4fcbf5 100644 --- a/lib/datadog/appsec/event.rb +++ b/lib/datadog/appsec/event.rb @@ -62,6 +62,7 @@ def self.record_via_span(span, *events) # prepare and gather tags to apply service_entry_tags = build_service_entry_tags(event_group) + # complex types are unsupported, we need to serialize to a string triggers = service_entry_tags.delete('_dd.appsec.triggers') span.set_tag('_dd.appsec.json', JSON.dump({ triggers: triggers })) @@ -104,8 +105,15 @@ def self.build_service_entry_tags(event_group) tags['_dd.origin'] = 'appsec' # accumulate triggers + waf_result = event[:waf_result] tags['_dd.appsec.triggers'] ||= [] - tags['_dd.appsec.triggers'] += event[:waf_result].events + tags['_dd.appsec.triggers'] += waf_result.events + + waf_result.derivatives.each do |key, value| + tags[key] = JSON.dump(value) + end + + tags end end end diff --git a/lib/datadog/appsec/processor.rb b/lib/datadog/appsec/processor.rb index 8e86cd37c07..c3a2c4de6e3 100644 --- a/lib/datadog/appsec/processor.rb +++ b/lib/datadog/appsec/processor.rb @@ -22,8 +22,6 @@ def run(input, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT) start_ns = Core::Utils::Time.get_time(:nanosecond) - # this WAF::Context#run call is not thread safe as it mutates the context - # TODO: remove multiple assignment _code, res = @context.run(input, timeout) stop_ns = Core::Utils::Time.get_time(:nanosecond) @@ -38,9 +36,34 @@ def run(input, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT) @run_mutex.unlock end + def extract_schema + return unless extract_schema? + + @run_mutex.lock + + input = { + 'waf.context.processor' => { + 'extract-schema' => true + } + } + + _code, res = @context.run(input, WAF::LibDDWAF::DDWAF_RUN_TIMEOUT) + + res + ensure + @run_mutex.unlock + end + def finalize @context.finalize end + + private + + def extract_schema? + Datadog.configuration.appsec.api_security.enabled && + Datadog.configuration.appsec.api_security.sample_rate.sample? + end end attr_reader :diagnostics, :addresses diff --git a/sig/datadog/appsec/processor.rbs b/sig/datadog/appsec/processor.rbs index e95895ef2ae..1938e5b4002 100644 --- a/sig/datadog/appsec/processor.rbs +++ b/sig/datadog/appsec/processor.rbs @@ -16,7 +16,11 @@ module Datadog def initialize: (Processor processor) -> void def run: (data input, ?::Integer timeout) -> WAF::Result + def extract_schema: () -> WAF::Result? def finalize: () -> void + + private + def extract_schema?: () -> bool end def self.active_context: () -> Context diff --git a/spec/datadog/appsec/contrib/rack/integration_test_spec.rb b/spec/datadog/appsec/contrib/rack/integration_test_spec.rb index b056505cf0c..5d3c5abc2fb 100644 --- a/spec/datadog/appsec/contrib/rack/integration_test_spec.rb +++ b/spec/datadog/appsec/contrib/rack/integration_test_spec.rb @@ -23,6 +23,8 @@ let(:appsec_ip_denylist) { [] } let(:appsec_user_id_denylist) { [] } let(:appsec_ruleset) { :recommended } + let(:api_security_enabled) { false } + let(:api_security_sample) { 0.0 } let(:crs_942_100) do { @@ -136,6 +138,8 @@ c.appsec.ip_denylist = appsec_ip_denylist c.appsec.user_id_denylist = appsec_user_id_denylist c.appsec.ruleset = appsec_ruleset + c.appsec.api_security.enabled = api_security_enabled + c.appsec.api_security.sample_rate = api_security_sample end end @@ -231,6 +235,19 @@ it_behaves_like 'a GET 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in headers' do @@ -243,6 +260,20 @@ it_behaves_like 'a GET 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + it { expect(triggers).to be_a Array } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in query string' do @@ -264,6 +295,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec api security tags' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + end end end @@ -278,6 +322,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering response' do @@ -302,6 +359,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -324,6 +394,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end end @@ -343,6 +426,19 @@ it_behaves_like 'a POST 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in application/x-www-form-url-encoded body' do @@ -362,6 +458,18 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -371,6 +479,19 @@ it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -394,6 +515,19 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -403,6 +537,19 @@ it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end end @@ -436,6 +583,19 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -445,6 +605,19 @@ it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end end diff --git a/spec/datadog/appsec/contrib/rails/integration_test_spec.rb b/spec/datadog/appsec/contrib/rails/integration_test_spec.rb index aa09be4da51..da31e563c5f 100644 --- a/spec/datadog/appsec/contrib/rails/integration_test_spec.rb +++ b/spec/datadog/appsec/contrib/rails/integration_test_spec.rb @@ -34,6 +34,8 @@ let(:appsec_user_id_denylist) { [] } let(:appsec_ruleset) { :recommended } let(:nested_app) { false } + let(:api_security_enabled) { false } + let(:api_security_sample) { 0.0 } let(:crs_942_100) do { @@ -93,6 +95,8 @@ c.appsec.ip_denylist = appsec_ip_denylist c.appsec.user_id_denylist = appsec_user_id_denylist c.appsec.ruleset = appsec_ruleset + c.appsec.api_security.enabled = api_security_enabled + c.appsec.api_security.sample_rate = api_security_sample c.appsec.instrument :rack if nested_app end @@ -182,6 +186,19 @@ def set_user it_behaves_like 'a GET 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in headers' do @@ -194,6 +211,20 @@ def set_user it_behaves_like 'a GET 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + it { expect(triggers).to be_a Array } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in query string' do @@ -206,6 +237,19 @@ def set_user it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -215,6 +259,19 @@ def set_user it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -234,6 +291,19 @@ def set_user it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -243,6 +313,19 @@ def set_user it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -257,6 +340,19 @@ def set_user it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering response' do @@ -269,6 +365,20 @@ def set_user it_behaves_like 'a GET 404 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_not_found } + it { expect(triggers).to be_a Array } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 404 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with user blocking ID' do @@ -281,6 +391,19 @@ def set_user it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'with an event-triggering user ID' do let(:appsec_user_id_denylist) { ['blocked-user-id'] } @@ -290,6 +413,19 @@ def set_user it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end end end @@ -309,6 +445,19 @@ def set_user it_behaves_like 'a POST 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in application/x-www-form-url-encoded body' do @@ -321,6 +470,19 @@ def set_user it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -330,6 +492,19 @@ def set_user it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -345,6 +520,19 @@ def set_user it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -354,6 +542,19 @@ def set_user it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end end @@ -369,6 +570,19 @@ def set_user it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -378,6 +592,19 @@ def set_user it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end end diff --git a/spec/datadog/appsec/contrib/sinatra/integration_test_spec.rb b/spec/datadog/appsec/contrib/sinatra/integration_test_spec.rb index 06ddebdfa69..a16802ac0b5 100644 --- a/spec/datadog/appsec/contrib/sinatra/integration_test_spec.rb +++ b/spec/datadog/appsec/contrib/sinatra/integration_test_spec.rb @@ -45,6 +45,8 @@ let(:appsec_ip_denylist) { [] } let(:appsec_user_id_denylist) { [] } let(:appsec_ruleset) { :recommended } + let(:api_security_enabled) { false } + let(:api_security_sample) { 0.0 } let(:crs_942_100) do { @@ -104,6 +106,8 @@ c.appsec.ip_denylist = appsec_ip_denylist c.appsec.user_id_denylist = appsec_user_id_denylist c.appsec.ruleset = appsec_ruleset + c.appsec.api_security.enabled = api_security_enabled + c.appsec.api_security.sample_rate = api_security_sample # TODO: test with c.appsec.instrument :rack end @@ -186,6 +190,19 @@ it_behaves_like 'a GET 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in headers' do @@ -198,6 +215,20 @@ it_behaves_like 'a GET 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + it { expect(triggers).to be_a Array } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in query string' do @@ -210,6 +241,19 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -219,6 +263,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -240,6 +297,19 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -249,6 +319,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -263,6 +346,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering response' do @@ -275,6 +371,20 @@ it_behaves_like 'a GET 404 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_not_found } + it { expect(triggers).to be_a Array } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 404 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with user blocking ID' do @@ -287,6 +397,19 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'with an event-triggering user ID' do let(:appsec_user_id_denylist) { ['blocked-user-id'] } @@ -296,6 +419,19 @@ it_behaves_like 'a GET 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a GET 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end end @@ -315,6 +451,19 @@ it_behaves_like 'a POST 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace without AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace without AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end context 'with an event-triggering request in application/x-www-form-url-encoded body' do @@ -327,6 +476,19 @@ it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end + context 'and a blocking rule' do let(:appsec_ruleset) { crs_942_100 } @@ -336,6 +498,19 @@ it_behaves_like 'a POST 403 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events', { blocking: true } + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_forbidden } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 403 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events', { blocking: true } + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -350,6 +525,19 @@ it_behaves_like 'a POST 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end end @@ -378,6 +566,19 @@ it_behaves_like 'a POST 200 span' it_behaves_like 'a trace with AppSec tags' it_behaves_like 'a trace with AppSec events' + + context 'with schema extraction' do + let(:api_security_enabled) { true } + let(:api_security_sample) { 1 } + + it { is_expected.to be_ok } + + it_behaves_like 'normal with tracing disable' + it_behaves_like 'a POST 200 span' + it_behaves_like 'a trace with AppSec tags' + it_behaves_like 'a trace with AppSec events' + it_behaves_like 'a trace with AppSec api security tags' + end end end end diff --git a/spec/datadog/appsec/contrib/support/integration/shared_examples.rb b/spec/datadog/appsec/contrib/support/integration/shared_examples.rb index 16779000993..ffa6aff3a90 100644 --- a/spec/datadog/appsec/contrib/support/integration/shared_examples.rb +++ b/spec/datadog/appsec/contrib/support/integration/shared_examples.rb @@ -120,6 +120,20 @@ end end +RSpec.shared_examples 'a trace with AppSec api security tags' do + it do + api_security_tags = service_span.send(:meta).select { |key, _value| key.include?('_dd.appsec.s') } + + expect(api_security_tags).to_not be_empty + end + + context 'with appsec disabled' do + let(:appsec_enabled) { false } + + it_behaves_like 'a trace without AppSec tags' + end +end + RSpec.shared_examples 'a trace without AppSec events' do it do expect(spans.select { |s| s.get_tag('appsec.event') }).to be_empty diff --git a/spec/datadog/appsec/event_spec.rb b/spec/datadog/appsec/event_spec.rb index f6d6f43cca9..99b97845cd2 100644 --- a/spec/datadog/appsec/event_spec.rb +++ b/spec/datadog/appsec/event_spec.rb @@ -41,11 +41,13 @@ dbl = double allow(dbl).to receive(:events).and_return([]) + allow(dbl).to receive(:derivatives).and_return(derivatives) dbl end let(:event_count) { 1 } + let(:derivatives) { {} } let(:events) do Array.new(event_count) do @@ -102,6 +104,19 @@ it 'marks the trace to be kept' do expect(trace.sampling_priority).to eq Datadog::Tracing::Sampling::Ext::Priority::USER_KEEP end + + context 'waf_result derivatives' do + let(:derivatives) do + { + '_dd.appsec.s.req.headers' => [{ 'host' => [8], 'version' => [8] }] + } + end + + it 'adds derivatives to the top level span meta' do + meta = top_level_span.meta + expect(meta['_dd.appsec.s.req.headers']).to eq JSON.dump([{ 'host' => [8], 'version' => [8] }]) + end + end end context 'with no event' do diff --git a/spec/datadog/appsec/processor_spec.rb b/spec/datadog/appsec/processor_spec.rb index bfa812d7e08..16e977c0181 100644 --- a/spec/datadog/appsec/processor_spec.rb +++ b/spec/datadog/appsec/processor_spec.rb @@ -291,4 +291,47 @@ it { expect(actions).to eq [['block']] } end end + + describe '#extract_schema' do + context 'when extrct_schema? returns true' do + around do |example| + ClimateControl.modify( + 'DD_EXPERIMENTAL_API_SECURITY_ENABLED' => 'true', + 'DD_API_SECURITY_REQUEST_SAMPLING' => '1' + ) do + example.run + end + end + + it 'calls the the WAF with the right arguments' do + input = { + 'waf.context.processor' => { + 'extract-schema' => true + } + } + + dummy_code = 1 + dummy_result = 2 + + expect(context.instance_variable_get(:@context)).to receive(:run).with( + input, + Datadog::AppSec::WAF::LibDDWAF::DDWAF_RUN_TIMEOUT + ).and_return([dummy_code, dummy_result]) + + expect(context.extract_schema).to eq dummy_result + end + end + + context 'when extrct_schema? returns false' do + around do |example| + ClimateControl.modify('DD_EXPERIMENTAL_API_SECURITY_ENABLED' => 'false') do + example.run + end + end + + it 'returns nil' do + expect(context.extract_schema).to be_nil + end + end + end end