Skip to content

Commit e05be0f

Browse files
authored
Merge branch 'main' into upgrade-latest-flask-version
2 parents 45ab77b + bd7f202 commit e05be0f

File tree

95 files changed

+2041
-2422
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+2041
-2422
lines changed

.github/workflows/system-tests.yml

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,6 @@ on:
1111
- cron: '00 04 * * 2-6'
1212

1313
jobs:
14-
system-tests-build-agent:
15-
runs-on: ubuntu-latest
16-
steps:
17-
18-
- name: Checkout system tests
19-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20-
with:
21-
persist-credentials: false
22-
repository: 'DataDog/system-tests'
23-
# Automatically managed, use scripts/update-system-tests-version to update
24-
ref: '64afc5f8c1c2aa19adefdf1abf5559b62af8e784'
25-
26-
- name: Build agent
27-
run: ./build.sh -i agent
28-
29-
- name: Save
30-
id: save
31-
run: |
32-
docker image save system_tests/agent:latest | gzip > agent_${{ github.sha }}.tar.gz
33-
34-
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
35-
with:
36-
name: agent_${{ github.sha }}
37-
path: |
38-
agent_${{ github.sha }}.tar.gz
39-
retention-days: 2
40-
4114
system-tests-build-weblog:
4215
runs-on: ubuntu-latest
4316
strategy:
@@ -69,7 +42,7 @@ jobs:
6942
persist-credentials: false
7043
repository: 'DataDog/system-tests'
7144
# Automatically managed, use scripts/update-system-tests-version to update
72-
ref: '64afc5f8c1c2aa19adefdf1abf5559b62af8e784'
45+
ref: '4dac1cb506ee28bdcac297ce3784f1b217679556'
7346

7447
- name: Checkout dd-trace-py
7548
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -98,7 +71,7 @@ jobs:
9871

9972
system-tests:
10073
runs-on: ubuntu-latest
101-
needs: [system-tests-build-agent, system-tests-build-weblog]
74+
needs: [system-tests-build-weblog]
10275
strategy:
10376
matrix:
10477
weblog-variant: [flask-poc, uwsgi-poc , django-poc, fastapi, python3.12, django-py3.13]
@@ -123,7 +96,7 @@ jobs:
12396
persist-credentials: false
12497
repository: 'DataDog/system-tests'
12598
# Automatically managed, use scripts/update-system-tests-version to update
126-
ref: '64afc5f8c1c2aa19adefdf1abf5559b62af8e784'
99+
ref: '4dac1cb506ee28bdcac297ce3784f1b217679556'
127100

128101
- name: Build runner
129102
uses: ./.github/actions/install_runner
@@ -133,16 +106,10 @@ jobs:
133106
name: ${{ matrix.weblog-variant }}_${{ github.sha }}
134107
path: images_artifacts/
135108

136-
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
137-
with:
138-
name: agent_${{ github.sha }}
139-
path: images_artifacts/
140-
141109
- name: docker load
142110
id: docker_load
143111
run: |
144112
docker load < images_artifacts/${{ matrix.weblog-variant}}_weblog_${{ github.sha }}.tar.gz
145-
docker load < images_artifacts/agent_${{ github.sha }}.tar.gz
146113
147114
- name: Run DEFAULT
148115
if: always() && steps.docker_load.outcome == 'success' && matrix.scenario == 'other'
@@ -310,7 +277,7 @@ jobs:
310277
persist-credentials: false
311278
repository: 'DataDog/system-tests'
312279
# Automatically managed, use scripts/update-system-tests-version to update
313-
ref: '64afc5f8c1c2aa19adefdf1abf5559b62af8e784'
280+
ref: '4dac1cb506ee28bdcac297ce3784f1b217679556'
314281
- name: Checkout dd-trace-py
315282
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
316283
with:

ddtrace/_monkey.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def _patch_all(**patch_modules: bool) -> None:
352352

353353
patch(raise_errors=False, **modules)
354354
if asm_config._iast_enabled:
355-
from ddtrace.appsec._iast._patch_modules import patch_iast
355+
from ddtrace.appsec._iast.main import patch_iast
356356
from ddtrace.appsec.iast import enable_iast_propagation
357357

358358
patch_iast()

ddtrace/appsec/_common_module_patches.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,32 @@ def _(module):
4242
subprocess_patch.patch()
4343
subprocess_patch.add_str_callback(_RASP_SYSTEM, wrapped_system_5542593D237084A7)
4444
subprocess_patch.add_lst_callback(_RASP_POPEN, popen_FD233052260D8B4D)
45+
log.debug("Patching common modules: subprocess_patch")
4546

