datadog-lambda 3.28.0 → 3.30.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e2d8635a06b2c62e65a431f5fede05eb2f2de447fe310541a62dfbdbedc41b5
4
- data.tar.gz: 6fc01a5fa9b25beb6296a2127de07962ea2cb2166b738c4e32a69a8d0acd9e28
3
+ metadata.gz: 16d17173aa40606df94b042f5b548443b57bc2d548a5da80d984c59a377ddcd5
4
+ data.tar.gz: 8ea8bf2a3559c0748405f249f169581783fb1e0a1ab23703378b119abc8362a8
5
5
  SHA512:
6
- metadata.gz: d7f386673cb7bf6ec500e7b144821d811921129a853f21d96a9cf54804321f17bd2ce18fdd420f272f77f192f581d983b6d2d079d97bfb5179252cc026d6f32f
7
- data.tar.gz: e9f6f80d1330d0ef908c11035c89f23ba3c4460a6de7f51f3c25cee65813fdf3ff17877836d649421144b457820b23cb79a08e9309b2eb7dfb2ab58b0ce2a105
6
+ metadata.gz: f117a37f4d08a925ff382dc29b01d89d73216351e3ea6452fcb98debeba8a0c07b2cc5b004526c19e8f47bca25034cf3134466c93813d2844df8a36e889d7a1f
7
+ data.tar.gz: b6ea614f6057797c8af1e1ba3385c2fd744fe4c346d9ce3d1cafd21eceb98ff247d312b460f17f63322d6324b9e6940c96d2fe22d29c047f6660a03e0e2b710d
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Lambda
5
+ module AppSec
6
+ # Normalizes API Gateway v1/v2 event payloads into a standard key set.
7
+ #
8
+ # NOTE: The REST API (v1) event does NOT have a version field.
9
+ # Only the HTTP API events have "version": "1.0" or "version": "2.0".
10
+ #
11
+ # @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format-structure
12
+ module EventNormalizer
13
+ module_function
14
+
15
+ def normalize(event)
16
+ event.key?('httpMethod') ? normalize_v1(event) : normalize_v2(event)
17
+ end
18
+
19
+ def normalize_v1(event)
20
+ data = {
21
+ 'method' => event['httpMethod'],
22
+ 'path' => event['path'],
23
+ 'headers' => event['headers'],
24
+ 'query' => event['multiValueQueryStringParameters'] || event['queryStringParameters'],
25
+ 'source_ip' => event.dig('requestContext', 'identity', 'sourceIp'),
26
+ 'body' => event['body'],
27
+ 'base64_encoded' => event['isBase64Encoded'],
28
+ 'path_params' => event['pathParameters']
29
+ }
30
+ data.compact!
31
+ data
32
+ end
33
+
34
+ def normalize_v2(event)
35
+ data = {
36
+ 'method' => event.dig('requestContext', 'http', 'method'),
37
+ 'path' => event['rawPath'],
38
+ 'headers' => event['headers'],
39
+ 'cookies' => event['cookies'],
40
+ 'query' => event['queryStringParameters'],
41
+ 'query_string' => event['rawQueryString'],
42
+ 'source_ip' => event.dig('requestContext', 'http', 'sourceIp'),
43
+ 'body' => event['body'],
44
+ 'base64_encoded' => event['isBase64Encoded'],
45
+ 'path_params' => event['pathParameters']
46
+ }
47
+ data.compact!
48
+ data
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Lambda
5
+ module AppSec
6
+ # Minimal request object for AppSec event recording.
7
+ #
8
+ # WARNING: It's a minimal data for interface compliance
9
+ #
10
+ # @see Datadog::AppSec::Event.record
11
+ # @see Datadog::AppSec::Contrib::Rack::Gateway::Request
12
+ class Request
13
+ attr_reader :host, :user_agent, :remote_addr, :headers,
14
+ :request_method, :path
15
+
16
+ class << self
17
+ def from_normalized(event)
18
+ new(
19
+ remote_addr: event['source_ip'],
20
+ headers: lowercase_headers(event),
21
+ request_method: event['method'],
22
+ path: event['path']
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def lowercase_headers(event)
29
+ (event['headers'] || {}).each_with_object({}) do |(key, value), hash|
30
+ hash[key.downcase] = value
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(remote_addr:, headers:, request_method:, path:)
36
+ @headers = headers
37
+ @host = headers['host']
38
+ @user_agent = headers['user-agent']
39
+ @remote_addr = remote_addr
40
+ @request_method = request_method
41
+ @path = path
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Lambda
5
+ module AppSec
6
+ # Minimal response object for AppSec event recording.
7
+ #
8
+ # WARNING: It's a minimal data for interface compliance
9
+ #
10
+ # @see Datadog::AppSec::APISecurity.sample?
11
+ # @see Datadog::AppSec::Contrib::Rack::Gateway::Response
12
+ class Response
13
+ attr_reader :status, :headers, :body
14
+
15
+ def self.from_normalized(normalized)
16
+ new(
17
+ status: normalized['status_code'].to_i,
18
+ headers: normalized['headers'] || {},
19
+ body: normalized['body']
20
+ )
21
+ end
22
+
23
+ def initialize(status:, headers:, body:)
24
+ @status = status
25
+ @headers = headers
26
+ @body = body
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Lambda
5
+ module AppSec
6
+ # Normalizes Lambda handler return values into a standard key set.
7
+ #
8
+ # @see https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
9
+ module ResponseNormalizer
10
+ module_function
11
+
12
+ def normalize(response)
13
+ data = {
14
+ 'status_code' => response['statusCode'],
15
+ 'headers' => merge_headers(response),
16
+ 'body' => response['body'],
17
+ 'base64_encoded' => response['isBase64Encoded']
18
+ }
19
+ data.compact!
20
+ data
21
+ end
22
+
23
+ # Merges single-value and multi-value headers into one hash with
24
+ # multi-value entries take priority when a key appears in both.
25
+ def merge_headers(response)
26
+ headers = response['headers']
27
+ multi_headers = response['multiValueHeaders']
28
+
29
+ return headers unless multi_headers
30
+ return multi_headers unless headers
31
+
32
+ headers.merge(multi_headers)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ require 'datadog/tracing/client_ip'
6
+ require 'datadog/core/header_collection'
7
+ require 'datadog/appsec/default_header_tags'
8
+
9
+ module Datadog
10
+ module Lambda
11
+ module AppSec
12
+ # Applies AppSec request, response, and keep-decision tags to the Lambda span
13
+ module Tagging
14
+ module_function
15
+
16
+ def tag_request(request, context:)
17
+ span = context.span
18
+ return unless span
19
+
20
+ headers = Datadog::Core::HeaderCollection.from_hash(request.headers)
21
+ Datadog::AppSec::DefaultHeaderTags.tag_request(span, headers)
22
+
23
+ return if span.get_tag(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP)
24
+
25
+ Datadog::Tracing::ClientIp.set_client_ip_tag!(
26
+ span, headers: headers, remote_ip: request.remote_addr
27
+ )
28
+ end
29
+
30
+ def tag_response(response, context:)
31
+ span = context.span
32
+ return unless span
33
+
34
+ headers = Datadog::Core::HeaderCollection.from_hash(response.headers)
35
+ Datadog::AppSec::DefaultHeaderTags.tag_response(span, headers)
36
+
37
+ return if headers.get('content-length')
38
+
39
+ if (body = response.body)
40
+ span.set_tag('http.response.headers.content-length', body.bytesize.to_s)
41
+ end
42
+ end
43
+
44
+ def tag_and_keep(context, cold_start:)
45
+ span = context.span
46
+ trace = context.trace
47
+
48
+ return unless trace && span
49
+
50
+ span.set_metric(Datadog::AppSec::Ext::TAG_APPSEC_ENABLED, 1)
51
+ span.set_tag('_dd.runtime_family', 'ruby')
52
+ span.set_tag('_dd.appsec.waf.version', Datadog::AppSec::WAF::VERSION::BASE_STRING)
53
+
54
+ ruleset_version = context.waf_runner_ruleset_version
55
+ return unless ruleset_version
56
+
57
+ span.set_tag('_dd.appsec.event_rules.version', ruleset_version)
58
+
59
+ return unless cold_start
60
+
61
+ trace.keep!
62
+ trace.set_tag(
63
+ Datadog::Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER,
64
+ Datadog::Tracing::Sampling::Ext::Decision::ASM
65
+ )
66
+ span.set_tag(
67
+ '_dd.appsec.event_rules.addresses', JSON.dump(context.waf_runner_known_addresses)
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'datadog/appsec/api_security'
4
+
5
+ require_relative 'appsec/tagging'
6
+ require_relative 'appsec/request'
7
+ require_relative 'appsec/response'
8
+ require_relative 'appsec/event_normalizer'
9
+ require_relative 'appsec/response_normalizer'
10
+
11
+ module Datadog
12
+ module Lambda
13
+ # AppSec integration for AWS Lambda invocations.
14
+ module AppSec
15
+ class << self
16
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
17
+ def on_start(event, trace:, span:, cold_start: false)
18
+ @request = nil
19
+ return unless enabled?
20
+
21
+ context = create_context(trace, span)
22
+ return unless Datadog::AppSec::Context.active
23
+
24
+ Tagging.tag_and_keep(context, cold_start: cold_start)
25
+
26
+ event = EventNormalizer.normalize(event)
27
+ @request = Request.from_normalized(event)
28
+
29
+ Tagging.tag_request(@request, context: context)
30
+ payload = Datadog::AppSec::Instrumentation::Gateway::DataContainer.new(
31
+ event, context: context
32
+ )
33
+
34
+ interrupt_params = catch(Datadog::AppSec::Ext::INTERRUPT) do
35
+ Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.request.start', payload)
36
+ nil
37
+ end
38
+
39
+ return unless interrupt_params
40
+
41
+ context.mark_as_interrupted!
42
+ response_override(interrupt_params, headers: @request.headers)
43
+ rescue StandardError => e
44
+ Datadog::AppSec::Context.deactivate if context
45
+ Datadog::Utils.logger.debug("failed to start AppSec: #{e}")
46
+ end
47
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
48
+
49
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
50
+ def on_finish(response)
51
+ return unless enabled?
52
+
53
+ context = Datadog::AppSec::Context.active
54
+ return unless context
55
+
56
+ normalized = ResponseNormalizer.normalize(response)
57
+ response = Response.from_normalized(normalized)
58
+
59
+ interrupt_params = nil
60
+ unless context.interrupted?
61
+ Tagging.tag_response(response, context: context)
62
+ payload = Datadog::AppSec::Instrumentation::Gateway::DataContainer.new(
63
+ normalized, context: context
64
+ )
65
+
66
+ interrupt_params = catch(Datadog::AppSec::Ext::INTERRUPT) do
67
+ Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.response.start', payload)
68
+ nil
69
+ end
70
+
71
+ context.mark_as_interrupted! if interrupt_params
72
+ end
73
+
74
+ extract_api_security_schema(context, request: @request, response: response)
75
+
76
+ Datadog::AppSec::Event.record(context, request: @request)
77
+ context.export_metrics
78
+ context.export_request_telemetry
79
+
80
+ response_override(interrupt_params, headers: @request.headers) if interrupt_params
81
+ rescue StandardError => e
82
+ Datadog::Utils.logger.debug "failed to finish AppSec: #{e}"
83
+ ensure
84
+ Datadog::AppSec::Context.deactivate if context
85
+ end
86
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
87
+
88
+ def catch_interrupt
89
+ interrupt_params = catch(Datadog::AppSec::Ext::INTERRUPT) { return yield }
90
+
91
+ if (context = Datadog::AppSec::Context.active)
92
+ context.mark_as_interrupted!
93
+ end
94
+
95
+ response_override(interrupt_params, headers: @request.headers)
96
+ end
97
+
98
+ private
99
+
100
+ def extract_api_security_schema(context, request:, response:)
101
+ return unless request && response
102
+
103
+ if Datadog::AppSec::APISecurity.enabled? &&
104
+ Datadog::AppSec::APISecurity.sample_trace?(context.trace) &&
105
+ Datadog::AppSec::APISecurity.sample?(request, response)
106
+ context.extract_schema!
107
+ end
108
+ end
109
+
110
+ def enabled?
111
+ defined?(Datadog::AppSec) &&
112
+ Datadog::AppSec.respond_to?(:enabled?) &&
113
+ Datadog::AppSec.enabled?
114
+ end
115
+
116
+ def create_context(trace, span)
117
+ return if trace.nil? || span.nil?
118
+
119
+ security_engine = Datadog::AppSec.security_engine
120
+ return unless security_engine
121
+
122
+ context = Datadog::AppSec::Context.new(trace, span, security_engine.new_runner)
123
+ Datadog::AppSec::Context.activate(context)
124
+
125
+ context
126
+ end
127
+
128
+ def response_override(interrupt_params, headers:)
129
+ response = Datadog::AppSec::Response.from_interrupt_params(
130
+ interrupt_params, headers['accept']
131
+ )
132
+
133
+ {
134
+ 'statusCode' => response.status,
135
+ 'headers' => response.headers,
136
+ 'body' => response.body.join
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -11,11 +11,16 @@
11
11
  require 'datadog/lambda/trace/context'
12
12
  require 'datadog/lambda/trace/patch_http'
13
13
  require 'datadog/lambda/trace/ddtrace'
14
+ require 'datadog/lambda/appsec'
14
15
 
15
16
  module Datadog
16
17
  module Trace
17
18
  # TraceListener tracks tracing context information
18
19
  class Listener
20
+ # AppSec blocking response that replaces the handler result.
21
+ # Set during either on_start or on_end when WAF decides to block.
22
+ attr_reader :response_override
23
+
19
24
  @trace = nil
20
25
  def initialize(handler_name:, function_name:, patch_http:,
21
26
  merge_xray_traces:)
@@ -50,10 +55,21 @@ module Datadog
50
55
  @trace = Datadog::Tracing.trace('aws.lambda', **options)
51
56
 
52
57
  Datadog::Trace.apply_datadog_trace_context(Datadog::Trace.trace_context)
58
+ @response_override = Datadog::Lambda::AppSec.on_start(
59
+ event, trace: Datadog::Tracing.active_trace, span: @trace, cold_start: cold_start
60
+ )
53
61
  end
54
62
  # rubocop:enable Metrics/AbcSize
55
63
 
64
+ def with_appsec(&block)
65
+ @response_override || Datadog::Lambda::AppSec.catch_interrupt(&block)
66
+ end
67
+
56
68
  def on_end(response:, request_context:)
69
+ if (override = Datadog::Lambda::AppSec.on_finish(response))
70
+ @response_override = override
71
+ end
72
+
57
73
  Datadog::Utils.send_end_invocation_request(response:, span_id: @trace.id, request_context:)
58
74
  @trace&.finish
59
75
  end
@@ -12,7 +12,7 @@ module Datadog
12
12
  module Lambda
13
13
  module VERSION
14
14
  MAJOR = 3
15
- MINOR = 28
15
+ MINOR = 30
16
16
  PATCH = 0
17
17
  PRE = nil
18
18
 
@@ -29,6 +29,8 @@ module Datadog
29
29
  # Configures Datadog's APM tracer with lambda specific defaults.
30
30
  # Same options can be given as Datadog.configure in tracer
31
31
  # See https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#quickstart-for-ruby-applications
32
+ #
33
+ # rubocop:disable Metrics/AbcSize
32
34
  def self.configure_apm
33
35
  require 'datadog/tracing'
34
36
  require 'datadog/tracing/transport/io'
@@ -48,30 +50,37 @@ module Datadog
48
50
  c.tracing.instrument :aws if trace_managed_services?
49
51
 
50
52
  yield(c) if block_given?
53
+
54
+ c.appsec.instrument(:aws_lambda)
51
55
  end
52
56
  end
57
+ # rubocop:enable Metrics/AbcSize
53
58
 
54
59
  # Wrap the body of a lambda invocation
55
60
  # @param event [Object] event sent to lambda
56
61
  # @param context [Object] lambda context
57
62
  # @param block [Proc] implementation of the handler function.
63
+ # rubocop:disable Metrics/CyclomaticComplexity
58
64
  def self.wrap(event, context, &block)
65
+ @response = nil
59
66
  @listener ||= initialize_listener
60
67
  record_enhanced('invocations', context)
61
68
  begin
62
69
  cold = @is_cold_start
63
70
  @listener&.on_start(event:, request_context: context, cold_start: cold)
64
- @response = block.call
71
+ @response = @listener ? @listener.with_appsec(&block) : block.call
65
72
  rescue StandardError => e
66
73
  record_enhanced('errors', context)
67
74
  raise e
68
75
  ensure
69
76
  @listener&.on_end(response: @response, request_context: context)
77
+ @response = @listener&.response_override || @response
70
78
  @is_cold_start = false
71
79
  @metrics_client.close
72
80
  end
73
81
  @response
74
82
  end
83
+ # rubocop:enable Metrics/CyclomaticComplexity
75
84
 
76
85
  # Gets the current tracing context
77
86
  def self.trace_context
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-lambda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.28.0
4
+ version: 3.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-14 00:00:00.000000000 Z
11
+ date: 2026-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-xray-sdk
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.30'
47
+ version: '2.40'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.30'
54
+ version: '2.40'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -119,6 +119,12 @@ extensions: []
119
119
  extra_rdoc_files: []
120
120
  files:
121
121
  - lib/datadog/lambda.rb
122
+ - lib/datadog/lambda/appsec.rb
123
+ - lib/datadog/lambda/appsec/event_normalizer.rb
124
+ - lib/datadog/lambda/appsec/request.rb
125
+ - lib/datadog/lambda/appsec/response.rb
126
+ - lib/datadog/lambda/appsec/response_normalizer.rb
127
+ - lib/datadog/lambda/appsec/tagging.rb
122
128
  - lib/datadog/lambda/metrics.rb
123
129
  - lib/datadog/lambda/trace/constants.rb
124
130
  - lib/datadog/lambda/trace/context.rb
@@ -134,7 +140,7 @@ licenses:
134
140
  - Apache-2.0
135
141
  metadata:
136
142
  allowed_push_host: https://rubygems.org
137
- post_install_message:
143
+ post_install_message:
138
144
  rdoc_options: []
139
145
  require_paths:
140
146
  - lib
@@ -149,8 +155,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
155
  - !ruby/object:Gem::Version
150
156
  version: '0'
151
157
  requirements: []
152
- rubygems_version: 3.3.5
153
- signing_key:
158
+ rubygems_version: 3.4.19
159
+ signing_key:
154
160
  specification_version: 4
155
161
  summary: Instruments your Ruby AWS Lambda functions with Datadog
156
162
  test_files: []