Skip to content

Commit 042601e

Browse files
committed
feat(asm): enable Threat Detection inside AWS Lambda for HTTP events
1 parent 5bbd404 commit 042601e

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

datadog_lambda/asm.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import logging
2+
from typing import Any, Dict, List, Optional
3+
4+
from ddtrace.contrib.internal.trace_utils import _get_request_header_client_ip
5+
from ddtrace.internal import core
6+
from ddtrace.trace import Span
7+
8+
from datadog_lambda.trigger import (
9+
EventSubtypes,
10+
EventTypes,
11+
_EventSource,
12+
_http_event_types,
13+
)
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
def _to_single_value_headers(request_headers: Dict[str, List[str]]) -> Dict[str, str]:
19+
"""
20+
Convert multi-value headers to single-value headers.
21+
If a header has multiple values, the first value is used.
22+
"""
23+
single_value_headers = {}
24+
for key, values in request_headers.items():
25+
if len(values) >= 1:
26+
single_value_headers[key] = values[0]
27+
return single_value_headers
28+
29+
30+
def asm_start_request(
31+
span: Span,
32+
event: Dict[str, Any],
33+
event_source: _EventSource,
34+
trigger_tags: Dict[str, str],
35+
):
36+
request_headers: Dict[str, str] = {}
37+
peer_ip: Optional[str] = None
38+
request_path_parameters: Optional[Dict[str, Any]] = None
39+
40+
if event_source.event_type == EventTypes.ALB:
41+
headers = event.get("headers")
42+
multi_value_request_headers = event.get("multiValueHeaders")
43+
if multi_value_request_headers:
44+
request_headers = _to_single_value_headers(multi_value_request_headers)
45+
else:
46+
request_headers = headers or {}
47+
48+
raw_uri = event.get("path")
49+
parsed_query = event.get("multiValueQueryStringParameters") or event.get(
50+
"queryStringParameters"
51+
)
52+
53+
elif event_source.event_type == EventTypes.LAMBDA_FUNCTION_URL:
54+
request_headers = event.get("headers", {})
55+
peer_ip = event.get("requestContext", {}).get("http", {}).get("sourceIp")
56+
raw_uri = event.get("rawPath")
57+
parsed_query = event.get("queryStringParameters")
58+
59+
elif event_source.event_type == EventTypes.API_GATEWAY:
60+
request_context = event.get("requestContext", {})
61+
request_path_parameters = event.get("pathParameters")
62+
63+
if event_source.subtype == EventSubtypes.API_GATEWAY:
64+
request_headers = _to_single_value_headers(
65+
event.get("multiValueHeaders", {})
66+
)
67+
peer_ip = request_context.get("identity", {}).get("sourceIp")
68+
raw_uri = event.get("path")
69+
parsed_query = event.get("multiValueQueryStringParameters")
70+
71+
elif event_source.subtype == EventSubtypes.HTTP_API:
72+
request_headers = event.get("headers", {})
73+
peer_ip = request_context.get("http", {}).get("sourceIp")
74+
raw_uri = event.get("rawPath")
75+
parsed_query = event.get("queryStringParameters")
76+
77+
elif event_source.subtype == EventSubtypes.WEBSOCKET:
78+
request_headers = _to_single_value_headers(
79+
event.get("multiValueHeaders", {})
80+
)
81+
peer_ip = request_context.get("identity", {}).get("sourceIp")
82+
raw_uri = event.get("path")
83+
parsed_query = event.get("multiValueQueryStringParameters")
84+
85+
else:
86+
return
87+
88+
else:
89+
return
90+
91+
body = event.get("body")
92+
is_base64_encoded = event.get("isBase64Encoded", False)
93+
94+
request_ip = _get_request_header_client_ip(request_headers, peer_ip, True)
95+
if request_ip is not None:
96+
span.set_tag_str("http.client_ip", request_ip)
97+
span.set_tag_str("network.client.ip", request_ip)
98+
99+
core.dispatch(
100+
"aws_lambda.start_request",
101+
(
102+
span,
103+
request_headers,
104+
request_ip,
105+
body,
106+
is_base64_encoded,
107+
raw_uri,
108+
trigger_tags.get("http.route"),
109+
trigger_tags.get("http.method"),
110+
parsed_query,
111+
request_path_parameters,
112+
),
113+
)
114+
115+
116+
def asm_start_response(
117+
span: Span,
118+
status_code: str,
119+
event_source: _EventSource,
120+
response: Dict[str, Any],
121+
):
122+
if event_source.event_type not in _http_event_types:
123+
return
124+
125+
response_headers = response.get("headers", {})
126+
if not isinstance(response_headers, dict):
127+
response_headers = {}
128+
129+
core.dispatch(
130+
"aws_lambda.start_response",
131+
(
132+
span,
133+
status_code,
134+
response_headers,
135+
),
136+
)

datadog_lambda/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def _resolve_env(self, key, default=None, cast=None, depends_on_tracing=False):
9595
data_streams_enabled = _get_env(
9696
"DD_DATA_STREAMS_ENABLED", "false", as_bool, depends_on_tracing=True
9797
)
98+
appsec_enabled = _get_env("DD_APPSEC_ENABLED", "false", as_bool)
9899

99100
is_gov_region = _get_env("AWS_REGION", "", lambda x: x.startswith("us-gov-"))
100101

datadog_lambda/wrapper.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from importlib import import_module
1010
from time import time_ns
1111

12+
from datadog_lambda.asm import asm_start_response, asm_start_request
1213
from datadog_lambda.dsm import set_dsm_context
1314
from datadog_lambda.extension import should_use_extension, flush_extension
1415
from datadog_lambda.cold_start import (
@@ -253,6 +254,8 @@ def _before(self, event, context):
253254
parent_span=self.inferred_span,
254255
span_pointers=calculate_span_pointers(event_source, event),
255256
)
257+
if config.appsec_enabled:
258+
asm_start_request(self.span, event, event_source, self.trigger_tags)
256259
else:
257260
set_correlation_ids()
258261
if config.profiling_enabled and is_new_sandbox():
@@ -285,6 +288,15 @@ def _after(self, event, context):
285288

286289
if status_code:
287290
self.span.set_tag("http.status_code", status_code)
291+
292+
if config.appsec_enabled:
293+
asm_start_response(
294+
self.span,
295+
status_code,
296+
self.event_source,
297+
response=self.response,
298+
)
299+
288300
self.span.finish()
289301

290302
if self.inferred_span:

0 commit comments

Comments
 (0)