datadog 2.20.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -1
  3. data/README.md +0 -1
  4. data/ext/LIBDATADOG_DEVELOPMENT.md +60 -0
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  6. data/ext/libdatadog_api/ddsketch.c +106 -0
  7. data/ext/libdatadog_api/init.c +3 -0
  8. data/ext/libdatadog_api/library_config.c +35 -27
  9. data/ext/libdatadog_api/process_discovery.c +24 -18
  10. data/ext/libdatadog_extconf_helpers.rb +1 -1
  11. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +26 -0
  12. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +59 -0
  13. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +29 -0
  14. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +26 -0
  15. data/lib/datadog/appsec/api_security/endpoint_collection.rb +10 -0
  16. data/lib/datadog/appsec/api_security/route_extractor.rb +6 -2
  17. data/lib/datadog/appsec/assets/waf_rules/README.md +30 -36
  18. data/lib/datadog/appsec/assets/waf_rules/recommended.json +359 -4
  19. data/lib/datadog/appsec/assets/waf_rules/strict.json +43 -2
  20. data/lib/datadog/appsec/autoload.rb +1 -1
  21. data/lib/datadog/appsec/compressed_json.rb +1 -1
  22. data/lib/datadog/appsec/configuration/settings.rb +9 -0
  23. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +3 -1
  24. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +3 -2
  25. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +3 -1
  26. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +3 -1
  27. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -4
  28. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +5 -1
  29. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +7 -2
  30. data/lib/datadog/appsec/contrib/rails/patcher.rb +30 -0
  31. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +3 -1
  32. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +10 -4
  33. data/lib/datadog/appsec/event.rb +12 -14
  34. data/lib/datadog/appsec/metrics/collector.rb +19 -3
  35. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +2 -1
  36. data/lib/datadog/appsec/monitor/gateway/watcher.rb +4 -4
  37. data/lib/datadog/appsec/remote.rb +25 -13
  38. data/lib/datadog/appsec/security_engine/result.rb +28 -9
  39. data/lib/datadog/appsec/security_engine/runner.rb +17 -7
  40. data/lib/datadog/appsec/security_event.rb +5 -7
  41. data/lib/datadog/core/configuration/agent_settings_resolver.rb +4 -4
  42. data/lib/datadog/core/configuration/components.rb +22 -8
  43. data/lib/datadog/core/configuration/config_helper.rb +100 -0
  44. data/lib/datadog/core/configuration/deprecations.rb +36 -0
  45. data/lib/datadog/core/configuration/ext.rb +0 -1
  46. data/lib/datadog/core/configuration/option.rb +38 -43
  47. data/lib/datadog/core/configuration/option_definition.rb +0 -9
  48. data/lib/datadog/core/configuration/options.rb +1 -5
  49. data/lib/datadog/core/configuration/settings.rb +10 -6
  50. data/lib/datadog/core/configuration/stable_config.rb +10 -0
  51. data/lib/datadog/core/configuration/supported_configurations.rb +337 -0
  52. data/lib/datadog/core/configuration.rb +2 -2
  53. data/lib/datadog/core/ddsketch.rb +21 -0
  54. data/lib/datadog/core/deprecations.rb +2 -2
  55. data/lib/datadog/core/environment/ext.rb +0 -2
  56. data/lib/datadog/core/environment/git.rb +2 -2
  57. data/lib/datadog/core/environment/variable_helpers.rb +3 -3
  58. data/lib/datadog/core/environment/yjit.rb +2 -1
  59. data/lib/datadog/core/metrics/client.rb +2 -2
  60. data/lib/datadog/core/pin.rb +4 -8
  61. data/lib/datadog/core/process_discovery/tracer_memfd.rb +2 -4
  62. data/lib/datadog/core/process_discovery.rb +48 -23
  63. data/lib/datadog/core/remote/component.rb +4 -6
  64. data/lib/datadog/core/runtime/ext.rb +0 -1
  65. data/lib/datadog/core/telemetry/component.rb +11 -0
  66. data/lib/datadog/core/telemetry/emitter.rb +6 -6
  67. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +30 -0
  68. data/lib/datadog/core/telemetry/event/app_started.rb +2 -2
  69. data/lib/datadog/core/telemetry/event.rb +1 -0
  70. data/lib/datadog/core/transport/response.rb +4 -1
  71. data/lib/datadog/core/utils/network.rb +19 -0
  72. data/lib/datadog/core.rb +2 -0
  73. data/lib/datadog/di/boot.rb +5 -3
  74. data/lib/datadog/di/component.rb +14 -0
  75. data/lib/datadog/di/context.rb +70 -0
  76. data/lib/datadog/di/el/compiler.rb +164 -0
  77. data/lib/datadog/di/el/evaluator.rb +159 -0
  78. data/lib/datadog/di/el/expression.rb +42 -0
  79. data/lib/datadog/di/el.rb +5 -0
  80. data/lib/datadog/di/error.rb +25 -0
  81. data/lib/datadog/di/instrumenter.rb +101 -32
  82. data/lib/datadog/di/probe.rb +35 -15
  83. data/lib/datadog/di/probe_builder.rb +39 -1
  84. data/lib/datadog/di/probe_file_loader.rb +1 -1
  85. data/lib/datadog/di/probe_manager.rb +3 -2
  86. data/lib/datadog/di/probe_notification_builder.rb +50 -51
  87. data/lib/datadog/di/serializer.rb +151 -7
  88. data/lib/datadog/opentelemetry/sdk/configurator.rb +1 -1
  89. data/lib/datadog/profiling/collectors/info.rb +1 -1
  90. data/lib/datadog/profiling/ext.rb +2 -1
  91. data/lib/datadog/profiling/http_transport.rb +1 -1
  92. data/lib/datadog/profiling/tasks/exec.rb +2 -2
  93. data/lib/datadog/tracing/component.rb +6 -17
  94. data/lib/datadog/tracing/configuration/dynamic.rb +2 -2
  95. data/lib/datadog/tracing/configuration/ext.rb +0 -3
  96. data/lib/datadog/tracing/configuration/settings.rb +15 -10
  97. data/lib/datadog/tracing/contrib/component.rb +2 -2
  98. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +7 -0
  99. data/lib/datadog/tracing/contrib/graphql/ext.rb +1 -0
  100. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +63 -27
  101. data/lib/datadog/tracing/contrib/rack/request_queue.rb +1 -0
  102. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +7 -1
  103. data/lib/datadog/tracing/contrib/rails/ext.rb +2 -1
  104. data/lib/datadog/tracing/contrib/rails/integration.rb +1 -1
  105. data/lib/datadog/tracing/contrib/span_attribute_schema.rb +1 -1
  106. data/lib/datadog/tracing/metadata/ext.rb +8 -0
  107. data/lib/datadog/version.rb +1 -1
  108. metadata +25 -9
  109. data/ext/libdatadog_api/macos_development.md +0 -26
