Skip to content

Commit c7bab68

Browse files
authored
Merge branch '1.x' into feat/debugging-py311-support
2 parents efc7b32 + a6ff0c3 commit c7bab68

24 files changed

+406
-139
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ ddtrace/appsec/ @DataDog/asm-python
2828
tests/appsec/ @DataDog/asm-python
2929
ddtrace/internal/remoteconfig @DataDog/apm-core-python @DataDog/asm-python
3030
tests/internal/remoteconfig @DataDog/apm-core-python @DataDog/asm-python
31+
tests/contrib/flask/test_flask_appsec.py @DataDog/asm-python
32+
tests/contrib/django/test_django_appsec.py @DataDog/asm-python
3133

3234
# Profiling
3335
ddtrace/profiling @DataDog/apm-core-python @DataDog/debugger-python

ddtrace/appsec/_remoteconfiguration.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010
if TYPE_CHECKING: # pragma: no cover
1111
from typing import Any
1212
from typing import Callable
13+
14+
try:
15+
from typing import Literal
16+
except ImportError:
17+
# Python < 3.8. The "type ignore" is to avoid a runtime check just to silence mypy.
18+
from typing_extensions import Literal # type: ignore
1319
from typing import Mapping
1420
from typing import Optional
21+
from typing import Union
1522

1623
from ddtrace import Tracer
1724
from ddtrace.internal.remoteconfig.client import ConfigMetadata
@@ -26,9 +33,46 @@ def enable_appsec_rc():
2633

2734
if _appsec_rc_features_is_enabled():
2835
from ddtrace.internal.remoteconfig import RemoteConfig
36+
from ddtrace.internal.remoteconfig.constants import ASM_DATA_PRODUCT
2937
from ddtrace.internal.remoteconfig.constants import ASM_FEATURES_PRODUCT
3038

3139
RemoteConfig.register(ASM_FEATURES_PRODUCT, appsec_rc_reload_features(tracer))
40+
RemoteConfig.register(ASM_DATA_PRODUCT, appsec_rc_reload_features(tracer))
41+
42+
43+
def _appsec_rules_data(tracer, features):
44+
# type: (Tracer, Union[Literal[False], Mapping[str, Any]]) -> None
45+
if features and tracer._appsec_processor:
46+
rules_data = features.get("rules_data", [])
47+
if rules_data:
48+
log.debug("Reloading Appsec rules data: %s", rules_data)
49+
tracer._appsec_processor._update_rules(rules_data)
50+
51+
52+
def _appsec_1click_activation(tracer, features):
53+
# type: (Tracer, Union[Literal[False], Mapping[str, Any]]) -> None
54+
if features is False:
55+
rc_appsec_enabled = False
56+
else:
57+
rc_appsec_enabled = features.get("asm", {}).get("enabled")
58+
59+
if rc_appsec_enabled is not None:
60+
from ddtrace.internal.remoteconfig import RemoteConfig
61+
from ddtrace.internal.remoteconfig.constants import ASM_DATA_PRODUCT
62+
63+
log.debug("Reloading Appsec 1-click: %s", rc_appsec_enabled)
64+
_appsec_enabled = True
65+
66+
if not (APPSEC_ENV not in os.environ and rc_appsec_enabled) and (
67+
not asbool(os.environ.get(APPSEC_ENV)) or not rc_appsec_enabled
68+
):
69+
_appsec_enabled = False
70+
RemoteConfig.unregister(ASM_DATA_PRODUCT)
71+
else:
72+
RemoteConfig.register(ASM_DATA_PRODUCT, appsec_rc_reload_features(tracer))
73+
74+
if tracer._appsec_enabled != _appsec_enabled:
75+
tracer.configure(appsec_enabled=_appsec_enabled)
3276

3377

