datadog-lambda 3.28.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 +4 -4
- data/lib/datadog/lambda/appsec/event_normalizer.rb +53 -0
- data/lib/datadog/lambda/appsec/request.rb +45 -0
- data/lib/datadog/lambda/appsec/response_normalizer.rb +37 -0
- data/lib/datadog/lambda/appsec.rb +138 -0
- data/lib/datadog/lambda/trace/listener.rb +12 -0
- data/lib/datadog/lambda/version.rb +1 -1
- data/lib/datadog/lambda.rb +10 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c288d37d55d147aaca103a9ddfb7a7b6f9817aad19528a8be645ac81c879bfc
|
|
4
|
+
data.tar.gz: 0d7252abd55bbbb0075e9e560292487091c8e462027d5001d2cf89237116a761
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/datadog/lambda.rb
CHANGED
|
@@ -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.
|
|
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: 2026-
|
|
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
|
|
@@ -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
|