@@ -31,15 +31,15 @@ module Datadog
31
31
  seq_id = self.class.sequence.next
32
32
  payload = Request.build_payload(event, seq_id, debug: debug?)
33
33
  res = @transport.send_telemetry(request_type: event.type, payload: payload)
34
- logger.debug { "Telemetry sent for event `#{event.type}` (response code: #{res.code})" }
34
+ if res.ok?
35
+ logger.debug { "Telemetry sent for event `#{event.type}`" }
36
+ else
37
+ logger.debug { "Failed to send telemetry for event `#{event.type}`: #{res.inspect}" }
38
+ end
35
39
  res
36
40
  rescue => e
37
41
  logger.debug {
38
- "Unable to send telemetry request for event `#{begin
39
- event.type
40
- rescue
41
- "unknown"
42
- end}`: #{e}"
42
+ "Unable to send telemetry request for event `#{event.respond_to?(:type) ? event.type : event.to_s}`: #{e.class}: #{e}"
43
43
  }
44
44
  Core::Transport::InternalErrorResponse.new(e)
45
45
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Datadog
6
+ module Core
7
+ module Telemetry
8
+ module Event
9
+ # Telemetry event class for sending 'app-endpoints' payload
10
+ class AppEndpointsLoaded < Base
11
+ def initialize(endpoints, is_first:)
12
+ @endpoints = endpoints
13
+ @is_first = !!is_first
14
+ end
15
+
16
+ def type
17
+ 'app-endpoints'
18
+ end
19
+
20
+ def payload
21
+ {
22
+ is_first: @is_first,
23
+ endpoints: @endpoints
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -100,7 +100,7 @@ module Datadog
100
100
  ),
101
101
 
102
102
  # Mix of env var, programmatic and default config, so we use unknown
