datadog 2.2.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -2
- data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
- data/ext/datadog_profiling_loader/extconf.rb +14 -26
- data/ext/datadog_profiling_native_extension/clock_id.h +1 -0
- data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -2
- data/ext/datadog_profiling_native_extension/clock_id_noop.c +1 -2
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +257 -69
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +53 -28
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +34 -4
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +4 -0
- data/ext/datadog_profiling_native_extension/collectors_stack.c +136 -81
- data/ext/datadog_profiling_native_extension/collectors_stack.h +2 -2
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +661 -48
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +10 -1
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +83 -0
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +53 -0
- data/ext/datadog_profiling_native_extension/extconf.rb +91 -69
- data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
- data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
- data/ext/datadog_profiling_native_extension/heap_recorder.c +54 -12
- data/ext/datadog_profiling_native_extension/heap_recorder.h +3 -1
- data/ext/datadog_profiling_native_extension/helpers.h +6 -17
- data/ext/datadog_profiling_native_extension/http_transport.c +41 -9
- data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +0 -86
- data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +2 -23
- data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +61 -172
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +116 -139
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +20 -11
- data/ext/datadog_profiling_native_extension/profiling.c +1 -3
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +0 -33
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +1 -26
- data/ext/datadog_profiling_native_extension/setup_signal_handler.h +1 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +14 -2
- data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -0
- data/ext/datadog_profiling_native_extension/time_helpers.c +0 -15
- data/ext/datadog_profiling_native_extension/time_helpers.h +36 -6
- data/ext/{datadog_profiling_native_extension → libdatadog_api}/crashtracker.c +37 -22
- data/ext/libdatadog_api/datadog_ruby_common.c +83 -0
- data/ext/libdatadog_api/datadog_ruby_common.h +53 -0
- data/ext/libdatadog_api/extconf.rb +108 -0
- data/ext/libdatadog_api/macos_development.md +26 -0
- data/ext/libdatadog_extconf_helpers.rb +130 -0
- data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
- data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
- data/lib/datadog/appsec/component.rb +29 -8
- data/lib/datadog/appsec/configuration/settings.rb +2 -2
- data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
- data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
- data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
- data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +35 -0
- data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +109 -0
- data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +71 -0
- data/lib/datadog/appsec/contrib/graphql/integration.rb +54 -0
- data/lib/datadog/appsec/contrib/graphql/patcher.rb +37 -0
- data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +59 -0
- data/lib/datadog/appsec/contrib/rack/gateway/request.rb +3 -6
- data/lib/datadog/appsec/event.rb +1 -1
- data/lib/datadog/appsec/processor/actions.rb +1 -1
- data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
- data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
- data/lib/datadog/appsec/processor.rb +36 -37
- data/lib/datadog/appsec/rate_limiter.rb +25 -40
- data/lib/datadog/appsec/remote.rb +7 -3
- data/lib/datadog/appsec/response.rb +15 -1
- data/lib/datadog/appsec.rb +3 -2
- data/lib/datadog/core/configuration/components.rb +18 -15
- data/lib/datadog/core/configuration/settings.rb +135 -9
- data/lib/datadog/core/crashtracking/agent_base_url.rb +21 -0
- data/lib/datadog/core/crashtracking/component.rb +111 -0
- data/lib/datadog/core/crashtracking/tag_builder.rb +39 -0
- data/lib/datadog/core/diagnostics/environment_logger.rb +8 -11
- data/lib/datadog/core/environment/execution.rb +5 -5
- data/lib/datadog/core/metrics/client.rb +7 -0
- data/lib/datadog/core/rate_limiter.rb +183 -0
- data/lib/datadog/core/remote/client/capabilities.rb +4 -3
- data/lib/datadog/core/remote/component.rb +4 -2
- data/lib/datadog/core/remote/negotiation.rb +4 -4
- data/lib/datadog/core/remote/tie.rb +2 -0
- data/lib/datadog/core/runtime/metrics.rb +1 -1
- data/lib/datadog/core/telemetry/component.rb +51 -2
- data/lib/datadog/core/telemetry/emitter.rb +9 -11
- data/lib/datadog/core/telemetry/event.rb +37 -1
- data/lib/datadog/core/telemetry/ext.rb +1 -0
- data/lib/datadog/core/telemetry/http/adapters/net.rb +10 -12
- data/lib/datadog/core/telemetry/http/ext.rb +3 -0
- data/lib/datadog/core/telemetry/http/transport.rb +38 -9
- data/lib/datadog/core/telemetry/logger.rb +51 -0
- data/lib/datadog/core/telemetry/logging.rb +71 -0
- data/lib/datadog/core/telemetry/request.rb +13 -1
- data/lib/datadog/core/utils/at_fork_monkey_patch.rb +102 -0
- data/lib/datadog/core/utils/time.rb +12 -0
- data/lib/datadog/di/code_tracker.rb +168 -0
- data/lib/datadog/di/configuration/settings.rb +163 -0
- data/lib/datadog/di/configuration.rb +11 -0
- data/lib/datadog/di/error.rb +31 -0
- data/lib/datadog/di/extensions.rb +16 -0
- data/lib/datadog/di/probe.rb +133 -0
- data/lib/datadog/di/probe_builder.rb +41 -0
- data/lib/datadog/di/redactor.rb +188 -0
- data/lib/datadog/di/serializer.rb +193 -0
- data/lib/datadog/di.rb +14 -0
- data/lib/datadog/kit/appsec/events.rb +2 -4
- data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
- data/lib/datadog/opentelemetry/sdk/span_processor.rb +10 -0
- data/lib/datadog/opentelemetry/sdk/trace/span.rb +23 -0
- data/lib/datadog/profiling/collectors/code_provenance.rb +7 -7
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +28 -26
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +11 -13
- data/lib/datadog/profiling/collectors/info.rb +15 -6
- data/lib/datadog/profiling/collectors/thread_context.rb +30 -2
- data/lib/datadog/profiling/component.rb +89 -95
- data/lib/datadog/profiling/exporter.rb +3 -3
- data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
- data/lib/datadog/profiling/ext.rb +21 -21
- data/lib/datadog/profiling/flush.rb +1 -1
- data/lib/datadog/profiling/http_transport.rb +14 -7
- data/lib/datadog/profiling/load_native_extension.rb +5 -5
- data/lib/datadog/profiling/preload.rb +1 -1
- data/lib/datadog/profiling/profiler.rb +5 -8
- data/lib/datadog/profiling/scheduler.rb +33 -25
- data/lib/datadog/profiling/stack_recorder.rb +3 -0
- data/lib/datadog/profiling/tag_builder.rb +2 -2
- data/lib/datadog/profiling/tasks/exec.rb +5 -5
- data/lib/datadog/profiling/tasks/setup.rb +16 -35
- data/lib/datadog/profiling.rb +4 -5
- data/lib/datadog/single_step_instrument.rb +12 -0
- data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
- data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
- data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
- data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
- data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
- data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
- data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
- data/lib/datadog/tracing/contrib/active_record/events/sql.rb +4 -1
- data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
- data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
- data/lib/datadog/tracing/contrib/ext.rb +14 -0
- data/lib/datadog/tracing/contrib/faraday/middleware.rb +9 -0
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +19 -0
- data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
- data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
- data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
- data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +14 -10
- data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +10 -4
- data/lib/datadog/tracing/contrib/http/instrumentation.rb +18 -15
- data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +6 -5
- data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
- data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +5 -0
- data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
- data/lib/datadog/tracing/contrib/lograge/patcher.rb +15 -0
- data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
- data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +5 -0
- data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +17 -13
- data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
- data/lib/datadog/tracing/contrib/patcher.rb +2 -1
- data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +5 -0
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +4 -1
- data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
- data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +28 -0
- data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
- data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +22 -10
- data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
- data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
- data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
- data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
- data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +5 -0
- data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +4 -1
- data/lib/datadog/tracing/diagnostics/environment_logger.rb +14 -16
- data/lib/datadog/tracing/distributed/propagation.rb +7 -0
- data/lib/datadog/tracing/metadata/errors.rb +9 -1
- data/lib/datadog/tracing/metadata/ext.rb +6 -0
- data/lib/datadog/tracing/pipeline/span_filter.rb +2 -2
- data/lib/datadog/tracing/remote.rb +5 -2
- data/lib/datadog/tracing/sampling/matcher.rb +6 -1
- data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
- data/lib/datadog/tracing/sampling/rule.rb +2 -0
- data/lib/datadog/tracing/sampling/rule_sampler.rb +9 -5
- data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
- data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
- data/lib/datadog/tracing/span.rb +9 -2
- data/lib/datadog/tracing/span_event.rb +41 -0
- data/lib/datadog/tracing/span_operation.rb +6 -2
- data/lib/datadog/tracing/trace_operation.rb +26 -2
- data/lib/datadog/tracing/tracer.rb +14 -12
- data/lib/datadog/tracing/transport/http/client.rb +1 -0
- data/lib/datadog/tracing/transport/io/client.rb +1 -0
- data/lib/datadog/tracing/transport/serializable_trace.rb +3 -0
- data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
- data/lib/datadog/tracing/workers.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- metadata +46 -11
- data/lib/datadog/profiling/crashtracker.rb +0 -91
- data/lib/datadog/profiling/ext/forking.rb +0 -98
- data/lib/datadog/tracing/sampling/rate_limiter.rb +0 -185
@@ -10,10 +10,11 @@ module Datadog
|
|
10
10
|
# Core-pluggable component for AppSec
|
11
11
|
class Component
|
12
12
|
class << self
|
13
|
-
def build_appsec_component(settings)
|
14
|
-
return
|
13
|
+
def build_appsec_component(settings, telemetry:)
|
14
|
+
return if !settings.respond_to?(:appsec) || !settings.appsec.enabled
|
15
|
+
return if incompatible_ffi_version?
|
15
16
|
|
16
|
-
processor = create_processor(settings)
|
17
|
+
processor = create_processor(settings, telemetry)
|
17
18
|
|
18
19
|
# We want to always instrument user events when AppSec is enabled.
|
19
20
|
# There could be cases in which users use the DD_APPSEC_ENABLED Env variable to
|
@@ -28,8 +29,27 @@ module Datadog
|
|
28
29
|
|
29
30
|
private
|
30
31
|
|
31
|
-
def
|
32
|
-
|
32
|
+
def incompatible_ffi_version?
|
33
|
+
ffi_version = Gem.loaded_specs['ffi'] && Gem.loaded_specs['ffi'].version
|
34
|
+
return true unless ffi_version
|
35
|
+
|
36
|
+
return false unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') &&
|
37
|
+
ffi_version < Gem::Version.new('1.16.0')
|
38
|
+
|
39
|
+
Datadog.logger.warn(
|
40
|
+
'AppSec is not supported in Ruby versions above 3.3.0 when using `ffi` versions older than 1.16.0, ' \
|
41
|
+
'and will be forcibly disabled due to a memory leak in `ffi`. ' \
|
42
|
+
'Please upgrade your `ffi` version to 1.16.0 or higher.'
|
43
|
+
)
|
44
|
+
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_processor(settings, telemetry)
|
49
|
+
rules = AppSec::Processor::RuleLoader.load_rules(
|
50
|
+
telemetry: telemetry,
|
51
|
+
ruleset: settings.appsec.ruleset
|
52
|
+
)
|
33
53
|
return nil unless rules
|
34
54
|
|
35
55
|
actions = rules['actions']
|
@@ -47,9 +67,10 @@ module Datadog
|
|
47
67
|
rules: [rules],
|
48
68
|
data: data,
|
49
69
|
exclusions: exclusions,
|
70
|
+
telemetry: telemetry
|
50
71
|
)
|
51
72
|
|
52
|
-
processor = Processor.new(ruleset: ruleset)
|
73
|
+
processor = Processor.new(ruleset: ruleset, telemetry: telemetry)
|
53
74
|
return nil unless processor.ready?
|
54
75
|
|
55
76
|
processor
|
@@ -63,11 +84,11 @@ module Datadog
|
|
63
84
|
@mutex = Mutex.new
|
64
85
|
end
|
65
86
|
|
66
|
-
def reconfigure(ruleset:, actions:)
|
87
|
+
def reconfigure(ruleset:, actions:, telemetry:)
|
67
88
|
@mutex.synchronize do
|
68
89
|
AppSec::Processor::Actions.merge(actions)
|
69
90
|
|
70
|
-
new = Processor.new(ruleset: ruleset)
|
91
|
+
new = Processor.new(ruleset: ruleset, telemetry: telemetry)
|
71
92
|
|
72
93
|
if new && new.ready?
|
73
94
|
old = @processor
|
@@ -9,8 +9,8 @@ module Datadog
|
|
9
9
|
# Settings
|
10
10
|
module Settings
|
11
11
|
# rubocop:disable Layout/LineLength
|
12
|
-
DEFAULT_OBFUSCATOR_KEY_REGEX = '(?i)(?:
|
13
|
-
DEFAULT_OBFUSCATOR_VALUE_REGEX = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret
|
12
|
+
DEFAULT_OBFUSCATOR_KEY_REGEX = '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\.net[_-]sessionid|sid|jwt'
|
13
|
+
DEFAULT_OBFUSCATOR_VALUE_REGEX = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\.net(?:[_-]|-)sessionid|sid|jwt)(?:\s*=[^;]|"\s*:\s*"[^"]+")|bearer\s+[a-z0-9\._\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\w=-]+\.ey[I-L][\w=-]+(?:\.[\w.+\/=-]+)?|[\-]{5}BEGIN[a-z\s]+PRIVATE\sKEY[\-]{5}[^\-]+[\-]{5}END[a-z\s]+PRIVATE\sKEY|ssh-rsa\s*[a-z0-9\/\.+]{100,}'
|
14
14
|
# rubocop:enable Layout/LineLength
|
15
15
|
APPSEC_VALID_TRACK_USER_EVENTS_MODE = [
|
16
16
|
'safe',
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module AppSec
|
5
|
+
module Contrib
|
6
|
+
module Devise
|
7
|
+
module Patcher
|
8
|
+
# To avoid tracking new sessions that are created by
|
9
|
+
# Rememberable strategy as Login Success events.
|
10
|
+
module RememberablePatch
|
11
|
+
def validate(*args)
|
12
|
+
@_datadog_skip_track_login_event = true
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../patcher'
|
4
4
|
require_relative 'patcher/authenticatable_patch'
|
5
|
+
require_relative 'patcher/rememberable_patch'
|
5
6
|
require_relative 'patcher/registration_controller_patch'
|
6
7
|
|
7
8
|
module Datadog
|
@@ -23,16 +24,25 @@ module Datadog
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def patch
|
26
|
-
|
27
|
+
patch_authenticatable_strategy
|
28
|
+
patch_rememberable_strategy
|
27
29
|
patch_registration_controller
|
28
30
|
|
29
31
|
Patcher.instance_variable_set(:@patched, true)
|
30
32
|
end
|
31
33
|
|
32
|
-
def
|
34
|
+
def patch_authenticatable_strategy
|
33
35
|
::Devise::Strategies::Authenticatable.prepend(AuthenticatablePatch)
|
34
36
|
end
|
35
37
|
|
38
|
+
def patch_rememberable_strategy
|
39
|
+
return unless ::Devise::STRATEGIES.include?(:rememberable)
|
40
|
+
|
41
|
+
# Rememberable strategy is required in autoloaded Rememberable model
|
42
|
+
::Devise::Models::Rememberable # rubocop:disable Lint/Void
|
43
|
+
::Devise::Strategies::Rememberable.prepend(RememberablePatch)
|
44
|
+
end
|
45
|
+
|
36
46
|
def patch_registration_controller
|
37
47
|
::ActiveSupport.on_load(:after_initialize) do
|
38
48
|
::Devise::RegistrationsController.prepend(RegistrationControllerPatch)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require_relative 'gateway/multiplex'
|
5
|
+
require_relative '../../instrumentation/gateway'
|
6
|
+
|
7
|
+
module Datadog
|
8
|
+
module AppSec
|
9
|
+
module Contrib
|
10
|
+
module GraphQL
|
11
|
+
# These methods will be called by the GraphQL runtime to send the variables to the WAF.
|
12
|
+
# We actually don't need to create any span/trace.
|
13
|
+
module AppSecTrace
|
14
|
+
def execute_multiplex(multiplex:)
|
15
|
+
return super unless Datadog::AppSec.enabled?
|
16
|
+
|
17
|
+
gateway_multiplex = Gateway::Multiplex.new(multiplex)
|
18
|
+
|
19
|
+
multiplex_return, multiplex_response = Instrumentation.gateway.push('graphql.multiplex', gateway_multiplex) do
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns an error * the number of queries so that the entire multiplex is blocked
|
24
|
+
if multiplex_response
|
25
|
+
blocked_event = multiplex_response.find { |action, _options| action == :block }
|
26
|
+
multiplex_return = AppSec::Response.graphql_response(gateway_multiplex) if blocked_event
|
27
|
+
end
|
28
|
+
|
29
|
+
multiplex_return
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
require_relative '../../../instrumentation/gateway/argument'
|
6
|
+
|
7
|
+
module Datadog
|
8
|
+
module AppSec
|
9
|
+
module Contrib
|
10
|
+
module GraphQL
|
11
|
+
module Gateway
|
12
|
+
# Gateway Request argument. Normalized extration of data from Rack::Request
|
13
|
+
class Multiplex < Instrumentation::Gateway::Argument
|
14
|
+
attr_reader :multiplex
|
15
|
+
|
16
|
+
def initialize(multiplex)
|
17
|
+
super()
|
18
|
+
@multiplex = multiplex
|
19
|
+
end
|
20
|
+
|
21
|
+
def arguments
|
22
|
+
@arguments ||= build_arguments_hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def queries
|
26
|
+
@multiplex.queries
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# This method builds an array of argument hashes for each field with arguments in the query.
|
32
|
+
#
|
33
|
+
# For example, given the following query:
|
34
|
+
# query ($postSlug: ID = "my-first-post", $withComments: Boolean!) {
|
35
|
+
# firstPost: post(slug: $postSlug) {
|
36
|
+
# title
|
37
|
+
# comments @include(if: $withComments) {
|
38
|
+
# author { name }
|
39
|
+
# content
|
40
|
+
# }
|
41
|
+
# }
|
42
|
+
# }
|
43
|
+
#
|
44
|
+
# The result would be:
|
45
|
+
# {"post"=>[{"slug"=>"my-first-post"}], "comments"=>[{"include"=>{"if"=>true}}]}
|
46
|
+
#
|
47
|
+
# Note that the `comments` "include" directive is included in the arguments list
|
48
|
+
def build_arguments_hash
|
49
|
+
queries.each_with_object({}) do |query, args_hash|
|
50
|
+
next unless query.selected_operation
|
51
|
+
|
52
|
+
arguments_from_selections(query.selected_operation.selections, query.variables, args_hash)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def arguments_from_selections(selections, query_variables, args_hash)
|
57
|
+
selections.each do |selection|
|
58
|
+
# rubocop:disable Style/ClassEqualityComparison
|
59
|
+
next unless selection.class.name == Integration::AST_NODE_CLASS_NAMES[:field]
|
60
|
+
# rubocop:enable Style/ClassEqualityComparison
|
61
|
+
|
62
|
+
selection_name = selection.alias || selection.name
|
63
|
+
|
64
|
+
if !selection.arguments.empty? || !selection.directives.empty?
|
65
|
+
args_hash[selection_name] ||= []
|
66
|
+
args_hash[selection_name] <<
|
67
|
+
arguments_hash(selection.arguments, query_variables).merge!(
|
68
|
+
arguments_from_directives(selection.directives, query_variables)
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
arguments_from_selections(selection.selections, query_variables, args_hash)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def arguments_from_directives(directives, query_variables)
|
77
|
+
directives.each_with_object({}) do |directive, args_hash|
|
78
|
+
# rubocop:disable Style/ClassEqualityComparison
|
79
|
+
next unless directive.class.name == Integration::AST_NODE_CLASS_NAMES[:directive]
|
80
|
+
# rubocop:enable Style/ClassEqualityComparison
|
81
|
+
|
82
|
+
args_hash[directive.name] = arguments_hash(directive.arguments, query_variables)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def arguments_hash(arguments, query_variables)
|
87
|
+
arguments.each_with_object({}) do |argument, args_hash|
|
88
|
+
args_hash[argument.name] = argument_value(argument, query_variables)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def argument_value(argument, query_variables)
|
93
|
+
case argument.value.class.name
|
94
|
+
when Integration::AST_NODE_CLASS_NAMES[:variable_identifier]
|
95
|
+
# we need to pass query.variables here instead of query.provided_variables,
|
96
|
+
# since #provided_variables don't know anything about variable default value
|
97
|
+
query_variables[argument.value.name]
|
98
|
+
when Integration::AST_NODE_CLASS_NAMES[:input_object]
|
99
|
+
arguments_hash(argument.value.arguments, query_variables)
|
100
|
+
else
|
101
|
+
argument.value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require_relative '../../../instrumentation/gateway'
|
5
|
+
require_relative '../reactive/multiplex'
|
6
|
+
require_relative '../../../reactive/operation'
|
7
|
+
|
8
|
+
module Datadog
|
9
|
+
module AppSec
|
10
|
+
module Contrib
|
11
|
+
module GraphQL
|
12
|
+
module Gateway
|
13
|
+
# Watcher for Rack gateway events
|
14
|
+
module Watcher
|
15
|
+
class << self
|
16
|
+
def watch
|
17
|
+
gateway = Instrumentation.gateway
|
18
|
+
|
19
|
+
watch_multiplex(gateway)
|
20
|
+
end
|
21
|
+
|
22
|
+
# This time we don't throw but use next
|
23
|
+
def watch_multiplex(gateway = Instrumentation.gateway)
|
24
|
+
gateway.watch('graphql.multiplex', :appsec) do |stack, gateway_multiplex|
|
25
|
+
block = false
|
26
|
+
event = nil
|
27
|
+
|
28
|
+
scope = AppSec::Scope.active_scope
|
29
|
+
|
30
|
+
if scope
|
31
|
+
AppSec::Reactive::Operation.new('graphql.multiplex') do |op|
|
32
|
+
GraphQL::Reactive::Multiplex.subscribe(op, scope.processor_context) do |result|
|
33
|
+
event = {
|
34
|
+
waf_result: result,
|
35
|
+
trace: scope.trace,
|
36
|
+
span: scope.service_entry_span,
|
37
|
+
multiplex: gateway_multiplex,
|
38
|
+
actions: result.actions
|
39
|
+
}
|
40
|
+
|
41
|
+
if scope.service_entry_span
|
42
|
+
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
|
43
|
+
scope.service_entry_span.set_tag('appsec.event', 'true')
|
44
|
+
end
|
45
|
+
|
46
|
+
scope.processor_context.events << event
|
47
|
+
end
|
48
|
+
|
49
|
+
block = GraphQL::Reactive::Multiplex.publish(op, gateway_multiplex)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
next [nil, [[:block, event]]] if block
|
54
|
+
|
55
|
+
ret, res = stack.call(gateway_multiplex.arguments)
|
56
|
+
|
57
|
+
if event
|
58
|
+
res ||= []
|
59
|
+
res << [:monitor, event]
|
60
|
+
end
|
61
|
+
|
62
|
+
[ret, res]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../integration'
|
4
|
+
require_relative 'patcher'
|
5
|
+
|
6
|
+
module Datadog
|
7
|
+
module AppSec
|
8
|
+
module Contrib
|
9
|
+
module GraphQL
|
10
|
+
# Description of GraphQL integration
|
11
|
+
class Integration
|
12
|
+
include Datadog::AppSec::Contrib::Integration
|
13
|
+
|
14
|
+
MINIMUM_VERSION = Gem::Version.new('2.0.19')
|
15
|
+
|
16
|
+
AST_NODE_CLASS_NAMES = {
|
17
|
+
field: 'GraphQL::Language::Nodes::Field',
|
18
|
+
directive: 'GraphQL::Language::Nodes::Directive',
|
19
|
+
variable_identifier: 'GraphQL::Language::Nodes::VariableIdentifier',
|
20
|
+
input_object: 'GraphQL::Language::Nodes::InputObject',
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
register_as :graphql, auto_patch: false
|
24
|
+
|
25
|
+
def self.version
|
26
|
+
Gem.loaded_specs['graphql'] && Gem.loaded_specs['graphql'].version
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.loaded?
|
30
|
+
!defined?(::GraphQL).nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.compatible?
|
34
|
+
super && version >= MINIMUM_VERSION && ast_node_classes_defined?
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.auto_instrument?
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.ast_node_classes_defined?
|
42
|
+
AST_NODE_CLASS_NAMES.all? do |_, class_name|
|
43
|
+
Object.const_defined?(class_name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def patcher
|
48
|
+
Patcher
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../patcher'
|
4
|
+
require_relative 'gateway/watcher'
|
5
|
+
|
6
|
+
if Gem.loaded_specs['graphql'] && Gem.loaded_specs['graphql'].version >= Gem::Version.new('2.0.19')
|
7
|
+
require_relative 'appsec_trace'
|
8
|
+
end
|
9
|
+
|
10
|
+
module Datadog
|
11
|
+
module AppSec
|
12
|
+
module Contrib
|
13
|
+
module GraphQL
|
14
|
+
# Patcher for AppSec on GraphQL
|
15
|
+
module Patcher
|
16
|
+
include Datadog::AppSec::Contrib::Patcher
|
17
|
+
|
18
|
+
module_function
|
19
|
+
|
20
|
+
def patched?
|
21
|
+
Patcher.instance_variable_get(:@patched)
|
22
|
+
end
|
23
|
+
|
24
|
+
def target_version
|
25
|
+
Integration.version
|
26
|
+
end
|
27
|
+
|
28
|
+
def patch
|
29
|
+
Gateway::Watcher.watch
|
30
|
+
::GraphQL::Schema.trace_with(AppSecTrace)
|
31
|
+
Patcher.instance_variable_set(:@patched, true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module AppSec
|
5
|
+
module Contrib
|
6
|
+
module GraphQL
|
7
|
+
module Reactive
|
8
|
+
# Dispatch data from a GraphQL resolve query to the WAF context
|
9
|
+
module Multiplex
|
10
|
+
ADDRESSES = [
|
11
|
+
'graphql.server.all_resolvers'
|
12
|
+
].freeze
|
13
|
+
private_constant :ADDRESSES
|
14
|
+
|
15
|
+
def self.publish(op, gateway_multiplex)
|
16
|
+
catch(:block) do
|
17
|
+
op.publish('graphql.server.all_resolvers', gateway_multiplex.arguments)
|
18
|
+
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.subscribe(op, waf_context)
|
24
|
+
op.subscribe(*ADDRESSES) do |*values|
|
25
|
+
Datadog.logger.debug { "reacted to #{ADDRESSES.inspect}: #{values.inspect}" }
|
26
|
+
arguments = values[0]
|
27
|
+
|
28
|
+
waf_args = {
|
29
|
+
'graphql.server.all_resolvers' => arguments
|
30
|
+
}
|
31
|
+
|
32
|
+
waf_timeout = Datadog.configuration.appsec.waf_timeout
|
33
|
+
result = waf_context.run(waf_args, waf_timeout)
|
34
|
+
|
35
|
+
Datadog.logger.debug { "WAF TIMEOUT: #{result.inspect}" } if result.timeout
|
36
|
+
|
37
|
+
case result.status
|
38
|
+
when :match
|
39
|
+
Datadog.logger.debug { "WAF: #{result.inspect}" }
|
40
|
+
|
41
|
+
yield result
|
42
|
+
throw(:block, true) unless result.actions.empty?
|
43
|
+
when :ok
|
44
|
+
Datadog.logger.debug { "WAF OK: #{result.inspect}" }
|
45
|
+
when :invalid_call
|
46
|
+
Datadog.logger.debug { "WAF CALL ERROR: #{result.inspect}" }
|
47
|
+
when :invalid_rule, :invalid_flow, :no_rule
|
48
|
+
Datadog.logger.debug { "WAF RULE ERROR: #{result.inspect}" }
|
49
|
+
else
|
50
|
+
Datadog.logger.debug { "WAF UNKNOWN: #{result.status.inspect} #{result.inspect}" }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -41,18 +41,15 @@ module Datadog
|
|
41
41
|
|
42
42
|
def headers
|
43
43
|
result = request.env.each_with_object({}) do |(k, v), h|
|
44
|
-
h[k.
|
44
|
+
h[k.delete_prefix('HTTP_').tap(&:downcase!).tap { |s| s.tr!('_', '-') }] = v if k.start_with?('HTTP_')
|
45
45
|
end
|
46
46
|
|
47
47
|
result['content-type'] = request.content_type if request.content_type
|
48
|
-
|
48
|
+
# Since Rack 3.1, content-length is nil if the body is empty, but we still want to send it to the WAF.
|
49
|
+
result['content-length'] = request.content_length || '0'
|
49
50
|
result
|
50
51
|
end
|
51
52
|
|
52
|
-
def body
|
53
|
-
request.body.read.tap { request.body.rewind }
|
54
|
-
end
|
55
|
-
|
56
53
|
def url
|
57
54
|
request.url
|
58
55
|
end
|
data/lib/datadog/appsec/event.rb
CHANGED
@@ -52,7 +52,7 @@ module Datadog
|
|
52
52
|
# ensure rate limiter is called only when there are events to record
|
53
53
|
return if events.empty? || span.nil?
|
54
54
|
|
55
|
-
Datadog::AppSec::RateLimiter.limit
|
55
|
+
Datadog::AppSec::RateLimiter.thread_local.limit do
|
56
56
|
record_via_span(span, *events)
|
57
57
|
end
|
58
58
|
end
|
@@ -9,7 +9,7 @@ module Datadog
|
|
9
9
|
# that load appsec rules and data from settings
|
10
10
|
module RuleLoader
|
11
11
|
class << self
|
12
|
-
def load_rules(ruleset:)
|
12
|
+
def load_rules(ruleset:, telemetry:)
|
13
13
|
begin
|
14
14
|
case ruleset
|
15
15
|
when :recommended, :strict
|
@@ -35,6 +35,8 @@ module Datadog
|
|
35
35
|
"libddwaf ruleset failed to load, ruleset: #{ruleset.inspect} error: #{e.inspect}"
|
36
36
|
end
|
37
37
|
|
38
|
+
telemetry.report(e, description: 'libddwaf ruleset failed to load')
|
39
|
+
|
38
40
|
nil
|
39
41
|
end
|
40
42
|
end
|
@@ -18,25 +18,35 @@ module Datadog
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
DEFAULT_WAF_PROCESSORS = begin
|
22
|
-
JSON.parse(Datadog::AppSec::Assets.waf_processors)
|
23
|
-
rescue StandardError => e
|
24
|
-
Datadog.logger.error { "libddwaf rulemerger failed to parse default waf processors. Error: #{e.inspect}" }
|
25
|
-
[]
|
26
|
-
end
|
27
|
-
|
28
|
-
DEFAULT_WAF_SCANNERS = begin
|
29
|
-
JSON.parse(Datadog::AppSec::Assets.waf_scanners)
|
30
|
-
rescue StandardError => e
|
31
|
-
Datadog.logger.error { "libddwaf rulemerger failed to parse default waf scanners. Error: #{e.inspect}" }
|
32
|
-
[]
|
33
|
-
end
|
34
|
-
|
35
21
|
class << self
|
22
|
+
# TODO: `processors` and `scanners` are not provided by the caller, consider removing them
|
36
23
|
def merge(
|
24
|
+
telemetry:,
|
37
25
|
rules:, data: [], overrides: [], exclusions: [], custom_rules: [],
|
38
|
-
processors:
|
26
|
+
processors: nil, scanners: nil
|
39
27
|
)
|
28
|
+
processors ||= begin
|
29
|
+
default_waf_processors
|
30
|
+
rescue StandardError => e
|
31
|
+
Datadog.logger.error("libddwaf rulemerger failed to parse default waf processors. Error: #{e.inspect}")
|
32
|
+
telemetry.report(
|
33
|
+
e,
|
34
|
+
description: 'libddwaf rulemerger failed to parse default waf processors'
|
35
|
+
)
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
|
39
|
+
scanners ||= begin
|
40
|
+
default_waf_scanners
|
41
|
+
rescue StandardError => e
|
42
|
+
Datadog.logger.error("libddwaf rulemerger failed to parse default waf scanners. Error: #{e.inspect}")
|
43
|
+
telemetry.report(
|
44
|
+
e,
|
45
|
+
description: 'libddwaf rulemerger failed to parse default waf scanners'
|
46
|
+
)
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
|
40
50
|
combined_rules = combine_rules(rules)
|
41
51
|
|
42
52
|
combined_data = combine_data(data) if data.any?
|
@@ -53,6 +63,14 @@ module Datadog
|
|
53
63
|
combined_rules
|
54
64
|
end
|
55
65
|
|
66
|
+
def default_waf_processors
|
67
|
+
@default_waf_processors ||= JSON.parse(Datadog::AppSec::Assets.waf_processors)
|
68
|
+
end
|
69
|
+
|
70
|
+
def default_waf_scanners
|
71
|
+
@default_waf_scanners ||= JSON.parse(Datadog::AppSec::Assets.waf_scanners)
|
72
|
+
end
|
73
|
+
|
56
74
|
private
|
57
75
|
|
58
76
|
def combine_rules(rules)
|