datadog-lambda 3.27.0 → 3.29.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: 327f22524c28715a772eae6ea159c4b39c2bdc5ce8ceca2c5f665f45d3ecaa72
4
- data.tar.gz: 325f5f3d40f965f34b9474df80580e6e8664dfedcfa8e072c24bcf065d0992e0
3
+ metadata.gz: 6c288d37d55d147aaca103a9ddfb7a7b6f9817aad19528a8be645ac81c879bfc
4
+ data.tar.gz: 0d7252abd55bbbb0075e9e560292487091c8e462027d5001d2cf89237116a761
5
5
  SHA512:
6
- metadata.gz: 1f2d0d594085a1c719d62329369706e0febc232aab59c8bb3fd56de050d6419dc74f07d692b294443ab80a6693d756515c09f71909741a9cd693632a330f9b8c
7
- data.tar.gz: cf47002396c9583e3afe62c4bb1ea654c4644e1b073ec13356b17c87e5f44bea8bda1a6335c3e2dfe71bb56a429a476f837b4ab89af0559b8810f1c2d69a5d70
6
+ metadata.gz: 789cf5a6f51a19ffb8e0ed0e701c976669f841a186245aa58b881d9eca93865e2a76b19866f0426f3e08cda03bc552686ea41893d15ba2ad29d82e88f8f7493d
7
+ data.tar.gz: 1ef9d2bc286e9b7279fe55cc548f17b579eb3bad7dedbc5d6109fda28d0a4f964ebe43d8569420e084da33a28386d8c70d48faeb96721277a3f28fe773e7e82e
@@ -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,45 @@
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
+
15
+ class << self
16
+ def from_normalized(event)
17
+ headers = lowercase_headers(event)
18
+
19
+ new(
20
+ host: headers['host'],
21
+ user_agent: headers['user-agent'],
22
+ remote_addr: event['source_ip'],
23
+ headers: headers
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def lowercase_headers(event)
30
+ (event['headers'] || {}).each_with_object({}) do |(key, value), hash|
31
+ hash[key.downcase] = value
32
+ end
33
+ end
34
+ end
35
+
36
+ def initialize(host:, user_agent:, remote_addr:, headers:)
37
+ @host = host
38
+ @user_agent = user_agent
39
+ @remote_addr = remote_addr
40
+ @headers = headers
41
+ end
42
+ end
43
+ end
44
+ end
45
+ 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,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'appsec/request'
5
+ require_relative 'appsec/event_normalizer'
6
+ require_relative 'appsec/response_normalizer'
7
+
8
+ module Datadog
9
+ module Lambda
10
+ # AppSec integration for AWS Lambda invocations.
11
+ module AppSec
12
+ class << self
13
+ # rubocop:disable Metrics/AbcSize
14
+ def on_start(event, trace:, span:, cold_start: false)
15
+ @request = nil
16
+ return unless enabled?
17
+
18
+ context = create_context(trace, span)
19
+ return unless Datadog::AppSec::Context.active
20
+
21
+ tag_and_keep(context, cold_start: cold_start)
22
+
23
+ event = EventNormalizer.normalize(event)
24
+ @request = Request.from_normalized(event)
25
+
26
+ payload = Datadog::AppSec::Instrumentation::Gateway::DataContainer.new(
27
+ event, context: context
28
+ )
29
+
30
+ interrupt_params = catch(Datadog::AppSec::Ext::INTERRUPT) do
31
+ Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.request.start', payload)
32
+ nil
33
+ end
34
+
35
+ return unless interrupt_params
36
+
37
+ context.mark_as_interrupted!
38
+ response_override(interrupt_params, headers: @request.headers)
39
+ rescue StandardError => e
40
+ Datadog::AppSec::Context.deactivate if context
41
+ Datadog::Utils.logger.debug("failed to start AppSec: #{e}")
42
+ end
43
+ # rubocop:enable Metrics/AbcSize
44
+
45
+ # rubocop:disable Metrics/AbcSize
46
+ def on_finish(response)
47
+ return unless enabled?
48
+
49
+ context = Datadog::AppSec::Context.active
50
+ return unless context
51
+
52
+ response = ResponseNormalizer.normalize(response)
53
+ payload = Datadog::AppSec::Instrumentation::Gateway::DataContainer.new(
54
+ response, context: context
55
+ )
56
+
57
+ interrupt_params = catch(Datadog::AppSec::Ext::INTERRUPT) do
58
+ Datadog::AppSec::Instrumentation.gateway.push('aws_lambda.response.start', payload)
59
+ nil
60
+ end
61
+
62
+ context.mark_as_interrupted! if interrupt_params
63
+
64
+ Datadog::AppSec::Event.record(context, request: @request)
65
+ context.export_metrics
66
+ context.export_request_telemetry
67
+
68
+ response_override(interrupt_params, headers: @request.headers) if interrupt_params
69
+ rescue StandardError => e
70
+ Datadog::Utils.logger.debug "failed to finish AppSec: #{e}"
71
+ ensure
72
+ Datadog::AppSec::Context.deactivate if context
73
+ end
74
+ # rubocop:enable Metrics/AbcSize
75
+
76
+ private
77
+
78
+ def enabled?
79
+ defined?(Datadog::AppSec) &&
80
+ Datadog::AppSec.respond_to?(:enabled?) &&
81
+ Datadog::AppSec.enabled?
82
+ end
83
+
84
+ def create_context(trace, span)
85
+ return if trace.nil? || span.nil?
86
+
87
+ security_engine = Datadog::AppSec.security_engine
88
+ return unless security_engine
89
+
90
+ context = Datadog::AppSec::Context.new(trace, span, security_engine.new_runner)
91
+ Datadog::AppSec::Context.activate(context)
92
+
93
+ context
94
+ end
95
+
96
+ def tag_and_keep(context, cold_start:)
97
+ span = context.span
98
+ trace = context.trace
99
+
100
+ return unless trace && span
101
+
102
+ span.set_metric(Datadog::AppSec::Ext::TAG_APPSEC_ENABLED, 1)
103
+ span.set_tag('_dd.runtime_family', 'ruby')
104
+ span.set_tag('_dd.appsec.waf.version', Datadog::AppSec::WAF::VERSION::BASE_STRING)
105
+
106
+ ruleset_version = context.waf_runner_ruleset_version
107
+ return unless ruleset_version
108
+
109
+ span.set_tag('_dd.appsec.event_rules.version', ruleset_version)
110
+
111
+ return unless cold_start
112
+
113
+ span.set_tag(
114
+ '_dd.appsec.event_rules.addresses', JSON.dump(context.waf_runner_known_addresses)
115
+ )
116
+
117
+ trace.keep!
118
+ trace.set_tag(
119
+ Datadog::Tracing::Metadata::Ext::Distributed::TAG_DECISION_MAKER,
120
+ Datadog::Tracing::Sampling::Ext::Decision::ASM
121
+ )
122
+ end
123
+
124
+ def response_override(interrupt_params, headers:)
125
+ response = Datadog::AppSec::Response.from_interrupt_params(
126
+ interrupt_params, headers['accept']
127
+ )
128
+
129
+ {
130
+ 'statusCode' => response.status,
131
+ 'headers' => response.headers,
132
+ 'body' => response.body.join
133
+ }
134
+ end
135
+ end
136
+ end
137
+ end
138
+ 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,17 @@ 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
 
56
64
  def on_end(response:, request_context:)
65
+ if (override = Datadog::Lambda::AppSec.on_finish(response))
66
+ @response_override = override
67
+ end
68
+
57
69
  Datadog::Utils.send_end_invocation_request(response:, span_id: @trace.id, request_context:)
58
70
  @trace&.finish
59
71
  end
@@ -12,7 +12,7 @@ module Datadog
12
12
  module Lambda
13
13
  module VERSION
14
14
  MAJOR = 3
15
- MINOR = 27
15
+ MINOR = 29
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, Metrics/PerceivedComplexity
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&.response_override || 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, Metrics/PerceivedComplexity
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.27.0
4
+ version: 3.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-16 00:00:00.000000000 Z
11
+ date: 2026-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-xray-sdk
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.12'
47
+ version: '2.30'
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.12'
54
+ version: '2.30'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '12.3'
61
+ version: '13.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '12.3'
68
+ version: '13.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -119,6 +119,10 @@ 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_normalizer.rb
122
126
  - lib/datadog/lambda/metrics.rb
123
127
  - lib/datadog/lambda/trace/constants.rb
124
128
  - lib/datadog/lambda/trace/context.rb