103
- conf_value('DD_AGENT_TRANSPORT', agent_transport, seq_id, 'unknown'),
103
+ conf_value('DD_AGENT_TRANSPORT', agent_transport, seq_id, 'unknown'), # rubocop:disable CustomCops/EnvStringValidationCop
104
104
 
105
105
  # writer_options is defined as an option that has a Hash value.
106
106
  conf_value(
@@ -177,7 +177,7 @@ module Datadog
177
177
  list.push(
178
178
  conf_value('instrumentation_source', instrumentation_source, seq_id, 'code'),
179
179
  conf_value('DD_INJECT_FORCE', Core::Environment::VariableHelpers.env_to_bool('DD_INJECT_FORCE', false), seq_id, 'env_var'),
180
- conf_value('DD_INJECTION_ENABLED', ENV['DD_INJECTION_ENABLED'] || '', seq_id, 'env_var'),
180
+ conf_value('DD_INJECTION_ENABLED', DATADOG_ENV['DD_INJECTION_ENABLED'] || '', seq_id, 'env_var'),
181
181
  )
182
182
 
183
183
  # Add some more custom additional payload values here
@@ -26,6 +26,7 @@ require_relative 'event/base'
26
26
  require_relative 'event/app_client_configuration_change'
27
27
  require_relative 'event/app_closing'
28
28
  require_relative 'event/app_dependencies_loaded'
29
+ require_relative 'event/app_endpoints_loaded'
29
30
  require_relative 'event/app_heartbeat'
30
31
  require_relative 'event/app_integrations_change'
31
32
  require_relative 'event/app_started'
@@ -34,7 +34,10 @@ module Datadog
34
34
  end
35
35
 
36
36
  def inspect
37
- "#{self.class} ok?:#{ok?} unsupported?:#{unsupported?}, " \
37
+ maybe_code = if respond_to?(:code)
38
+ " code:#{code}," # steep:ignore
39
+ end
40
+ "#{self.class} ok?:#{ok?},#{maybe_code} unsupported?:#{unsupported?}, " \
38
41
  "not_found?:#{not_found?}, client_error?:#{client_error?}, " \
39
42
  "server_error?:#{server_error?}, internal_error?:#{internal_error?}, " \
40
43
  "payload:#{payload}"
@@ -13,6 +13,7 @@ module Datadog
13
13
  true-client-ip
14
14
  x-client-ip
15
15
  x-forwarded
16
+ forwarded
16
17
  forwarded-for
17
18
  x-cluster-client-ip
18
19
  fastly-client-ip
@@ -73,6 +74,8 @@ module Datadog
73
74
  next unless value
74
75
 
75
76
  ips = value.split(',')
77
+ ips = process_forwarded_header_values(ips) if name == 'forwarded'
78
+
76
79
  ips.each do |ip|
77
80
  parsed_ip = ip_to_ipaddr(ip.strip)
78
81
 
@@ -83,6 +86,22 @@ module Datadog
83
86
  nil
84
87
  end
85
88
 
89
+ def process_forwarded_header_values(values)
90
+ values.each_with_object([]) do |value, acc|
91
+ value.downcase!
92
+
93
+ value.split(';').each do |tuple_str|
94
+ tuple_str.strip!
95
+ next unless tuple_str.start_with?('for=')
96
+
97
+ tuple_str.delete_prefix!('for=')
98
+ tuple_str.delete!('"')
99
+
100
+ acc << tuple_str
101
+ end
102
+ end
103
+ end
104
+
86
105
  # Returns whether the given value is more likely to be an IPv4 than an IPv6 address.
87
106
  #
88
107
  # This is done by checking if a dot (`'.'`) character appears before a colon (`':'`) in the value.
data/lib/datadog/core.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'core/deprecations'
4
+ require_relative 'core/configuration/config_helper'
4
5
  require_relative 'core/extensions'
5
6
 
6
7
  # We must load core extensions to make certain global APIs
@@ -21,6 +22,7 @@ module Datadog
21
22
  end
22
23
  end
23
24
 
25
+ DATADOG_ENV = Core::Configuration::ConfigHelper.new
24
26
  extend Core::Extensions
25
27
 
26
28
  # Add shutdown hook:
@@ -5,6 +5,7 @@ require_relative 'base'
5
5
  require_relative 'error'
6
6
  require_relative 'code_tracker'
7
7
  require_relative 'component'
8
+ require_relative 'context'
8
9
  require_relative 'instrumenter'
9
10
  require_relative 'probe'
10
11
  require_relative 'probe_builder'
@@ -16,7 +17,8 @@ require_relative 'serializer'
16
17
  require_relative 'transport/http'
17
18
  require_relative 'utils'
18
19
 
19
- if %w[1 true yes].include?(ENV['DD_DYNAMIC_INSTRUMENTATION_ENABLED']) # steep:ignore
20
+ if %w[1 true yes].include?(Datadog::DATADOG_ENV['DD_DYNAMIC_INSTRUMENTATION_ENABLED']) # steep:ignore
21
+
20
22
  # For initial release of Dynamic Instrumentation, activate code tracking
21
23
  # only if DI is explicitly requested in the environment.
22
24
  # Code tracking is required for line probes to work; see the comments
@@ -33,8 +35,8 @@ require_relative 'contrib'
33
35
 
34
36
  Datadog::DI::Contrib.load_now_or_later
35
37
 
36
- if %w[1 true yes].include?(ENV['DD_DYNAMIC_INSTRUMENTATION_ENABLED']) # steep:ignore
37
- if ENV['DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE']
38
+ if %w[1 true yes].include?(Datadog::DATADOG_ENV['DD_DYNAMIC_INSTRUMENTATION_ENABLED']) # steep:ignore
39
+ if Datadog::DATADOG_ENV['DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE']
38
40
  require_relative 'probe_file_loader'
39
41
  Datadog::DI::ProbeFileLoader.load_now_or_later
40
42
  end
@@ -115,6 +115,20 @@ module Datadog
115
115
 
116
116
  def parse_probe_spec_and_notify(probe_spec)
117
117
  probe = ProbeBuilder.build_from_remote_config(probe_spec)
118
+ rescue => exc
119
+ begin
120
+ probe = Struct.new(:id).new(
121
+ probe_spec['id'],
122
+ )
123
+ payload = probe_notification_builder.build_errored(probe, exc)
124
+ probe_notifier_worker.add_status(payload)
125
+ rescue # standard:disable Lint/UselessRescue
126
+ # TODO report via instrumentation telemetry?
127
+ raise
128
+ end
129
+
130
+ raise
131
+ else
118
132
  payload = probe_notification_builder.build_received(probe)
119
133
  probe_notifier_worker.add_status(payload)
120
134
  probe
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # Contains local and instance variables used when evaluating
6
+ # expressions in DI Expression Language.
7
+ #
8
+ # @api private
9
+ class Context
10
+ def initialize(probe:, settings:, serializer:, locals: nil,
11
+ # In Ruby everything is a method, therefore we should always have
12
+ # a target self. However, if we are not capturing a snapshot,
13
+ # there is no need to pass in the target self.
14
+ target_self: nil,
15
+ path: nil, caller_locations: nil,
16
+ serialized_entry_args: nil,
17
+ return_value: nil, duration: nil, exception: nil)
18
+ @probe = probe
19
+ @settings = settings
20
+ @serializer = serializer
21
+ @locals = locals
22
+ @target_self = target_self
23
+ @path = path
24
+ @caller_locations = caller_locations
25
+ @serialized_entry_args = serialized_entry_args
26
+ @return_value = return_value
27
+ @duration = duration
28
+ @exception = exception
29
+ end
30
+
31
+ attr_reader :probe
32
+ attr_reader :settings
33
+ attr_reader :serializer
34
+ attr_reader :locals
35
+ attr_reader :target_self
36
+ # Actual path of the instrumented file.
37
+ attr_reader :path
38
+ # TODO check how many stack frames we should be keeping/sending,
39
+ # this should be all frames for enriched probes and no frames for
40
+ # non-enriched probes?
41
+ attr_reader :caller_locations
42
+ attr_reader :serialized_entry_args
43
+ # Return value for the method, for a method probe
44
+ attr_reader :return_value
45
+ # How long the method took to execute, for a method probe
46
+ attr_reader :duration
47
+ # Exception raised by the method, if any, for a method probe
48
+ attr_reader :exception
49
+
50
+ def serialized_locals
51
+ # TODO cache?
52
+ locals && serializer.serialize_vars(locals,
53
+ depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
54
+ attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)
55
+ end
56
+
57
+ def fetch(var_name)
58
+ unless locals
59
+ # TODO return "undefined" instead?
60
+ return nil
61
+ end
62
+ locals[var_name.to_sym]
63
+ end
64
+
65
+ def fetch_ivar(var_name)
66
+ target_self.instance_variable_get(var_name)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # DI Expression Language compiler.
7
+ #
8
+ # Converts AST in probe definitions into Expression objects.
9
+ #
10
+ # WARNING: this class produces strings that are then eval'd as
11
+ # Ruby code. Input ASTs are user-controlled. As such the compiler
12
+ # must sanitize and escape all input to avoid injection.
13
+ #
14
+ # Besides quotes and backslashes we must also escape # which is
15
+ # starting string interpolation (#{...}).
16
+ #
17
+ # @api private
18
+ class Compiler
19
+ def compile(ast)
20
+ compile_partial(ast)
21
+ end
22
+
23
+ private
24
+
25
+ OPERATORS = {
26
+ 'eq' => '==',
27
+ 'ne' => '!=',
28
+ 'ge' => '>=',
29
+ 'gt' => '>',
30
+ 'le' => '<=',
31
+ 'lt' => '<',
32
+ }.freeze
33
+
34
+ SINGLE_ARG_METHODS = %w[
35
+ len isEmpty isUndefined
36
+ ].freeze
37
+
38
+ TWO_ARG_METHODS = %w[
39
+ startsWith endsWith contains matches
40
+ getmember index instanceof
41
+ ].freeze
42
+
43
+ MULTI_ARG_METHODS = {
44
+ 'and' => '&&',
45
+ 'or' => '||',
46
+ }.freeze
47
+
48
+ def compile_partial(ast)
49
+ case ast
50
+ when Hash
51
+ if ast.length != 1
52
+ raise DI::Error::InvalidExpression, "Expected hash of length 1: #{ast}"
53
+ end
54
+ op, target = ast.first
55
+ case op
56
+ when 'ref'
57
+ unless String === target
58
+ raise DI::Error::InvalidExpression, "Bad ref value type: #{target.class}: #{target}"
59
+ end
60
+ case target
61
+ when '@it'
62
+ 'current_item'
63
+ when '@key'
64
+ 'current_key'
65
+ when '@value'
66
+ 'current_value'
67
+ when '@return'
68
+ # For @return, @duration and @exception we shadow
69
+ # instance variables.
70
+ "context.return_value"
71
+ when '@duration'
72
+ # There is no way to explicitly format the duration.
73
+ # TODO come up with better formatting?
74
+ # We could format to a string here but what if customer
75
+ # has @duration as part of an expression and wants
76
+ # to retain it as a number?
77
+ "(context.duration * 1000)"
78
+ when '@exception'
79
+ "context.exception"
80
+ else
81
+ # Ruby technically allows all kinds of symbols in variable
82
+ # names, for example spaces and many characters.
83
+ # Start out with strict validation to avoid possible
84
+ # surprises and need to escape.
85
+ unless target =~ %r{\A(@?)([a-zA-Z0-9_]+)\z}
86
+ raise DI::Error::BadVariableName, "Bad variable name: #{target}"
87
+ end
88
+ method_name = (($1 == '@') ? 'iref' : 'ref')
89
+ "#{method_name}('#{target}')"
90
+ end
91
+ when *SINGLE_ARG_METHODS
92
+ method_name = op.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }
93
+ "#{method_name}(#{compile_partial(target)}, '#{var_name_maybe(target)}')"
94
+ when *TWO_ARG_METHODS
95
+ method_name = op.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }
96
+ unless Array === target && target.length == 2
97
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
98
+ end
99
+ first, second = target
100
+ "#{method_name}(#{compile_partial(first)}, (#{compile_partial(second)}))"
101
+ when *MULTI_ARG_METHODS.keys
102
+ unless Array === target && target.length >= 1
103
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
104
+ end
105
+ compiled_targets = target.map do |item|
106
+ "(#{compile_partial(item)})"
107
+ end
108
+ compiled_op = MULTI_ARG_METHODS[op]
109
+ "(#{compiled_targets.join(" #{compiled_op} ")})"
110
+ when 'substring'
111
+ unless Array === target && target.length == 3
112
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
113
+ end
114
+ "#{op}(#{target.map { |arg| "(#{compile_partial(arg)})" }.join(", ")})"
115
+ when 'not'
116
+ "!(#{compile_partial(target)})"
117
+ when *OPERATORS.keys
118
+ unless Array === target && target.length == 2
119
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
120
+ end
121
+ first, second = target
122
+ operator = OPERATORS.fetch(op)
123
+ "(#{compile_partial(first)}) #{operator} (#{compile_partial(second)})"
124
+ when 'any', 'all', 'filter'
125
+ "#{op}(#{compile_partial(target.first)}) { |current_item, current_key, current_value| #{compile_partial(target.last)} }"
126
+ else
127
+ raise DI::Error::InvalidExpression, "Unknown operation: #{op}"
128
+ end
129
+ when Numeric, true, false, nil
130
+ # No escaping is needed for the values here.
131
+ ast.inspect
132
+ when String
133
+ "\"#{escape(ast)}\""
134
+ when Array
135
+ # Arrays are commonly used as arguments of operators/methods,
136
+ # but there are no arrays at the top level in the syntax that
137
+ # we currently understand. Provide a helpful error message in case
138
+ # syntax is expanded in the future.
139
+ raise DI::Error::InvalidExpression, "Array is not valid at its location, do you need to upgrade dd-trace-rb? #{ast}"
140
+ else
141
+ raise DI::Error::InvalidExpression, "Unknown type in AST: #{ast}"
142
+ end
143
+ end
144
+
145
+ # Returns a textual description of +target+ for use in exception
146
+ # messages. +target+ could be any expression language expression.
147
+ # WARNING: the result of this method is included in eval'd code,
148
+ # it must be sanitized to avoid injection.
149
+ def var_name_maybe(target)
150
+ if Hash === target && target.length == 1 && target.keys.first == 'ref' &&
151
+ String === (value = target.values.first)
152
+ escape(value)
153
+ else
154
+ '(expression)'
155
+ end
156
+ end
157
+
158
+ def escape(needle)
159
+ needle.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub('#') { "\\#" }
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # Evaluator for expression language.
7
+ #
8
+ # @api private
9
+ class Evaluator
10
+ def ref(var)
11
+ @context.fetch(var)
12
+ end
13
+
14
+ def iref(var)
15
+ @context.fetch_ivar(var)
16
+ end
17
+
18
+ def len(var, var_name)
19
+ case var
20
+ when Array, String
21
+ var.length
22
+ else
23
+ raise DI::Error::ExpressionEvaluationError, "Unsupported type for length: #{var.class}: #{var_name}"
24
+ end
25
+ end
26
+
27
+ def is_empty(var, var_name)
28
+ case var
29
+ when nil, Numeric
30
+ false
31
+ when Array, String
32
+ var.empty?
33
+ else
34
+ raise DI::Error::ExpressionEvaluationError, "Unsupported type for isEmpty: #{var.class}: #{var_name}"
35
+ end
36
+ end
37
+
38
+ def is_undefined(var, var_name)
39
+ var.nil?
40
+ end
41
+
42
+ def contains(haystack, needle)
43
+ if String === haystack && String === needle or # standard:disable Style/AndOr
44
+ Array === haystack
45
+ haystack.include?(needle)
46
+ else
47
+ raise DI::Error::ExpressionEvaluationError, "Invalid arguments for contains: #{haystack}, #{needle}"
48
+ end
49
+ end
50
+
51
+ def matches(haystack, needle)
52
+ re = Regexp.compile(needle)
53
+ !!(haystack =~ re)
54
+ end
55
+
56
+ def getmember(object, field)
57
+ object.instance_variable_get("@#{field}")
58
+ end
59
+
60
+ def index(array_or_hash, index_or_key)
61
+ case array_or_hash
62
+ when Array
63
+ case index_or_key
64
+ when Integer
65
+ array_or_hash[index_or_key]
66
+ else
67
+ raise DI::Error::ExpressionEvaluationError, "Invalid index value: #{index_or_key}"
68
+ end
69
+ when Hash
70
+ array_or_hash[index_or_key]
71
+ else
72
+ raise DI::Error::ExpressionEvaluationError, "Invalid argument for index: #{array_or_hash}"
73
+ end
74
+ end
75
+
76
+ def substring(object, from, to)
77
+ unless String === object
78
+ raise DI::Error::ExpressionEvaluationError, "Invalid type for substring: #{object}"
79
+ end
80
+ object[from...to]
81
+ end
82
+
83
+ def starts_with(haystack, needle)
84
+ # To guard against running arbitrary customer code, check that
85
+ # the haystack is a string. This does not help if customer
86
+ # overrode String#start_with? but at least it's better than nothing.
87
+ String === haystack && haystack.start_with?(needle)
88
+ end
89
+
90
+ def ends_with(haystack, needle)
91
+ String === haystack && haystack.end_with?(needle)
92
+ end
93
+
94
+ def all(collection, &block)
95
+ case collection
96
+ when Array
97
+ collection.all? do |item|
98
+ block.call(item)
99
+ end
100
+ when Hash
101
+ # For hashes, the expression language has both @it and
102
+ # @key/@value. Manufacture @it from the key and value.
103
+ collection.all? do |key, value|
104
+ block.call([key, value], key, value)
105
+ end
106
+ else
107
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for all: #{collection.class}"
108
+ end
109
+ end
110
+
111
+ def any(collection, &block)
112
+ case collection
113
+ when Array
114
+ collection.any? do |item|
115
+ block.call(item)
116
+ end
117
+ when Hash
118
+ collection.any? do |key, value|
119
+ # For hashes, the expression language has both @it and
120
+ # @key/@value. Manufacture @it from the key and value.
121
+ block.call([key, value], key, value)
122
+ end
123
+ else
124
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for any: #{collection.class}"
125
+ end
126
+ end
127
+
128
+ def filter(collection, &block)
129
+ case collection
130
+ when Array
131
+ collection.select do |item|
132
+ block.call(item)
133
+ end
134
+ when Hash
135
+ collection.select do |key, value|
136
+ block.call([key, value], key, value)
137
+ end.to_h
138
+ else
139
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for filter: #{collection.class}"
140
+ end
141
+ end
142
+
143
+ def instanceof(object, cls_name)
144
+ cls = object.class
145
+ loop do
146
+ if cls.name == cls_name
147
+ return true
148
+ end
149
+ if supercls = cls.superclass # standard:disable Lint/AssignmentInCondition
150
+ cls = supercls
151
+ else
152
+ return false
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # Represents an Expression Language expression.
7
+ #
8
+ # @api private
9
+ class Expression
10
+ def initialize(dsl_expr, compiled_expr)
11
+ unless String === compiled_expr
12
+ raise ArgumentError, "compiled_expr must be a string"
13
+ end
14
+
15
+ @dsl_expr = dsl_expr
16
+
17
+ cls = Class.new(Evaluator)
18
+ cls.class_exec do
19
+ eval(<<-RUBY, Object.new.send(:binding), __FILE__, __LINE__ + 1) # standard:disable Security/Eval
20
+ def evaluate(context)
21
+ @context = context
22
+ #{compiled_expr}
23
+ end
24
+ RUBY
25
+ end
26
+ @evaluator = cls.new
27
+ end
28
+
29
+ attr_reader :dsl_expr
30
+ attr_reader :evaluator
31
+
32
+ def evaluate(context)
33
+ @evaluator.evaluate(context)
34
+ end
35
+
36
+ def satisfied?(context)
37
+ !!evaluate(context)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'el/expression'
4
+ require_relative 'el/compiler'
5
+ require_relative 'el/evaluator'
@@ -48,6 +48,31 @@ module Datadog
48
48
  # and the user will need to make their suffix more precise.
49
49
  class MultiplePathsMatch < Error
50
50
  end
51
+
52
+ # Base class for exceptions arising during expression language AST
53
+ # compilation into Ruby code.
54
+ #
55
+ # Expression language does not specify behavior in all cases,
56
+ # leaving some choices to the language implementation in the tracers.
57
+ # It is therefore possible that some technically valid expressions are
58
+ # prohibited by our implementation.
59
+ #
60
+ # It is also possible that the sanitizers/validators prohibit some
61
+ # esoteric constructs that are technically valid in Ruby,
62
+ # for example if instance variable name rules are relaxed to allow
63
+ # arbitrary characters in them as permitted in method names.
64
+ class InvalidExpression < Error
65
+ end
66
+
67
+ # Variable name with invalid characters in an expression language
68
+ # expression.
69
+ class BadVariableName < InvalidExpression
70
+ end
71
+
72
+ # Base class for exceptions arising when evaluating expression language
73
+ # expressions.
74
+ class ExpressionEvaluationError < Error
75
+ end
51
76
  end
52
77
  end
53
78
  end