-
Notifications
You must be signed in to change notification settings - Fork 45
feat: initial support for ASM inside the tracer #621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
florentinl
merged 7 commits into
main
from
florentin.labelle/APPSEC-57889/enable-asm-for-lambda
Jun 26, 2025
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
be5f6f6
build: add back libddwaf in the layer
florentinl 5bbd404
fix: ensure the start_ns of a function url inferred span is an int
florentinl 042601e
feat(asm): enable Threat Detection inside AWS Lambda for HTTP events
florentinl aa69a89
test(asm): test parsing events for lambda
florentinl 040a122
build: bump layer size check
florentinl 095ba6d
fix(asm): work with non dictionary responses
florentinl 130bd0a
fix(asm): add extra check + comment on listeners
florentinl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
from copy import deepcopy | ||
import logging | ||
from typing import Any, Dict, List, Optional, Union | ||
|
||
from ddtrace.contrib.internal.trace_utils import _get_request_header_client_ip | ||
from ddtrace.internal import core | ||
from ddtrace.trace import Span | ||
|
||
from datadog_lambda.trigger import ( | ||
EventSubtypes, | ||
EventTypes, | ||
_EventSource, | ||
_http_event_types, | ||
) | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def _to_single_value_headers(headers: Dict[str, List[str]]) -> Dict[str, str]: | ||
""" | ||
Convert multi-value headers to single-value headers. | ||
If a header has multiple values, join them with commas. | ||
""" | ||
single_value_headers = {} | ||
for key, values in headers.items(): | ||
single_value_headers[key] = ", ".join(values) | ||
return single_value_headers | ||
|
||
|
||
def _merge_single_and_multi_value_headers( | ||
single_value_headers: Dict[str, str], | ||
multi_value_headers: Dict[str, List[str]], | ||
): | ||
""" | ||
Merge single-value headers with multi-value headers. | ||
If a header exists in both, we merge them removing duplicates | ||
""" | ||
merged_headers = deepcopy(multi_value_headers) | ||
for key, value in single_value_headers.items(): | ||
if key not in merged_headers: | ||
merged_headers[key] = [value] | ||
elif value not in merged_headers[key]: | ||
merged_headers[key].append(value) | ||
return _to_single_value_headers(merged_headers) | ||
|
||
|
||
def asm_start_request( | ||
span: Span, | ||
event: Dict[str, Any], | ||
event_source: _EventSource, | ||
trigger_tags: Dict[str, str], | ||
): | ||
if event_source.event_type not in _http_event_types: | ||
return | ||
|
||
request_headers: Dict[str, str] = {} | ||
peer_ip: Optional[str] = None | ||
request_path_parameters: Optional[Dict[str, Any]] = None | ||
route: Optional[str] = None | ||
|
||
if event_source.event_type == EventTypes.ALB: | ||
headers = event.get("headers") | ||
multi_value_request_headers = event.get("multiValueHeaders") | ||
if multi_value_request_headers: | ||
request_headers = _to_single_value_headers(multi_value_request_headers) | ||
else: | ||
request_headers = headers or {} | ||
|
||
raw_uri = event.get("path") | ||
parsed_query = event.get("multiValueQueryStringParameters") or event.get( | ||
"queryStringParameters" | ||
) | ||
|
||
elif event_source.event_type == EventTypes.LAMBDA_FUNCTION_URL: | ||
request_headers = event.get("headers", {}) | ||
peer_ip = event.get("requestContext", {}).get("http", {}).get("sourceIp") | ||
raw_uri = event.get("rawPath") | ||
parsed_query = event.get("queryStringParameters") | ||
|
||
elif event_source.event_type == EventTypes.API_GATEWAY: | ||
request_context = event.get("requestContext", {}) | ||
request_path_parameters = event.get("pathParameters") | ||
route = trigger_tags.get("http.route") | ||
|
||
if event_source.subtype == EventSubtypes.API_GATEWAY: | ||
request_headers = event.get("headers", {}) | ||
peer_ip = request_context.get("identity", {}).get("sourceIp") | ||
raw_uri = event.get("path") | ||
parsed_query = event.get("multiValueQueryStringParameters") | ||
|
||
elif event_source.subtype == EventSubtypes.HTTP_API: | ||
request_headers = event.get("headers", {}) | ||
peer_ip = request_context.get("http", {}).get("sourceIp") | ||
raw_uri = event.get("rawPath") | ||
parsed_query = event.get("queryStringParameters") | ||
|
||
elif event_source.subtype == EventSubtypes.WEBSOCKET: | ||
request_headers = _to_single_value_headers( | ||
event.get("multiValueHeaders", {}) | ||
) | ||
peer_ip = request_context.get("identity", {}).get("sourceIp") | ||
raw_uri = event.get("path") | ||
parsed_query = event.get("multiValueQueryStringParameters") | ||
|
||
else: | ||
return | ||
|
||
else: | ||
return | ||
|
||
body = event.get("body") | ||
is_base64_encoded = event.get("isBase64Encoded", False) | ||
|
||
request_ip = _get_request_header_client_ip(request_headers, peer_ip, True) | ||
if request_ip is not None: | ||
span.set_tag_str("http.client_ip", request_ip) | ||
span.set_tag_str("network.client.ip", request_ip) | ||
|
||
core.dispatch( | ||
# The matching listener is registered in ddtrace.appsec._handlers | ||
"aws_lambda.start_request", | ||
( | ||
span, | ||
request_headers, | ||
request_ip, | ||
body, | ||
is_base64_encoded, | ||
raw_uri, | ||
route, | ||
trigger_tags.get("http.method"), | ||
parsed_query, | ||
request_path_parameters, | ||
), | ||
) | ||
|
||
|
||
def asm_start_response( | ||
span: Span, | ||
status_code: str, | ||
event_source: _EventSource, | ||
response: Union[Dict[str, Any], str, None], | ||
): | ||
if event_source.event_type not in _http_event_types: | ||
florentinl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return | ||
|
||
if isinstance(response, dict) and ( | ||
"headers" in response or "multiValueHeaders" in response | ||
): | ||
headers = response.get("headers", {}) | ||
multi_value_request_headers = response.get("multiValueHeaders") | ||
if isinstance(multi_value_request_headers, dict) and isinstance(headers, dict): | ||
response_headers = _merge_single_and_multi_value_headers( | ||
headers, multi_value_request_headers | ||
) | ||
elif isinstance(headers, dict): | ||
response_headers = headers | ||
else: | ||
response_headers = { | ||
"content-type": "application/json", | ||
} | ||
else: | ||
response_headers = { | ||
"content-type": "application/json", | ||
} | ||
|
||
core.dispatch( | ||
# The matching listener is registered in ddtrace.appsec._handlers | ||
"aws_lambda.start_response", | ||
( | ||
span, | ||
status_code, | ||
response_headers, | ||
), | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
from importlib import import_module | ||
from time import time_ns | ||
|
||
from datadog_lambda.asm import asm_start_response, asm_start_request | ||
from datadog_lambda.dsm import set_dsm_context | ||
from datadog_lambda.extension import should_use_extension, flush_extension | ||
from datadog_lambda.cold_start import ( | ||
|
@@ -253,6 +254,8 @@ def _before(self, event, context): | |
parent_span=self.inferred_span, | ||
span_pointers=calculate_span_pointers(event_source, event), | ||
) | ||
if config.appsec_enabled: | ||
asm_start_request(self.span, event, event_source, self.trigger_tags) | ||
else: | ||
set_correlation_ids() | ||
if config.profiling_enabled and is_new_sandbox(): | ||
|
@@ -285,6 +288,15 @@ def _after(self, event, context): | |
|
||
if status_code: | ||
self.span.set_tag("http.status_code", status_code) | ||
|
||
if config.appsec_enabled: | ||
asm_start_response( | ||
self.span, | ||
status_code, | ||
self.event_source, | ||
response=self.response, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes you are right, it happens in two cases:
I pushed a fix, thank you. |
||
) | ||
|
||
self.span.finish() | ||
|
||
if self.inferred_span: | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
tests/event_samples/application-load-balancer-mutivalue-headers.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"requestContext": { | ||
"elb": { | ||
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-xyz/123abc" | ||
} | ||
}, | ||
"httpMethod": "GET", | ||
"path": "/lambda", | ||
"queryStringParameters": { | ||
"query": "1234ABCD" | ||
}, | ||
"multiValueHeaders": { | ||
"accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"], | ||
"accept-encoding": ["gzip"], | ||
"accept-language": ["en-US,en;q=0.9"], | ||
"connection": ["keep-alive"], | ||
"host": ["lambda-alb-123578498.us-east-2.elb.amazonaws.com"], | ||
"upgrade-insecure-requests": ["1"], | ||
"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"], | ||
"x-amzn-trace-id": ["Root=1-5c536348-3d683b8b04734faae651f476"], | ||
"x-forwarded-for": ["72.12.164.125"], | ||
"x-forwarded-port": ["80"], | ||
"x-forwarded-proto": ["http"], | ||
"x-imforwards": ["20"], | ||
"x-datadog-trace-id": ["12345"], | ||
"x-datadog-parent-id": ["67890"], | ||
"x-datadog-sampling-priority": ["2"] | ||
}, | ||
"body": "", | ||
"isBase64Encoded": false | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.