From 9f06a5e0cba5556e3e0193c9fc9212a24e2df1f1 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Thu, 23 Jan 2025 15:50:26 +0100 Subject: [PATCH 1/2] Add telemetry metrics reporting to the rasp runs --- lib/datadog/appsec/context.rb | 4 +- lib/datadog/appsec/ext.rb | 7 +- lib/datadog/appsec/metrics.rb | 1 + lib/datadog/appsec/metrics/telemetry.rb | 23 +++++ sig/datadog/appsec/context.rbs | 2 +- sig/datadog/appsec/ext.rbs | 15 ++- sig/datadog/appsec/metrics/telemetry.rbs | 9 ++ spec/datadog/appsec/context_spec.rb | 51 ++++++++++ spec/datadog/appsec/metrics/telemetry_spec.rb | 96 +++++++++++++++++++ 9 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 lib/datadog/appsec/metrics/telemetry.rb create mode 100644 sig/datadog/appsec/metrics/telemetry.rbs create mode 100644 spec/datadog/appsec/metrics/telemetry_spec.rb diff --git a/lib/datadog/appsec/context.rb b/lib/datadog/appsec/context.rb index afc67f1a2bd..7038c376079 100644 --- a/lib/datadog/appsec/context.rb +++ b/lib/datadog/appsec/context.rb @@ -46,10 +46,12 @@ def run_waf(persistent_data, ephemeral_data, timeout = WAF::LibDDWAF::DDWAF_RUN_ result end - def run_rasp(_type, persistent_data, ephemeral_data, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT) + def run_rasp(type, persistent_data, ephemeral_data, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT) result = @waf_runner.run(persistent_data, ephemeral_data, timeout) + Metrics::Telemetry.report_rasp(type, result) @metrics.record_rasp(result) + result end diff --git a/lib/datadog/appsec/ext.rb b/lib/datadog/appsec/ext.rb index 2c6e6f5fae3..6c76e708ad1 100644 --- a/lib/datadog/appsec/ext.rb +++ b/lib/datadog/appsec/ext.rb @@ -3,7 +3,10 @@ module Datadog module AppSec module Ext - RASP_SQLI = :sql_injection + RASP_SQLI = 'sql_injection' + RASP_LFI = 'lfi' + RASP_SSRF = 'ssrf' + INTERRUPT = :datadog_appsec_interrupt CONTEXT_KEY = 'datadog.appsec.context' ACTIVE_CONTEXT_KEY = :datadog_appsec_active_context @@ -11,6 +14,8 @@ module Ext TAG_APPSEC_ENABLED = '_dd.appsec.enabled' TAG_APM_ENABLED = '_dd.apm.enabled' TAG_DISTRIBUTED_APPSEC_EVENT = '_dd.p.appsec' + + TELEMETRY_METRICS_NAMESPACE = 'appsec' end end end diff --git a/lib/datadog/appsec/metrics.rb b/lib/datadog/appsec/metrics.rb index 62e34ef7f2b..2f78f5bc3a0 100644 --- a/lib/datadog/appsec/metrics.rb +++ b/lib/datadog/appsec/metrics.rb @@ -10,3 +10,4 @@ module Metrics require_relative 'metrics/collector' require_relative 'metrics/exporter' +require_relative 'metrics/telemetry' diff --git a/lib/datadog/appsec/metrics/telemetry.rb b/lib/datadog/appsec/metrics/telemetry.rb new file mode 100644 index 00000000000..26eecaa4ed6 --- /dev/null +++ b/lib/datadog/appsec/metrics/telemetry.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Datadog + module AppSec + module Metrics + # A class responsible for reporting WAF and RASP telemetry metrics. + module Telemetry + module_function + + def report_rasp(type, result) + return if result.is_a?(SecurityEngine::Result::Error) + + tags = { rule_type: type, waf_version: Datadog::AppSec::WAF::VERSION::BASE_STRING } + namespace = Ext::TELEMETRY_METRICS_NAMESPACE + + AppSec.telemetry.inc(namespace, 'rasp.rule.eval', 1, tags: tags) + AppSec.telemetry.inc(namespace, 'rasp.rule.match', 1, tags: tags) if result.match? + AppSec.telemetry.inc(namespace, 'rasp.timeout', 1, tags: tags) if result.timeout? + end + end + end + end +end diff --git a/sig/datadog/appsec/context.rbs b/sig/datadog/appsec/context.rbs index f8540125e16..1a3ecc0896a 100644 --- a/sig/datadog/appsec/context.rbs +++ b/sig/datadog/appsec/context.rbs @@ -33,7 +33,7 @@ module Datadog def run_waf: (input_data persistent_data, input_data ephemeral_data, ?Integer timeout) -> SecurityEngine::result - def run_rasp: (::Symbol _type, input_data persistent_data, input_data ephemeral_data, ?Integer timeout) -> SecurityEngine::result + def run_rasp: (Ext::rasp_rule_type type, input_data persistent_data, input_data ephemeral_data, ?Integer timeout) -> SecurityEngine::result def export_metrics: () -> void diff --git a/sig/datadog/appsec/ext.rbs b/sig/datadog/appsec/ext.rbs index 8c4b59fb575..58bb2277da3 100644 --- a/sig/datadog/appsec/ext.rbs +++ b/sig/datadog/appsec/ext.rbs @@ -1,14 +1,27 @@ module Datadog module AppSec module Ext - RASP_SQLI: ::Symbol + type rasp_rule_type = ::String + + RASP_SQLI: ::String + + RASP_LFI: ::String + + RASP_SSRF: ::String + INTERRUPT: ::Symbol + CONTEXT_KEY: ::String + ACTIVE_CONTEXT_KEY: ::Symbol TAG_APPSEC_ENABLED: ::String + TAG_APM_ENABLED: ::String + TAG_DISTRIBUTED_APPSEC_EVENT: ::String + + TELEMETRY_METRICS_NAMESPACE: ::String end end end diff --git a/sig/datadog/appsec/metrics/telemetry.rbs b/sig/datadog/appsec/metrics/telemetry.rbs new file mode 100644 index 00000000000..c96161017e7 --- /dev/null +++ b/sig/datadog/appsec/metrics/telemetry.rbs @@ -0,0 +1,9 @@ +module Datadog + module AppSec + module Metrics + module Telemetry + def self?.report_rasp: (Ext::rasp_rule_type type, SecurityEngine::result result) -> void + end + end + end +end diff --git a/spec/datadog/appsec/context_spec.rb b/spec/datadog/appsec/context_spec.rb index 5b5f5d3c031..171efd0f9b2 100644 --- a/spec/datadog/appsec/context_spec.rb +++ b/spec/datadog/appsec/context_spec.rb @@ -126,6 +126,57 @@ end end + describe '#run_rasp' do + context 'when a matching run was made' do + before { allow(Datadog::AppSec).to receive(:telemetry).and_return(telemetry) } + + let(:persistent_data) do + { 'server.request.query' => { 'q' => "1' OR 1=1;" } } + end + let(:ephemeral_data) do + { + 'server.db.statement' => "SELECT * FROM users WHERE name = '1' OR 1=1;", + 'server.db.system' => 'mysql' + } + end + + it 'sends telemetry metrics' do + expect(telemetry).to receive(:inc) + .with('appsec', anything, kind_of(Integer), anything) + .at_least(:once) + + context.run_rasp('sqli', persistent_data, ephemeral_data, 10_000) + end + end + + context 'when a run was a failure' do + before do + allow(Datadog::AppSec).to receive(:telemetry).and_return(telemetry) + allow_any_instance_of(Datadog::AppSec::SecurityEngine::Runner).to receive(:run) + .and_return(run_result) + end + + let(:run_result) do + Datadog::AppSec::SecurityEngine::Result::Error.new(duration_ext_ns: 0) + end + let(:persistent_data) do + { 'server.request.query' => { 'q' => "1' OR 1=1;" } } + end + let(:ephemeral_data) do + { + 'server.db.statement' => "SELECT * FROM users WHERE name = '1' OR 1=1;", + 'server.db.system' => 'mysql' + } + end + + it 'sends telemetry metrics' do + expect(telemetry).not_to receive(:inc) + + context.run_rasp('sqli', persistent_data, ephemeral_data, 10_000) + end + end + end + describe '#extract_schema' do it 'calls the waf runner with specific addresses' do expect_any_instance_of(Datadog::AppSec::SecurityEngine::Runner).to receive(:run) diff --git a/spec/datadog/appsec/metrics/telemetry_spec.rb b/spec/datadog/appsec/metrics/telemetry_spec.rb new file mode 100644 index 00000000000..4340082354a --- /dev/null +++ b/spec/datadog/appsec/metrics/telemetry_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'datadog/appsec/spec_helper' + +RSpec.describe Datadog::AppSec::Metrics::Telemetry do + before do + stub_const('Datadog::AppSec::Ext::TELEMETRY_METRICS_NAMESPACE', 'specsec') + stub_const('Datadog::AppSec::WAF::VERSION::BASE_STRING', '1.42.99') + + allow(Datadog::AppSec).to receive(:telemetry).and_return(telemetry) + end + + let(:telemetry) { instance_double(Datadog::Core::Telemetry::Component) } + + describe '.report_rasp' do + context 'when reporting a match run result' do + let(:run_result) do + Datadog::AppSec::SecurityEngine::Result::Match.new( + events: [], actions: {}, derivatives: {}, timeout: false, duration_ns: 0, duration_ext_ns: 0 + ) + end + + it 'does not set WAF metrics on the span' do + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.rule.eval', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.rule.match', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + + described_class.report_rasp('my-type', run_result) + end + end + + context 'when reporting a match run result with timeout' do + let(:run_result) do + Datadog::AppSec::SecurityEngine::Result::Match.new( + events: [], actions: {}, derivatives: {}, timeout: true, duration_ns: 0, duration_ext_ns: 0 + ) + end + + it 'does not set WAF metrics on the span' do + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.rule.eval', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.rule.match', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.timeout', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + + described_class.report_rasp('my-type', run_result) + end + end + + context 'when reporting a ok run result' do + let(:run_result) do + Datadog::AppSec::SecurityEngine::Result::Ok.new( + events: [], actions: {}, derivatives: {}, timeout: false, duration_ns: 0, duration_ext_ns: 0 + ) + end + + it 'does not set WAF metrics on the span' do + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.rule.eval', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + + described_class.report_rasp('my-type', run_result) + end + end + + context 'when reporting a ok run result with timeout' do + let(:run_result) do + Datadog::AppSec::SecurityEngine::Result::Ok.new( + events: [], actions: {}, derivatives: {}, timeout: true, duration_ns: 0, duration_ext_ns: 0 + ) + end + + it 'does not set WAF metrics on the span' do + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.rule.eval', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + expect(telemetry).to receive(:inc) + .with('specsec', 'rasp.timeout', 1, tags: { rule_type: 'my-type', waf_version: '1.42.99' }) + + described_class.report_rasp('my-type', run_result) + end + end + + context 'when reporting a error run result' do + let(:run_result) do + Datadog::AppSec::SecurityEngine::Result::Error.new(duration_ext_ns: 0) + end + + it 'does not set WAF metrics on the span' do + expect(telemetry).not_to receive(:inc) + + described_class.report_rasp('my-type', run_result) + end + end + end +end From 9e07af4d5d964e20c3a6b403261c472dc43628f7 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Fri, 24 Jan 2025 10:25:22 +0100 Subject: [PATCH 2/2] Increase WAF calls timeout to avoid false positive --- spec/datadog/appsec/context_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/datadog/appsec/context_spec.rb b/spec/datadog/appsec/context_spec.rb index 171efd0f9b2..3815636a264 100644 --- a/spec/datadog/appsec/context_spec.rb +++ b/spec/datadog/appsec/context_spec.rb @@ -88,7 +88,7 @@ 'server.request.headers.no_cookies' => { 'user-agent' => 'Nessus SOAP' } } - Array.new(3) { context.run_waf(persistent_data, {}, 10_000) } + Array.new(3) { context.run_waf(persistent_data, {}, 1_000_000) } end it 'returns a single match and rest is ok' do @@ -110,8 +110,8 @@ } [ - context.run_waf(persistent_data_1, {}, 10_000), - context.run_waf(persistent_data_2, {}, 10_000), + context.run_waf(persistent_data_1, {}, 1_000_000), + context.run_waf(persistent_data_2, {}, 1_000_000), ] end @@ -145,7 +145,7 @@ .with('appsec', anything, kind_of(Integer), anything) .at_least(:once) - context.run_rasp('sqli', persistent_data, ephemeral_data, 10_000) + context.run_rasp('sqli', persistent_data, ephemeral_data, 1_000_000) end end @@ -172,7 +172,7 @@ it 'sends telemetry metrics' do expect(telemetry).not_to receive(:inc) - context.run_rasp('sqli', persistent_data, ephemeral_data, 10_000) + context.run_rasp('sqli', persistent_data, ephemeral_data, 1_000_000) end end end