4647
if _is_patched:
4748
return
4849

4950
try_wrap_function_wrapper("builtins", "open", wrapped_open_CFDDB7ABBA9081B6)
5051
try_wrap_function_wrapper("urllib.request", "OpenerDirector.open", wrapped_open_ED4CF71136E15EBF)
5152
core.on("asm.block.dbapi.execute", execute_4C9BAC8E228EB347)
53+
log.debug("Patching common modules: builtins and urllib.request")
5254
_is_patched = True
5355

5456

5557
def unpatch_common_modules():
5658
global _is_patched
5759
if not _is_patched:
5860
return
61+
5962
try_unwrap("builtins", "open")
6063
try_unwrap("urllib.request", "OpenerDirector.open")
6164
try_unwrap("_io", "BytesIO.read")
6265
try_unwrap("_io", "StringIO.read")
66+
subprocess_patch.unpatch()
6367
subprocess_patch.del_str_callback(_RASP_SYSTEM)
6468
subprocess_patch.del_lst_callback(_RASP_POPEN)
69+
70+
log.debug("Unpatching common modules subprocess, builtins and urllib.request")
6571
_is_patched = False
6672

6773

@@ -323,7 +329,7 @@ def try_unwrap(module, name):
323329
apply_patch(parent, attribute, original)
324330
del _DD_ORIGINAL_ATTRIBUTES[(parent, attribute)]
325331
except ModuleNotFoundError:
326-
pass
332+
log.debug("ERROR unwrapping %s.%s ", module, name)
327333

328334

329335
def try_wrap_function_wrapper(module_name: str, name: str, wrapper: Callable) -> None:
@@ -332,7 +338,7 @@ def _(module):
332338
try:
333339
wrap_object(module, name, FunctionWrapper, (wrapper,))
334340
except (ImportError, AttributeError):
335-
log.debug("ASM patching. Module %s.%s does not exist", module_name, name)
341+
log.debug("Module %s.%s does not exist", module_name, name)
336342

337343

338344
def wrap_object(module, name, factory, args=(), kwargs=None):

ddtrace/appsec/_handlers.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import io
22
import json
3+
from typing import Any
4+
from typing import Dict
5+
from typing import Optional
36

47
import xmltodict
58

9+
from ddtrace._trace.span import Span
10+
from ddtrace.appsec._asm_request_context import _call_waf
11+
from ddtrace.appsec._asm_request_context import _call_waf_first
612
from ddtrace.appsec._asm_request_context import get_blocked
713
from ddtrace.appsec._constants import SPAN_DATA_NAMES
14+
from ddtrace.appsec._http_utils import extract_cookies_from_headers
15+
from ddtrace.appsec._http_utils import normalize_headers
16+
from ddtrace.appsec._http_utils import parse_http_body
817
from ddtrace.contrib import trace_utils
918
from ddtrace.contrib.internal.trace_utils_base import _get_request_header_user_agent
1019
from ddtrace.contrib.internal.trace_utils_base import _set_url_tag
11-
from ddtrace.ext import SpanTypes
1220
from ddtrace.ext import http
1321
from ddtrace.internal import core
1422
from ddtrace.internal.constants import RESPONSE_HEADERS
@@ -53,7 +61,7 @@ def _on_set_http_meta(
5361
response_headers,
5462
response_cookies,
5563
):
56-
if asm_config._asm_enabled and span.span_type == SpanTypes.WEB:
64+
if asm_config._asm_enabled and span.span_type in asm_config._asm_http_span_types:
5765
# avoid circular import
5866
from ddtrace.appsec._asm_request_context import set_waf_address
5967