3478
def appsec_rc_reload_features(tracer):
@@ -50,16 +94,8 @@ def _reload_features(metadata, features):
5094
"""
5195

5296
if features is not None:
53-
log.debug("Reloading appsec rc: %s", features)
54-
rc_appsec_enabled = features.get("asm", {}).get("enabled") if features is not False else False
55-
56-
_appsec_enabled = True
57-
58-
if not (APPSEC_ENV not in os.environ and rc_appsec_enabled is True) and (
59-
asbool(os.environ.get(APPSEC_ENV)) is False or rc_appsec_enabled is False
60-
):
61-
_appsec_enabled = False
62-
63-
tracer.configure(appsec_enabled=_appsec_enabled)
97+
log.debug("Updating ASM Remote Configuration: %s", features)
98+
_appsec_rules_data(tracer, features)
99+
_appsec_1click_activation(tracer, features)
64100

65101
return _reload_features

ddtrace/appsec/processor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import os.path
5+
from typing import Any
56
from typing import List
67
from typing import Set
78
from typing import TYPE_CHECKING
@@ -210,6 +211,13 @@ def __attrs_post_init__(self):
210211
# we always need the response headers
211212
self._mark_needed(_Addresses.SERVER_RESPONSE_HEADERS_NO_COOKIES)
212213

214+
def _update_rules(self, new_rules):
215+
# type: (List[Dict[str, Any]]) -> None
216+
try:
217+
self._ddwaf.update_rules(new_rules)
218+
except TypeError:
219+
log.debug("Error updating ASM rules", exc_info=True)
220+
213221
def on_span_start(self, span):
214222
# type: (Span) -> None
215223
pass

ddtrace/appsec/utils.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import base64
12
import os
3+
import sys
24

35
from ddtrace.constants import APPSEC_ENV
6+
from ddtrace.internal.compat import to_bytes_py2
47
from ddtrace.internal.utils.formats import asbool
58

69

@@ -13,13 +16,32 @@ def _appsec_rc_features_is_enabled():
1316

1417
def _appsec_rc_capabilities():
1518
# type: () -> str
16-
"""return the bit of the composed capabilities in base64
19+
r"""return the bit representation of the composed capabilities in base64
1720
bit 0: Reserved
18-
bit 1: ASM Activation
21+
bit 1: ASM 1-click Activation
1922
bit 2: ASM Ip blocking
2023
21-
TODO: refactor to compose the string and encode it
24+
Int Number -> binary number -> bytes representation -> base64 representation
25+
ASM Activation:
26+
2 -> 10 -> b'\x02' -> "Ag=="
27+
ASM Ip blocking:
28+
4 -> 100 -> b'\x04' -> "BA=="
29+
ASM Activation and ASM Ip blocking:
30+
6 -> 110 -> b'\x06' -> "Bg=="
31+
...
32+
256 -> 100000000 -> b'\x01\x00' -> b'AQA='
2233
"""
34+
value = 0b0
35+
2336
if _appsec_rc_features_is_enabled():
24-
return "Ag=="
25-
return ""
37+
value |= 1 << 1
38+
value |= 1 << 2
39+
40+
result = ""
41+
if sys.version_info.major < 3:
42+
bytes_res = to_bytes_py2(value, (value.bit_length() + 7) // 8, "big")
43+
result = base64.b64encode(bytes_res) # type: ignore[assignment, arg-type]
44+
else:
45+
result = str(base64.b64encode(value.to_bytes((value.bit_length() + 7) // 8, "big")), encoding="utf-8")
46+
47+
return result

ddtrace/contrib/gevent/patch.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def patch():
2525
This action ensures that if a user extends the ``Greenlet``
2626
class, the ``TracedGreenlet`` is used as a parent class.
2727
"""
28+
if getattr(gevent, "__datadog_patch", False):
29+
return
30+
setattr(gevent, "__datadog_patch", True)
31+
2832
_replace(TracedGreenlet, TracedIMap, TracedIMapUnordered)
2933
ddtrace.tracer.configure(context_provider=GeventContextProvider())
3034

@@ -35,6 +39,10 @@ def unpatch():
3539
before executing application code, otherwise the ``DatadogGreenlet``
3640
class may be used during initialization.
3741
"""
42+
if not getattr(gevent, "__datadog_patch", False):
43+
return
44+
setattr(gevent, "__datadog_patch", False)
45+
3846
_replace(__Greenlet, __IMap, __IMapUnordered)
3947
ddtrace.tracer.configure(context_provider=DefaultContextProvider())
4048

ddtrace/internal/compat.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,23 @@ def maybe_stringify(obj):
275275
return None
276276

277277

278+
def to_bytes_py2(n, length, byteorder):
279+
# type: (int, int, str) -> Text
280+
"""
281+
Convert a string to bytes in the format expected by the remote config
282+
capabilities string, considering the byteorder, which is needed
283+
for Python 2.
284+
"""
285+
if byteorder == "little":
286+
order = range(length)
287+
elif byteorder == "big":
288+
order = reversed(range(length)) # type: ignore[assignment]
289+
else:
290+
raise ValueError("byteorder must be either 'little' or 'big'")
291+
292+
return "".join(chr((n >> i * 8) & 0xFF) for i in order)
293+
294+
278295
NoneType = type(None)
279296

280297
BUILTIN_SIMPLE_TYPES = frozenset([int, float, str, bytes, bool, NoneType, type, long])

ddtrace/internal/remoteconfig/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ def register(cls, product, handler):
6363
except Exception:
6464
log.warning("error starting the RCM client", exc_info=True)
6565

66+
@classmethod
67+
def unregister(cls, product):
68+
try:
69+
cls._worker._client.unregister_product(product)
70+
except Exception:
71+
log.warning("error starting the RCM client", exc_info=True)
72+
6673
@classmethod
6774
def disable(cls):
6875
# type: () -> None

ddtrace/internal/remoteconfig/client.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from ddtrace.internal.runtime import container
2626
from ddtrace.internal.utils.time import parse_isoformat
2727

28+
from ..utils.version import _pep440_to_semver
29+
2830

2931
if TYPE_CHECKING: # pragma: no cover
3032
from typing import Callable
@@ -187,33 +189,6 @@ class RemoteConfigClient(object):
187189
and dispatches configurations to registered products.
188190
"""
189191

190-
@staticmethod
191-
def _get_version():
192-
# type: () -> str
193-
# The library uses a PEP 440-compliant versioning scheme, but the
194-
# RCM spec requires that we use a SemVer-compliant version.
195-
#
196-
# However, we may have versions like:
197-
#
198-
# - 1.7.1.dev3+gf258c7d9
199-
# - 1.7.1rc2.dev3+gf258c7d9
200-
#
201-
# Which are not Semver-compliant.
202-
#
203-
# The easiest fix is to replace the first occurrence of "rc" or
204-
# ".dev" with "-rc" or "-dev" to make them compliant.
205-
#
206-
# Other than X.Y.Z, we are allowed `-<dot separated pre-release>+<build identifier>`
207-
# https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
208-
#
209-
# e.g. 1.7.1-rc2.dev3+gf258c7d9 is valid
210-
tracer_version = ddtrace.__version__
211-
if "rc" in tracer_version:
212-
tracer_version = tracer_version.replace("rc", "-rc", 1)
213-
elif ".dev" in tracer_version:
214-
tracer_version = tracer_version.replace(".dev", "-dev", 1)
215-
return tracer_version
216-
217192
def __init__(self):
218193
# type: () -> None
219194
self.id = str(uuid.uuid4())
@@ -230,7 +205,7 @@ def __init__(self):
230205
self._client_tracer = dict(
231206
runtime_id=runtime.get_runtime_id(),
232207
language="python",
233-
tracer_version=self._get_version(),
208+
tracer_version=_pep440_to_semver(),
234209
service=ddtrace.config.service,
235210
env=ddtrace.config.env,
236211
app_version=ddtrace.config.version,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
ASM_FEATURES_PRODUCT = "ASM_FEATURES"
2+
ASM_DATA_PRODUCT = "ASM_DATA"
23
REMOTE_CONFIG_AGENT_ENDPOINT = "v0.7/config"

ddtrace/internal/telemetry/writer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ..periodic import PeriodicService
1717
from ..runtime import get_runtime_id
1818
from ..service import ServiceStatus
19+
from ..utils.formats import asbool
1920
from ..utils.formats import parse_tags_str
2021
from ..utils.time import StopWatch
2122
from .data import get_application
@@ -54,11 +55,14 @@ def __init__(self, agent_url=None):
5455
self._integrations_queue = [] # type: List[Dict]
5556
self._lock = forksafe.Lock() # type: forksafe.ResetObject
5657
self._forked = False # type: bool
58+
# Debug flag that enables payload debug mode.
59+
self._debug = asbool(os.environ.get("DD_TELEMETRY_DEBUG", "false"))
5760
forksafe.register(self._fork_writer)
5861

5962
self._headers = {
6063
"Content-type": "application/json",
6164
"DD-Telemetry-API-Version": "v1",
65+
"DD-Telemetry-Debug-Enabled": str(self._debug).lower(),
6266
} # type: Dict[str, str]
6367
additional_header_str = os.environ.get("_DD_TELEMETRY_WRITER_ADDITIONAL_HEADERS")
6468
if additional_header_str is not None:
@@ -254,6 +258,7 @@ def _create_telemetry_request(self, payload, payload_type, sequence_id):
254258
"runtime_id": get_runtime_id(),
255259
"api_version": "v1",
256260
"seq_id": sequence_id,
261+
"debug": self._debug,
257262
"application": get_application(config.service, config.version, config.env),
258263
"host": get_host_info(),
259264
"payload": payload,

ddtrace/internal/utils/version.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import typing
2+
from typing import Optional
23

34
import packaging.version
45

6+
from ddtrace.version import get_version
7+
58

69
def parse_version(version):
710
# type: (str) -> typing.Tuple[int, int, int]
@@ -47,3 +50,31 @@ def parse_version(version):
4750
parsed.release[1] if len(parsed.release) >= 2 else 0,
4851
parsed.release[2] if len(parsed.release) >= 3 else 0,
4952
)
53+
54+
55+
def _pep440_to_semver(version=None):
56+
# type: (Optional[str]) -> str
57+
# The library uses a PEP 440-compliant (https://peps.python.org/pep-0440/) versioning
58+
# scheme, but the RCM spec requires that we use a SemVer-compliant version.
59+
#
60+
# However, we may have versions like:
61+
#
62+
# - 1.7.1.dev3+gf258c7d9
63+
# - 1.7.1rc2.dev3+gf258c7d9
64+
#
65+
# Which are not Semver-compliant.
66+
#
67+
# The easiest fix is to replace the first occurrence of "rc" or
68+
# ".dev" with "-rc" or "-dev" to make them compliant.
69+
#
70+
# Other than X.Y.Z, we are allowed `-<dot separated pre-release>+<build identifier>`
71+
# https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
72+
#
73+
# e.g. 1.7.1-rc2.dev3+gf258c7d9 is valid
74+
75+
tracer_version = version or get_version()
76+
if "rc" in tracer_version and "-rc" not in tracer_version:
77+
tracer_version = tracer_version.replace("rc", "-rc", 1)
78+
elif ".dev" in tracer_version:
79+
tracer_version = tracer_version.replace(".dev", "-dev", 1)
80+
return tracer_version

0 commit comments

Comments
 (0)