Skip to content

Commit 555ed5b

Browse files
authored
feat: initial support for ASM inside the tracer (#621)
* build: add back libddwaf in the layer * fix: ensure the start_ns of a function url inferred span is an int * feat(asm): enable Threat Detection inside AWS Lambda for HTTP events * test(asm): test parsing events for lambda * build: bump layer size check * fix(asm): work with non dictionary responses * fix(asm): add extra check + comment on listeners
1 parent b08b763 commit 555ed5b

File tree

8 files changed

+550
-4
lines changed

8 files changed

+550
-4
lines changed

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ RUN pip install --no-cache-dir . -t ./python/lib/$runtime/site-packages
2121
RUN rm -rf ./python/lib/$runtime/site-packages/botocore*
2222
RUN rm -rf ./python/lib/$runtime/site-packages/setuptools
2323
RUN rm -rf ./python/lib/$runtime/site-packages/jsonschema/tests
24-
RUN find . -name 'libddwaf.so' -delete
2524
RUN rm -f ./python/lib/$runtime/site-packages/ddtrace/appsec/_iast/_taint_tracking/*.so
2625
RUN rm -f ./python/lib/$runtime/site-packages/ddtrace/appsec/_iast/_stacktrace*.so
2726
RUN rm -f ./python/lib/$runtime/site-packages/ddtrace/internal/datadog/profiling/libdd_wrapper*.so

datadog_lambda/asm.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from copy import deepcopy
2+
import logging
3+
from typing import Any, Dict, List, Optional, Union
4+
5+
from ddtrace.contrib.internal.trace_utils import _get_request_header_client_ip
6+
from ddtrace.internal import core
7+
from ddtrace.trace import Span
8+
9+
from datadog_lambda.trigger import (
10+
EventSubtypes,
11+
EventTypes,
12+
_EventSource,
13+
_http_event_types,
14+
)
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
def _to_single_value_headers(headers: Dict[str, List[str]]) -> Dict[str, str]:
20+
"""
21+
Convert multi-value headers to single-value headers.
22+
If a header has multiple values, join them with commas.
23+
"""
24+
single_value_headers = {}
25+
for key, values in headers.items():
26+
single_value_headers[key] = ", ".join(values)
27+
return single_value_headers
28+
29+
30+
def _merge_single_and_multi_value_headers(
31+
single_value_headers: Dict[str, str],
32+
multi_value_headers: Dict[str, List[str]],
33+
):
34+
"""
35+
Merge single-value headers with multi-value headers.
36+
If a header exists in both, we merge them removing duplicates
37+
"""
38+
merged_headers = deepcopy(multi_value_headers)
39+
for key, value in single_value_headers.items():
40+
if key not in merged_headers:
41+
merged_headers[key] = [value]
42+
elif value not in merged_headers[key]:
43+
merged_headers[key].append(value)
44+
return _to_single_value_headers(merged_headers)
45+
46+
47+
def asm_start_request(
48+
span: Span,
49+
event: Dict[str, Any],
50+
event_source: _EventSource,
51+
trigger_tags: Dict[str, str],
52+
):
53+
if event_source.event_type not in _http_event_types:
54+
return
55+
56+
request_headers: Dict[str, str] = {}
57+
peer_ip: Optional[str] = None
58+
request_path_parameters: Optional[Dict[str, Any]] = None
59+
route: Optional[str] = None
60+
61+
if event_source.event_type == EventTypes.ALB:
62+
headers = event.get("headers")
63+
multi_value_request_headers = event.get("multiValueHeaders")
64+
if multi_value_request_headers:
65+
request_headers = _to_single_value_headers(multi_value_request_headers)
66+
else:
67+
request_headers = headers or {}
68+
69+
raw_uri = event.get("path")
70+
parsed_query = event.get("multiValueQueryStringParameters") or event.get(
71+
"queryStringParameters"
72+
)
73+
74+
elif event_source.event_type == EventTypes.LAMBDA_FUNCTION_URL:
75+
request_headers = event.get("headers", {})
76+
peer_ip = event.get("requestContext", {}).get("http", {}).get("sourceIp")
77+
raw_uri = event.get("rawPath")
78+
parsed_query = event.get("queryStringParameters")
79+
80+
elif event_source.event_type == EventTypes.API_GATEWAY:
81+
request_context = event.get("requestContext", {})
82+
request_path_parameters = event.get("pathParameters")
83+
route = trigger_tags.get("http.route")
84+
85+
if event_source.subtype == EventSubtypes.API_GATEWAY:
86+
request_headers = event.get("headers", {})
87+
peer_ip = request_context.get("identity", {}).get("sourceIp")
88+
raw_uri = event.get("path")
89+
parsed_query = event.get("multiValueQueryStringParameters")
90+
91+
elif event_source.subtype == EventSubtypes.HTTP_API:
92+
request_headers = event.get("headers", {})
93+
peer_ip = request_context.get("http", {}).get("sourceIp")
94+
raw_uri = event.get("rawPath")
95+
parsed_query = event.get("queryStringParameters")
96+
97+
elif event_source.subtype == EventSubtypes.WEBSOCKET:
98+
request_headers = _to_single_value_headers(
99+
event.get("multiValueHeaders", {})
100+
)
101+
peer_ip = request_context.get("identity", {}).get("sourceIp")
102+
raw_uri = event.get("path")
103+
parsed_query = event.get("multiValueQueryStringParameters")
104+
105+
else:
106+
return
107+
108+
else:
109+
return
110+
111+
body = event.get("body")
112+
is_base64_encoded = event.get("isBase64Encoded", False)
113+
114+
request_ip = _get_request_header_client_ip(request_headers, peer_ip, True)
115+
if request_ip is not None:
116+
span.set_tag_str("http.client_ip", request_ip)
117+
span.set_tag_str("network.client.ip", request_ip)
118+
119+
core.dispatch(
120+
# The matching listener is registered in ddtrace.appsec._handlers
121+
"aws_lambda.start_request",
122+
(
123+
span,
124+
request_headers,
125+
request_ip,
126+
body,
127+
is_base64_encoded,
128+
raw_uri,
129+
route,
130+
trigger_tags.get("http.method"),
131+
parsed_query,
132+
request_path_parameters,
133+
),
134+
)
135+
136+
137+
def asm_start_response(
138+
span: Span,
139+
status_code: str,
140+
event_source: _EventSource,
141+
response: Union[Dict[str, Any], str, None],
142+
):
143+
if event_source.event_type not in _http_event_types:
144+
return
145+
146+
if isinstance(response, dict) and (
147+
"headers" in response or "multiValueHeaders" in response
148+
):
149+
headers = response.get("headers", {})
150+
multi_value_request_headers = response.get("multiValueHeaders")
151+
if isinstance(multi_value_request_headers, dict) and isinstance(headers, dict):
152+
response_headers = _merge_single_and_multi_value_headers(
153+
headers, multi_value_request_headers
154+
)
155+
elif isinstance(headers, dict):
156+
response_headers = headers
157+
else:
158+
response_headers = {
159+
"content-type": "application/json",
160+
}
161+
else:
162+
response_headers = {
163+
"content-type": "application/json",
164+
}
165+
166+
core.dispatch(
167+
# The matching listener is registered in ddtrace.appsec._handlers
168+
"aws_lambda.start_response",
169+
(
170+
span,
171+
status_code,
172+
response_headers,
173+
),
174+
)

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/tracing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ def create_inferred_span_from_lambda_function_url_event(event, context):
859859
InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="sync")
860860
if span:
861861
span.set_tags(tags)
862-
span.start_ns = int(request_time_epoch) * 1e6
862+
span.start_ns = int(request_time_epoch * 1e6)
863863
return span
864864

865865

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:

scripts/check_layer_size.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
# Compares layer size to threshold, and fails if below that threshold
99

1010
set -e
11-
MAX_LAYER_COMPRESSED_SIZE_KB=$(expr 5 \* 1024)
12-
MAX_LAYER_UNCOMPRESSED_SIZE_KB=$(expr 13 \* 1024)
11+
MAX_LAYER_COMPRESSED_SIZE_KB=$(expr 6 \* 1024)
12+
MAX_LAYER_UNCOMPRESSED_SIZE_KB=$(expr 15 \* 1024)
1313

1414

1515
LAYER_FILES_PREFIX="datadog_lambda_py"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"requestContext": {
3+
"elb": {
4+
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-xyz/123abc"
5+
}
6+
},
7+
"httpMethod": "GET",
8+
"path": "/lambda",
9+
"queryStringParameters": {
10+
"query": "1234ABCD"
11+
},
12+
"multiValueHeaders": {
13+
"accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"],
14+
"accept-encoding": ["gzip"],
15+
"accept-language": ["en-US,en;q=0.9"],
16+
"connection": ["keep-alive"],
17+
"host": ["lambda-alb-123578498.us-east-2.elb.amazonaws.com"],
18+
"upgrade-insecure-requests": ["1"],
19+
"user-agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"],
20+
"x-amzn-trace-id": ["Root=1-5c536348-3d683b8b04734faae651f476"],
21+
"x-forwarded-for": ["72.12.164.125"],
22+
"x-forwarded-port": ["80"],
23+
"x-forwarded-proto": ["http"],
24+
"x-imforwards": ["20"],
25+
"x-datadog-trace-id": ["12345"],
26+
"x-datadog-parent-id": ["67890"],
27+
"x-datadog-sampling-priority": ["2"]
28+
},
29+
"body": "",
30+
"isBase64Encoded": false
31+
}

0 commit comments

Comments
 (0)