@@ -77,6 +85,74 @@ def _on_set_http_meta(
7785
set_waf_address(k, v)
7886

7987

88+
# AWS Lambda
89+
def _on_lambda_start_request(
90+
span: Span,
91+
request_headers: Dict[str, str],
92+
request_ip: Optional[str],
93+
body: Optional[str],
94+
is_body_base64: bool,
95+
raw_uri: str,
96+
route: str,
97+
method: str,
98+
parsed_query: Dict[str, Any],
99+
):
100+
if not (asm_config._asm_enabled and span.span_type in asm_config._asm_http_span_types):
101+
return
102+
103+
headers = normalize_headers(request_headers)
104+
request_body = parse_http_body(headers, body, is_body_base64)
105+
request_cookies = extract_cookies_from_headers(headers)
106+
107+
_on_set_http_meta(
108+
span,
109+
request_ip,
110+
raw_uri,
111+
route,
112+
method,
113+
headers,
114+
request_cookies,
115+
parsed_query,
116+
None,
117+
request_body,
118+
None,
119+
None,
120+
None,
121+
)
122+
123+
_call_waf_first(("aws_lambda",))
124+
125+
126+
def _on_lambda_start_response(
127+
span: Span,
128+
status_code: str,
129+
response_headers: Dict[str, str],
130+
):
131+
if not (asm_config._asm_enabled and span.span_type in asm_config._asm_http_span_types):
132+
return
133+
134+
waf_headers = normalize_headers(response_headers)
135+
response_cookies = extract_cookies_from_headers(waf_headers)
136+
137+
_on_set_http_meta(
138+
span,
139+
None,
140+
None,
141+
None,
142+
None,
143+
None,
144+
None,
145+
None,
146+
None,
147+
None,
148+
status_code,
149+
waf_headers,
150+
response_cookies,
151+
)
152+
153+
_call_waf(("aws_lambda",))
154+
155+
80156
# ASGI
81157

82158

@@ -307,6 +383,9 @@ def listen():
307383

308384
core.on("asgi.request.parse.body", _on_asgi_request_parse_body, "await_receive_and_body")
309385

386+
core.on("aws_lambda.start_request", _on_lambda_start_request)
387+
core.on("aws_lambda.start_response", _on_lambda_start_response)
388+
310389
core.on("grpc.server.response.message", _on_grpc_server_response)
311390
core.on("grpc.server.data", _on_grpc_server_data)
312391

ddtrace/appsec/_http_utils.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import base64
2+
from http.cookies import SimpleCookie
3+
import json
4+
from typing import Any
5+
from typing import Dict
6+
from typing import Optional
7+
from typing import Union
8+
from urllib.parse import parse_qs
9+
10+
import xmltodict
11+
12+
from ddtrace.internal.utils import http as http_utils
13+
14+
15+
def normalize_headers(
16+
request_headers: Dict[str, str],
17+
) -> Dict[str, Optional[str]]:
18+
"""Normalize headers according to the WAF expectations.
19+
20+
The WAF expects headers to be lowercased and empty values to be None.
21+
"""
22+
headers: Dict[str, Optional[str]] = {}
23+
for key, value in request_headers.items():
24+
normalized_key = http_utils.normalize_header_name(key)
25+
if value:
26+
headers[normalized_key] = str(value).strip()
27+
else:
28+
headers[normalized_key] = None
29+
return headers
30+
31+
32+
def parse_http_body(
33+
normalized_headers: Dict[str, Optional[str]],
34+
body: Optional[str],
35+
is_body_base64: bool,
36+
) -> Union[str, Dict[str, Any], None]:
37+
"""Parse a request body based on the content-type header."""
38+
if body is None:
39+
return None
40+
if is_body_base64:
41+
try:
42+
body = base64.b64decode(body).decode()
43+
except (ValueError, TypeError):
44+
return None
45+
46+
try:
47+
content_type = normalized_headers.get("content-type")
48+
if not content_type:
49+
return None
50+
51+
if content_type in ("application/json", "application/vnd.api+json", "text/json"):
52+
return json.loads(body)
53+
elif content_type in ("application/x-url-encoded", "application/x-www-form-urlencoded"):
54+
return parse_qs(body)
55+
elif content_type in ("application/xml", "text/xml"):
56+
return xmltodict.parse(body)
57+
elif content_type.startswith("multipart/form-data"):
58+
return http_utils.parse_form_multipart(body, normalized_headers)
59+
elif content_type == "text/plain":
60+
return body
61+
else:
62+
return None
63+
64+
except Exception:
65+
return None
66+
67+
68+
def extract_cookies_from_headers(
69+
normalized_headers: Dict[str, Optional[str]],
70+
) -> Optional[Dict[str, str]]:
71+
"""Extract cookies from the WAF headers."""
72+
cookie_names = {"cookie", "set-cookie"}
73+
for name in cookie_names:
74+
if name in normalized_headers:
75+
cookie = SimpleCookie()
76+
header = normalized_headers[name]
77+
del normalized_headers[name]
78+
if header:
79+
cookie.load(header)
80+
return {k: v.value for k, v in cookie.items()}
81+
return None

0 commit comments

Comments
 (0)