diff --git a/ddtrace/__init__.py b/ddtrace/__init__.py index 835291fadb7..b555d1117ca 100644 --- a/ddtrace/__init__.py +++ b/ddtrace/__init__.py @@ -1,4 +1,5 @@ import sys +import os import warnings @@ -42,29 +43,29 @@ # initialization, which added this module to sys.modules. We catch deprecation # warnings as this is only to retain a side effect of the package # initialization. +# TODO: Remove this in v3.0 when the ddtrace/tracer.py module is removed with warnings.catch_warnings(): warnings.simplefilter("ignore") from .tracer import Tracer as _ - __version__ = get_version() -# a global tracer instance with integration settings -tracer = Tracer() +# TODO: Deprecate accessing tracer from ddtrace.__init__ module in v4.0 +if os.environ.get("_DD_GLOBAL_TRACER_INIT", "true").lower() in ("1", "true"): + from ddtrace.trace import tracer # noqa: F401 __all__ = [ "patch", "patch_all", "Pin", "Span", - "tracer", "Tracer", "config", "DDTraceDeprecationWarning", ] -_DEPRECATED_MODULE_ATTRIBUTES = [ +_DEPRECATED_TRACE_ATTRIBUTES = [ "Span", "Tracer", "Pin", @@ -72,10 +73,12 @@ def __getattr__(name): - if name in _DEPRECATED_MODULE_ATTRIBUTES: + if name in _DEPRECATED_TRACE_ATTRIBUTES: debtcollector.deprecate( ("%s.%s is deprecated" % (__name__, name)), + message="Import from ddtrace.trace instead.", category=DDTraceDeprecationWarning, + removal_version="3.0.0", ) if name in globals(): diff --git a/ddtrace/_trace/pin.py b/ddtrace/_trace/pin.py index d12303a57ea..7dd83474749 100644 --- a/ddtrace/_trace/pin.py +++ b/ddtrace/_trace/pin.py @@ -6,6 +6,7 @@ import wrapt import ddtrace +from ddtrace.vendor.debtcollector import deprecate from ..internal.logger import get_logger @@ -41,6 +42,12 @@ def __init__( _config=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> None + if tracer is not None and tracer is not ddtrace.tracer: + deprecate( + "Initializing ddtrace.Pin with `tracer` argument is deprecated", + message="All Pin instances should use the global tracer instance", + removal_version="3.0.0", + ) tracer = tracer or ddtrace.tracer self.tags = tags self.tracer = tracer @@ -72,15 +79,15 @@ def __repr__(self): def _find(*objs): # type: (Any) -> Optional[Pin] """ - Return the first :class:`ddtrace.trace.Pin` found on any of the provided objects or `None` if none were found + Return the first :class:`ddtrace.pin.Pin` found on any of the provided objects or `None` if none were found >>> pin = Pin._find(wrapper, instance, conn) - :param objs: The objects to search for a :class:`ddtrace.trace.Pin` on + :param objs: The objects to search for a :class:`ddtrace.pin.Pin` on :type objs: List of objects - :rtype: :class:`ddtrace.trace.Pin`, None - :returns: The first found :class:`ddtrace.trace.Pin` or `None` is none was found + :rtype: :class:`ddtrace.pin.Pin`, None + :returns: The first found :class:`ddtrace.pin.Pin` or `None` is none was found """ for obj in objs: pin = Pin.get_from(obj) @@ -98,10 +105,10 @@ def get_from(obj): >>> pin = Pin.get_from(conn) - :param obj: The object to look for a :class:`ddtrace.trace.Pin` on + :param obj: The object to look for a :class:`ddtrace.pin.Pin` on :type obj: object - :rtype: :class:`ddtrace.trace.Pin`, None - :returns: :class:`ddtrace.trace.Pin` associated with the object or None + :rtype: :class:`ddtrace.pin.Pin`, None + :returns: :class:`ddtrace.pin.Pin` associated with the object, or None if none was found """ if hasattr(obj, "__getddpin__"): return obj.__getddpin__() @@ -132,6 +139,12 @@ def override( >>> # Override a pin for a specific connection >>> Pin.override(conn, service='user-db') """ + if tracer is not None: + deprecate( + "Calling ddtrace.Pin.override(...) with the `tracer` argument is deprecated", + message="All Pin instances should use the global tracer instance", + removal_version="3.0.0", + ) if not obj: return @@ -193,6 +206,13 @@ def clone( if not tags and self.tags: tags = self.tags.copy() + if tracer is not None: + deprecate( + "Initializing ddtrace.Pin with `tracer` argument is deprecated", + message="All Pin instances should use the global tracer instance", + removal_version="3.0.0", + ) + # we use a copy instead of a deepcopy because we expect configurations # to have only a root level dictionary without nested objects. Using # deepcopy introduces a big overhead: diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 83d2d580c2d..5bde36ef480 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -18,6 +18,7 @@ from ddtrace import _hooks from ddtrace import config from ddtrace._trace.context import Context +from ddtrace._trace.filters import TraceFilter from ddtrace._trace.processor import SpanAggregator from ddtrace._trace.processor import SpanProcessor from ddtrace._trace.processor import TopLevelSpanProcessor @@ -68,7 +69,6 @@ from ddtrace.settings import Config from ddtrace.settings.asm import config as asm_config from ddtrace.settings.peer_service import _ps_config -from ddtrace.trace import TraceFilter from ddtrace.vendor.debtcollector import deprecate diff --git a/ddtrace/appsec/_constants.py b/ddtrace/appsec/_constants.py index d16de0e1379..92b9e239900 100644 --- a/ddtrace/appsec/_constants.py +++ b/ddtrace/appsec/_constants.py @@ -182,6 +182,7 @@ class WAF_DATA_NAMES(metaclass=Constant_Class): REQUEST_COOKIES: Literal["server.request.cookies"] = "server.request.cookies" REQUEST_HTTP_IP: Literal["http.client_ip"] = "http.client_ip" REQUEST_USER_ID: Literal["usr.id"] = "usr.id" + REQUEST_USERNAME: Literal["usr.login"] = "usr.login" RESPONSE_STATUS: Literal["server.response.status"] = "server.response.status" RESPONSE_HEADERS_NO_COOKIES: Literal["server.response.headers.no_cookies"] = "server.response.headers.no_cookies" RESPONSE_BODY: Literal["server.response.body"] = "server.response.body" @@ -196,6 +197,7 @@ class WAF_DATA_NAMES(metaclass=Constant_Class): REQUEST_COOKIES, REQUEST_HTTP_IP, REQUEST_USER_ID, + REQUEST_USERNAME, RESPONSE_STATUS, RESPONSE_HEADERS_NO_COOKIES, RESPONSE_BODY, diff --git a/ddtrace/appsec/_iast/_ast/ast_patching.py b/ddtrace/appsec/_iast/_ast/ast_patching.py index c4a224c2a02..bb3a9c74d44 100644 --- a/ddtrace/appsec/_iast/_ast/ast_patching.py +++ b/ddtrace/appsec/_iast/_ast/ast_patching.py @@ -9,6 +9,7 @@ from types import ModuleType from typing import Iterable from typing import Optional +from typing import Set from typing import Text from typing import Tuple @@ -26,57 +27,43 @@ _PREFIX = IAST.PATCH_ADDED_SYMBOL_PREFIX # Prefixes for modules where IAST patching is allowed -IAST_ALLOWLIST: Tuple[Text, ...] = ("tests.appsec.iast.",) +# Only packages that have the test_propagation=True in test_packages and are not in the denylist must be here +IAST_ALLOWLIST: Tuple[Text, ...] = ( + "attrs.", + "beautifulsoup4.", + "cachetools.", + "cryptography.", + "docutils.", + "idna.", + "iniconfig.", + "jinja2.", + "lxml.", + "multidict.", + "platformdirs", + "pygments.", + "pynacl.", + "pyparsing.", + "multipart", + "sqlalchemy.", + "tomli", + "yarl.", +) + +# NOTE: For testing reasons, don't add astunparse here, see test_ast_patching.py IAST_DENYLIST: Tuple[Text, ...] = ( - "altgraph.", - "dipy.", - "black.", - "mypy.", - "mypy_extensions.", - "autopep8.", - "pycodestyle.", - "pydicom.", - "pyinstaller.", - "pystray.", - "contourpy.", - "cx_logging.", - "dateutil.", - "pytz.", - "wcwidth.", - "win32ctypes.", - "xlib.", - "cycler.", - "cython.", - "dnspython.", - "elasticdeform.", - "numpy.", - "matplotlib.", - "skbase.", - "scipy.", - "networkx.", - "imageio.", - "fonttools.", - "nibabel.", - "nilearn.", - "gprof2dot.", - "h5py.", - "kiwisolver.", - "pandas.", - "pdf2image.", - "pefile.", - "pil.", - "threadpoolctl.", - "tifffile.", - "tqdm.", - "trx.", - "flask.", - "werkzeug.", + "_psycopg.", # PostgreSQL adapter for Python (v3) + "_pytest.", "aiohttp._helpers.", "aiohttp._http_parser.", "aiohttp._http_writer.", "aiohttp._websocket.", "aiohttp.log.", "aiohttp.tcp_helpers.", + "aioquic.", + "altgraph.", + "anyio.", + "api_pb2.", # Patching crashes with these auto-generated modules, propagation is not needed + "api_pb2_grpc.", # Patching crashes with these auto-generated modules, propagation is not needed "asyncio.base_events.", "asyncio.base_futures.", "asyncio.base_subprocess.", @@ -99,11 +86,15 @@ "asyncio.transports.", "asyncio.trsock.", "asyncio.unix_events.", + "asyncpg.pgproto.", "attr._config.", "attr._next_gen.", "attr.filters.", "attr.setters.", + "autopep8.", "backports.", + "black.", + "blinker.", "boto3.docs.docstring.", "boto3.s3.", "botocore.docs.bcdoc.", @@ -111,6 +102,8 @@ "botocore.vendored.requests.", "brotli.", "brotlicffi.", + "bytecode.", + "cattrs.", "cchardet.", "certifi.", "cffi.", @@ -145,14 +138,23 @@ "colorama.", "concurrent.futures.", "configparser.", + "contourpy.", "coreschema.", "crispy_forms.", + "crypto.", # This module is patched by the IAST patch methods, propagation is not needed + "cx_logging.", + "cycler.", + "cython.", + "dateutil.", "dateutil.", + "ddsketch.", + "ddtrace.", "defusedxml.", + "deprecated.", "difflib.", "dill.info.", "dill.settings.", - "silk.", # django-silk package + "dipy.", "django.apps.config.", "django.apps.registry.", "django.conf.", @@ -298,72 +300,87 @@ "django_filters.rest_framework.filterset.", "django_filters.utils.", "django_filters.widgets.", - "crypto.", # This module is patched by the IAST patch methods, propagation is not needed - "deprecated.", - "api_pb2.", # Patching crashes with these auto-generated modules, propagation is not needed - "api_pb2_grpc.", # Patching crashes with these auto-generated modules, propagation is not needed - "asyncpg.pgproto.", - "blinker.", - "bytecode.", - "cattrs.", - "ddsketch.", - "ddtrace.", + "dnspython.", + "elasticdeform.", "envier.", "exceptiongroup.", + "flask.", + "fonttools.", "freezegun.", # Testing utilities for time manipulation + "google.auth.", + "googlecloudsdk.", + "gprof2dot.", + "h11.", + "h5py.", + "httpcore.", + "httptools.", + "httpx.", "hypothesis.", # Testing utilities + "imageio.", "importlib_metadata.", "inspect.", # this package is used to get the stack frames, propagation is not needed "itsdangerous.", + "kiwisolver.", + "matplotlib.", "moto.", # used for mocking AWS, propagation is not needed + "mypy.", + "mypy_extensions.", + "networkx.", + "nibabel.", + "nilearn.", + "numba.", + "numpy.", "opentelemetry-api.", "packaging.", + "pandas.", + "pdf2image.", + "pefile.", + "pil.", "pip.", "pkg_resources.", "pluggy.", "protobuf.", "psycopg.", # PostgreSQL adapter for Python (v3) - "_psycopg.", # PostgreSQL adapter for Python (v3) "psycopg2.", # PostgreSQL adapter for Python (v2) + "pycodestyle.", "pycparser.", # this package is called when a module is imported, propagation is not needed + "pydicom.", + "pyinstaller.", + "pynndescent.", + "pystray.", "pytest.", # Testing framework - "_pytest.", + "pytz.", + "rich.", + "sanic.", + "scipy.", "setuptools.", + "silk.", # django-silk package + "skbase.", "sklearn.", # Machine learning library + "sniffio.", "sqlalchemy.orm.interfaces.", # Performance optimization + "threadpoolctl.", + "tifffile.", + "tqdm.", + "trx.", "typing_extensions.", + "umap.", "unittest.mock.", - "uvloop.", "urlpatterns_reverse.tests.", # assertRaises eat exceptions in native code, so we don't call the original function - "wrapt.", - "zipp.", - # This is a workaround for Sanic failures: + "uvicorn.", + "uvloop.", + "wcwidth.", "websocket.", - "h11.", - "aioquic.", - "httptools.", - "sniffio.", - "sanic.", - "rich.", - "httpx.", "websockets.", - "uvicorn.", - "anyio.", - "httpcore.", - "google.auth.", - "googlecloudsdk.", - "umap.", - "pynndescent.", - "numba.", + "werkzeug.", + "win32ctypes.", + "wrapt.", + "xlib.", + "zipp.", ) - -if IAST.PATCH_MODULES in os.environ: - IAST_ALLOWLIST += tuple(os.environ[IAST.PATCH_MODULES].split(IAST.SEP_MODULES)) - -if IAST.DENY_MODULES in os.environ: - IAST_DENYLIST += tuple(os.environ[IAST.DENY_MODULES].split(IAST.SEP_MODULES)) - +USER_ALLOWLIST = tuple(os.environ.get(IAST.PATCH_MODULES, "").split(IAST.SEP_MODULES)) +USER_DENYLIST = tuple(os.environ.get(IAST.DENY_MODULES, "").split(IAST.SEP_MODULES)) ENCODING = "" @@ -399,6 +416,8 @@ def build_trie(words: Iterable[str]) -> _TrieNode: _TRIE_ALLOWLIST = build_trie(IAST_ALLOWLIST) _TRIE_DENYLIST = build_trie(IAST_DENYLIST) +_TRIE_USER_ALLOWLIST = build_trie(USER_ALLOWLIST) +_TRIE_USER_DENYLIST = build_trie(USER_DENYLIST) def _trie_has_prefix_for(trie: _TrieNode, string: str) -> bool: @@ -429,11 +448,26 @@ def get_encoding(module_path: Text) -> Text: _NOT_PATCH_MODULE_NAMES = {i.lower() for i in _stdlib_for_python_version() | set(builtin_module_names)} +_IMPORTLIB_PACKAGES: Set[str] = set() + def _in_python_stdlib(module_name: str) -> bool: return module_name.split(".")[0].lower() in _NOT_PATCH_MODULE_NAMES +def _is_first_party(module_name: str): + global _IMPORTLIB_PACKAGES + if "vendor." in module_name or "vendored." in module_name: + return False + + if not _IMPORTLIB_PACKAGES: + from ddtrace.internal.packages import get_package_distributions + + _IMPORTLIB_PACKAGES = set(get_package_distributions()) + + return module_name.split(".")[0] not in _IMPORTLIB_PACKAGES + + def _should_iast_patch(module_name: Text) -> bool: """ select if module_name should be patch from the longest prefix that match in allow or deny list. @@ -444,17 +478,30 @@ def _should_iast_patch(module_name: Text) -> bool: # max_deny = max((len(prefix) for prefix in IAST_DENYLIST if module_name.startswith(prefix)), default=-1) # diff = max_allow - max_deny # return diff > 0 or (diff == 0 and not _in_python_stdlib_or_third_party(module_name)) + if _in_python_stdlib(module_name): + log.debug("IAST: denying %s. it's in the _in_python_stdlib", module_name) + return False + + if _is_first_party(module_name): + return True + + # else: third party. Check that is in the allow list and not in the deny list dotted_module_name = module_name.lower() + "." + + # User allow or deny list set by env var have priority + if _trie_has_prefix_for(_TRIE_USER_ALLOWLIST, dotted_module_name): + return True + + if _trie_has_prefix_for(_TRIE_USER_DENYLIST, dotted_module_name): + return False + if _trie_has_prefix_for(_TRIE_ALLOWLIST, dotted_module_name): + if _trie_has_prefix_for(_TRIE_DENYLIST, dotted_module_name): + return False log.debug("IAST: allowing %s. it's in the IAST_ALLOWLIST", module_name) return True - if _trie_has_prefix_for(_TRIE_DENYLIST, dotted_module_name): - log.debug("IAST: denying %s. it's in the IAST_DENYLIST", module_name) - return False - if _in_python_stdlib(module_name): - log.debug("IAST: denying %s. it's in the _in_python_stdlib", module_name) - return False - return True + log.debug("IAST: denying %s. it's in the IAST_DENYLIST", module_name) + return False def visit_ast( diff --git a/ddtrace/appsec/_processor.py b/ddtrace/appsec/_processor.py index 54a9f624afe..030399f8a50 100644 --- a/ddtrace/appsec/_processor.py +++ b/ddtrace/appsec/_processor.py @@ -262,6 +262,7 @@ def _waf_action( custom_data: Optional[Dict[str, Any]] = None, crop_trace: Optional[str] = None, rule_type: Optional[str] = None, + force_sent: bool = False, ) -> Optional[DDWaf_result]: """ Call the `WAF` with the given parameters. If `custom_data_names` is specified as @@ -293,7 +294,7 @@ def _waf_action( force_keys = custom_data.get("PROCESSOR_SETTINGS", {}).get("extract-schema", False) if custom_data else False for key, waf_name in iter_data: # type: ignore[attr-defined] - if key in data_already_sent: + if key in data_already_sent and not force_sent: continue # ensure ephemeral addresses are sent, event when value is None if waf_name not in WAF_DATA_NAMES.PERSISTENT_ADDRESSES and custom_data: diff --git a/ddtrace/appsec/_trace_utils.py b/ddtrace/appsec/_trace_utils.py index 8609344f05a..77cb1aaca3a 100644 --- a/ddtrace/appsec/_trace_utils.py +++ b/ddtrace/appsec/_trace_utils.py @@ -9,11 +9,13 @@ from ddtrace.appsec._asm_request_context import in_asm_context from ddtrace.appsec._constants import APPSEC from ddtrace.appsec._constants import LOGIN_EVENTS_MODE +from ddtrace.appsec._constants import WAF_ACTIONS from ddtrace.appsec._utils import _hash_user_id from ddtrace.contrib.trace_utils import set_user from ddtrace.ext import SpanTypes from ddtrace.ext import user from ddtrace.internal import core +from ddtrace.internal._exceptions import BlockingException from ddtrace.internal.logger import get_logger from ddtrace.settings.asm import config as asm_config @@ -121,21 +123,31 @@ def track_user_login_success_event( real_mode = login_events_mode if login_events_mode != LOGIN_EVENTS_MODE.AUTO else asm_config._user_event_mode if real_mode == LOGIN_EVENTS_MODE.DISABLED: return + initial_login = login + initial_user_id = user_id if real_mode == LOGIN_EVENTS_MODE.ANON: - login = name = email = None + name = email = None + login = None if login is None else _hash_user_id(str(login)) span = _track_user_login_common(tracer, True, metadata, login_events_mode, login, name, email, span) if not span: return - if real_mode == LOGIN_EVENTS_MODE.ANON and isinstance(user_id, str): user_id = _hash_user_id(user_id) - if in_asm_context(): - call_waf_callback(custom_data={"REQUEST_USER_ID": str(user_id), "LOGIN_SUCCESS": real_mode}) - if login_events_mode != LOGIN_EVENTS_MODE.SDK: span.set_tag_str(APPSEC.USER_LOGIN_USERID, str(user_id)) set_user(tracer, user_id, name, email, scope, role, session_id, propagate, span) + if in_asm_context(): + res = call_waf_callback( + custom_data={ + "REQUEST_USER_ID": str(initial_user_id) if initial_user_id else None, + "REQUEST_USERNAME": initial_login, + "LOGIN_SUCCESS": real_mode, + }, + force_sent=True, + ) + if res and any(action in [WAF_ACTIONS.BLOCK_ACTION, WAF_ACTIONS.REDIRECT_ACTION] for action in res.actions): + raise BlockingException(get_blocked()) def track_user_login_failure_event( @@ -159,6 +171,8 @@ def track_user_login_failure_event( real_mode = login_events_mode if login_events_mode != LOGIN_EVENTS_MODE.AUTO else asm_config._user_event_mode if real_mode == LOGIN_EVENTS_MODE.DISABLED: return + if real_mode == LOGIN_EVENTS_MODE.ANON and isinstance(login, str): + login = _hash_user_id(login) span = _track_user_login_common(tracer, False, metadata, login_events_mode, login) if not span: return @@ -265,7 +279,7 @@ def should_block_user(tracer: Tracer, userid: str) -> bool: if get_blocked(): return True - _asm_request_context.call_waf_callback(custom_data={"REQUEST_USER_ID": str(userid)}) + _asm_request_context.call_waf_callback(custom_data={"REQUEST_USER_ID": str(userid)}, force_sent=True) return bool(get_blocked()) diff --git a/ddtrace/contrib/aredis/__init__.py b/ddtrace/contrib/aredis/__init__.py index 8448740104f..1d651b9c616 100644 --- a/ddtrace/contrib/aredis/__init__.py +++ b/ddtrace/contrib/aredis/__init__.py @@ -50,7 +50,7 @@ Instance Configuration ~~~~~~~~~~~~~~~~~~~~~~ -To configure particular aredis instances use the :class:`Pin <ddtrace.Pin>` API:: +To configure particular aredis instances use the :class:`Pin <ddtrace.trace.Pin>` API:: import aredis from ddtrace.trace import Pin diff --git a/ddtrace/contrib/grpc/__init__.py b/ddtrace/contrib/grpc/__init__.py index c95633b4024..8ad2a705233 100644 --- a/ddtrace/contrib/grpc/__init__.py +++ b/ddtrace/contrib/grpc/__init__.py @@ -48,6 +48,7 @@ from ddtrace import patch from ddtrace.trace import Pin + patch(grpc=True) # override the pin on the client @@ -62,7 +63,7 @@ from grpc.framework.foundation import logging_pool from ddtrace import patch - from ddtrace.trace import Pin, Tracer + from ddtrace.trace import Pin patch(grpc=True) diff --git a/ddtrace/contrib/httpx/__init__.py b/ddtrace/contrib/httpx/__init__.py index 28621de44f2..95762604687 100644 --- a/ddtrace/contrib/httpx/__init__.py +++ b/ddtrace/contrib/httpx/__init__.py @@ -57,7 +57,7 @@ Instance Configuration ~~~~~~~~~~~~~~~~~~~~~~ -To configure particular ``httpx`` client instances use the :class:`Pin <ddtrace.Pin>` API:: +To configure particular ``httpx`` client instances use the :class:`Pin <ddtrace.trace.Pin>` API:: import httpx from ddtrace.trace import Pin diff --git a/ddtrace/contrib/internal/django/patch.py b/ddtrace/contrib/internal/django/patch.py index 98a6163a6e5..8bc523dd1c1 100644 --- a/ddtrace/contrib/internal/django/patch.py +++ b/ddtrace/contrib/internal/django/patch.py @@ -17,6 +17,7 @@ import wrapt from wrapt.importer import when_imported +import ddtrace from ddtrace import config from ddtrace.appsec._utils import _UserInfoRetriever from ddtrace.constants import SPAN_KIND @@ -147,7 +148,12 @@ def cursor(django, pin, func, instance, args, kwargs): tags = {"django.db.vendor": vendor, "django.db.alias": alias} tags.update(getattr(conn, "_datadog_tags", {})) - pin = Pin(service, tags=tags, tracer=pin.tracer) + # Calling ddtrace.pin.Pin(...) with the `tracer` argument generates a deprecation warning. + # Remove this if statement when the `tracer` argument is removed + if pin.tracer is ddtrace.tracer: + pin = Pin(service, tags=tags) + else: + pin = Pin(service, tags=tags, tracer=pin.tracer) cursor = func(*args, **kwargs) diff --git a/ddtrace/contrib/internal/mongoengine/trace.py b/ddtrace/contrib/internal/mongoengine/trace.py index c5f3e834aed..93868e096ce 100644 --- a/ddtrace/contrib/internal/mongoengine/trace.py +++ b/ddtrace/contrib/internal/mongoengine/trace.py @@ -23,12 +23,17 @@ class WrappedConnect(wrapt.ObjectProxy): def __init__(self, connect): super(WrappedConnect, self).__init__(connect) - ddtrace.Pin(_SERVICE, tracer=ddtrace.tracer).onto(self) + ddtrace.trace.Pin(_SERVICE).onto(self) def __call__(self, *args, **kwargs): client = self.__wrapped__(*args, **kwargs) - pin = ddtrace.Pin.get_from(self) + pin = ddtrace.trace.Pin.get_from(self) if pin: - ddtrace.Pin(service=pin.service, tracer=pin.tracer).onto(client) + # Calling ddtrace.pin.Pin(...) with the `tracer` argument generates a deprecation warning. + # Remove this if statement when the `tracer` argument is removed + if pin.tracer is ddtrace.tracer: + ddtrace.trace.Pin(service=pin.service).onto(client) + else: + ddtrace.trace.Pin(service=pin.service, tracer=pin.tracer).onto(client) return client diff --git a/ddtrace/contrib/internal/pylibmc/client.py b/ddtrace/contrib/internal/pylibmc/client.py index 917a42b293e..3ea6f09c62c 100644 --- a/ddtrace/contrib/internal/pylibmc/client.py +++ b/ddtrace/contrib/internal/pylibmc/client.py @@ -51,7 +51,12 @@ def __init__(self, client=None, service=memcached.SERVICE, tracer=None, *args, * super(TracedClient, self).__init__(client) schematized_service = schematize_service_name(service) - pin = ddtrace.Pin(service=schematized_service, tracer=tracer) + # Calling ddtrace.pin.Pin(...) with the `tracer` argument generates a deprecation warning. + # Remove this if statement when the `tracer` argument is removed + if tracer is ddtrace.tracer: + pin = ddtrace.trace.Pin(service=schematized_service) + else: + pin = ddtrace.trace.Pin(service=schematized_service, tracer=tracer) pin.onto(self) # attempt to collect the pool of urls this client talks to @@ -64,7 +69,7 @@ def clone(self, *args, **kwargs): # rewrap new connections. cloned = self.__wrapped__.clone(*args, **kwargs) traced_client = TracedClient(cloned) - pin = ddtrace.Pin.get_from(self) + pin = ddtrace.trace.Pin.get_from(self) if pin: pin.clone().onto(traced_client) return traced_client @@ -155,7 +160,7 @@ def _no_span(self): def _span(self, cmd_name): """Return a span timing the given command.""" - pin = ddtrace.Pin.get_from(self) + pin = ddtrace.trace.Pin.get_from(self) if not pin or not pin.enabled(): return self._no_span() diff --git a/ddtrace/contrib/internal/pymongo/client.py b/ddtrace/contrib/internal/pymongo/client.py index 426d205f9da..2cdf2185586 100644 --- a/ddtrace/contrib/internal/pymongo/client.py +++ b/ddtrace/contrib/internal/pymongo/client.py @@ -61,7 +61,7 @@ def __setddpin__(client, pin): pin.onto(client._topology) def __getddpin__(client): - return ddtrace.Pin.get_from(client._topology) + return ddtrace.trace.Pin.get_from(client._topology) # Set a pin on the mongoclient pin on the topology object # This allows us to pass the same pin to the server objects @@ -103,7 +103,7 @@ def _trace_topology_select_server(func, args, kwargs): # Ensure the pin used on the traced mongo client is passed down to the topology instance # This allows us to pass the same pin in traced server objects. topology_instance = get_argument_value(args, kwargs, 0, "self") - pin = ddtrace.Pin.get_from(topology_instance) + pin = ddtrace.trace.Pin.get_from(topology_instance) if pin is not None: pin.onto(server) @@ -125,7 +125,7 @@ def _datadog_trace_operation(operation, wrapped): log.exception("error parsing query") # Gets the pin from the mogno client (through the topology object) - pin = ddtrace.Pin.get_from(wrapped) + pin = ddtrace.trace.Pin.get_from(wrapped) # if we couldn't parse or shouldn't trace the message, just go. if not cmd or not pin or not pin.enabled(): return None @@ -220,7 +220,7 @@ def _trace_socket_command(func, args, kwargs): except Exception: log.exception("error parsing spec. skipping trace") - pin = ddtrace.Pin.get_from(socket_instance) + pin = ddtrace.trace.Pin.get_from(socket_instance) # skip tracing if we don't have a piece of data we need if not dbname or not cmd or not pin or not pin.enabled(): return func(*args, **kwargs) @@ -239,7 +239,7 @@ def _trace_socket_write_command(func, args, kwargs): except Exception: log.exception("error parsing msg") - pin = ddtrace.Pin.get_from(socket_instance) + pin = ddtrace.trace.Pin.get_from(socket_instance) # if we couldn't parse it, don't try to trace it. if not cmd or not pin or not pin.enabled(): return func(*args, **kwargs) @@ -252,7 +252,7 @@ def _trace_socket_write_command(func, args, kwargs): def _trace_cmd(cmd, socket_instance, address): - pin = ddtrace.Pin.get_from(socket_instance) + pin = ddtrace.trace.Pin.get_from(socket_instance) s = pin.tracer.trace( schematize_database_operation("pymongo.cmd", database_provider="mongodb"), span_type=SpanTypes.MONGODB, diff --git a/ddtrace/contrib/internal/sqlalchemy/engine.py b/ddtrace/contrib/internal/sqlalchemy/engine.py index 57b6db4e9fc..3b5f96be9e7 100644 --- a/ddtrace/contrib/internal/sqlalchemy/engine.py +++ b/ddtrace/contrib/internal/sqlalchemy/engine.py @@ -67,7 +67,12 @@ def __init__(self, tracer, service, engine): self.name = schematize_database_operation("%s.query" % self.vendor, database_provider=self.vendor) # attach the PIN - Pin(tracer=tracer, service=self.service).onto(engine) + # Calling ddtrace.pin.Pin(...) with the `tracer` argument generates a deprecation warning. + # Remove this if statement when the `tracer` argument is removed + if self.tracer is ddtrace.tracer: + Pin(service=self.service).onto(engine) + else: + Pin(tracer=tracer, service=self.service).onto(engine) listen(engine, "before_cursor_execute", self._before_cur_exec) listen(engine, "after_cursor_execute", self._after_cur_exec) diff --git a/ddtrace/contrib/internal/tornado/application.py b/ddtrace/contrib/internal/tornado/application.py index 3a7dc832b5e..86794689835 100644 --- a/ddtrace/contrib/internal/tornado/application.py +++ b/ddtrace/contrib/internal/tornado/application.py @@ -55,4 +55,9 @@ def tracer_config(__init__, app, args, kwargs): tracer.set_tags(tags) # configure the PIN object for template rendering - ddtrace.Pin(service=service, tracer=tracer).onto(template) + # Required for backwards compatibility. Remove the else clause when + # the `ddtrace.Pin` object no longer accepts the Pin argument. + if tracer is ddtrace.tracer: + ddtrace.trace.Pin(service=service).onto(template) + else: + ddtrace.trace.Pin(service=service, tracer=tracer).onto(template) diff --git a/ddtrace/contrib/redis/__init__.py b/ddtrace/contrib/redis/__init__.py index 638d08b0a79..9b498614e4b 100644 --- a/ddtrace/contrib/redis/__init__.py +++ b/ddtrace/contrib/redis/__init__.py @@ -52,7 +52,7 @@ Instance Configuration ~~~~~~~~~~~~~~~~~~~~~~ -To configure particular redis instances use the :class:`Pin <ddtrace.Pin>` API:: +To configure particular redis instances use the :class:`Pin <ddtrace.trace.Pin>` API:: import redis from ddtrace.trace import Pin diff --git a/ddtrace/contrib/tornado/__init__.py b/ddtrace/contrib/tornado/__init__.py index ad0adef2dd5..10390e77e6e 100644 --- a/ddtrace/contrib/tornado/__init__.py +++ b/ddtrace/contrib/tornado/__init__.py @@ -76,11 +76,6 @@ def log_exception(self, typ, value, tb): 'default_service': 'my-tornado-app', 'tags': {'env': 'production'}, 'distributed_tracing': False, - 'settings': { - 'FILTERS': [ - FilterRequestsOnUrl(r'http://test\\.example\\.com'), - ], - }, }, } diff --git a/ddtrace/contrib/vertica/__init__.py b/ddtrace/contrib/vertica/__init__.py index 4da8a844e83..7271c1c92ad 100644 --- a/ddtrace/contrib/vertica/__init__.py +++ b/ddtrace/contrib/vertica/__init__.py @@ -28,14 +28,14 @@ ``Pin`` API:: from ddtrace import patch - from ddtrace.trace import Pin, Tracer + from ddtrace.trace import Pin patch(vertica=True) import vertica_python conn = vertica_python.connect(**YOUR_VERTICA_CONFIG) - # override the service and tracer to be used + # override the service Pin.override(conn, service='myverticaservice') """ diff --git a/ddtrace/contrib/yaaredis/__init__.py b/ddtrace/contrib/yaaredis/__init__.py index 2eefb3beb93..7c0c9bd1b21 100644 --- a/ddtrace/contrib/yaaredis/__init__.py +++ b/ddtrace/contrib/yaaredis/__init__.py @@ -50,7 +50,7 @@ Instance Configuration ~~~~~~~~~~~~~~~~~~~~~~ -To configure particular yaaredis instances use the :class:`Pin <ddtrace.Pin>` API:: +To configure particular yaaredis instances use the :class:`Pin <ddtrace.trace.Pin>` API:: import yaaredis from ddtrace.trace import Pin diff --git a/ddtrace/filters.py b/ddtrace/filters.py index 3c9c42892b8..bd6367d5635 100644 --- a/ddtrace/filters.py +++ b/ddtrace/filters.py @@ -4,7 +4,7 @@ deprecate( - "The ddtrace.filters module is deprecated and will be removed.", - message="Import ``TraceFilter`` and/or ``FilterRequestsOnUrl`` from the ddtrace.trace package.", + "The ddtrace.filters module and the ``FilterRequestsOnUrl`` class is deprecated and will be removed.", + message="Import ``TraceFilter`` from the ddtrace.trace package.", category=DDTraceDeprecationWarning, ) diff --git a/ddtrace/llmobs/_evaluators/ragas/base.py b/ddtrace/llmobs/_evaluators/ragas/base.py index 17cf5807af0..798c8e2fccc 100644 --- a/ddtrace/llmobs/_evaluators/ragas/base.py +++ b/ddtrace/llmobs/_evaluators/ragas/base.py @@ -26,8 +26,10 @@ class RagasDependencies: def __init__(self): import ragas - self.ragas_version = parse_version(ragas.__version__) - if self.ragas_version >= (0, 2, 0) or self.ragas_version < (0, 1, 10): + self.ragas_version = ragas.__version__ # type: str + + parsed_version = parse_version(ragas.__version__) + if parsed_version >= (0, 2, 0) or parsed_version < (0, 1, 10): raise NotImplementedError( "Ragas version: {} is not supported".format(self.ragas_version), ) diff --git a/ddtrace/propagation/http.py b/ddtrace/propagation/http.py index 0cd5b69db46..fdaf97410ad 100644 --- a/ddtrace/propagation/http.py +++ b/ddtrace/propagation/http.py @@ -42,7 +42,6 @@ from ..internal.compat import ensure_text from ..internal.constants import _PROPAGATION_BEHAVIOR_RESTART from ..internal.constants import _PROPAGATION_STYLE_BAGGAGE -from ..internal.constants import _PROPAGATION_STYLE_NONE from ..internal.constants import _PROPAGATION_STYLE_W3C_TRACECONTEXT from ..internal.constants import DD_TRACE_BAGGAGE_MAX_BYTES from ..internal.constants import DD_TRACE_BAGGAGE_MAX_ITEMS @@ -879,20 +878,6 @@ def _inject(span_context, headers): headers[_HTTP_HEADER_TRACESTATE] = span_context._tracestate -class _NOP_Propagator: - @staticmethod - def _extract(headers): - # type: (Dict[str, str]) -> None - return None - - # this method technically isn't needed with the current way we have HTTPPropagator.inject setup - # but if it changes then we might want it - @staticmethod - def _inject(span_context, headers): - # type: (Context , Dict[str, str]) -> Dict[str, str] - return headers - - class _BaggageHeader: """Helper class to inject/extract Baggage Headers""" @@ -964,7 +949,6 @@ def _extract(headers: Dict[str, str]) -> Context: PROPAGATION_STYLE_B3_MULTI: _B3MultiHeader, PROPAGATION_STYLE_B3_SINGLE: _B3SingleHeader, _PROPAGATION_STYLE_W3C_TRACECONTEXT: _TraceContext, - _PROPAGATION_STYLE_NONE: _NOP_Propagator, _PROPAGATION_STYLE_BAGGAGE: _BaggageHeader, } @@ -1068,6 +1052,8 @@ def parent_call(): :param dict headers: HTTP headers to extend with tracing attributes. :param Span non_active_span: Only to be used if injecting a non-active span. """ + if not config._propagation_style_inject: + return if non_active_span is not None and non_active_span.context is not span_context: log.error( "span_context and non_active_span.context are not the same, but should be. non_active_span.context " @@ -1141,7 +1127,7 @@ def my_controller(url, headers): :return: New `Context` with propagated attributes. """ context = Context() - if not headers: + if not headers or not config._propagation_style_extract: return context try: style = "" diff --git a/ddtrace/settings/config.py b/ddtrace/settings/config.py index adcd9505a52..81ed7a3ab19 100644 --- a/ddtrace/settings/config.py +++ b/ddtrace/settings/config.py @@ -150,12 +150,15 @@ def _parse_propagation_styles(styles_str): category=DDTraceDeprecationWarning, ) style = PROPAGATION_STYLE_B3_SINGLE - if not style: + if not style or style == _PROPAGATION_STYLE_NONE: continue if style not in PROPAGATION_STYLE_ALL: log.warning("Unknown DD_TRACE_PROPAGATION_STYLE: {!r}, allowed values are %r", style, PROPAGATION_STYLE_ALL) continue styles.append(style) + # Remove "none" if it's present since it lacks a propagator + if _PROPAGATION_STYLE_NONE in styles: + styles.remove(_PROPAGATION_STYLE_NONE) return styles diff --git a/ddtrace/trace/__init__.py b/ddtrace/trace/__init__.py index dcd3aeb928e..f709310d589 100644 --- a/ddtrace/trace/__init__.py +++ b/ddtrace/trace/__init__.py @@ -1,8 +1,18 @@ from ddtrace._trace.context import Context -from ddtrace._trace.filters import FilterRequestsOnUrl from ddtrace._trace.filters import TraceFilter from ddtrace._trace.pin import Pin +from ddtrace._trace.span import Span +from ddtrace._trace.tracer import Tracer -# TODO: Move `ddtrace.Tracer`, `ddtrace.Span`, and `ddtrace.tracer` to this module -__all__ = ["Context", "Pin", "TraceFilter", "FilterRequestsOnUrl"] +# a global tracer instance with integration settings +tracer = Tracer() + +__all__ = [ + "Context", + "Pin", + "TraceFilter", + "Tracer", + "Span", + "tracer", +] diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index 9906fddea89..c1c41df00c0 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -332,24 +332,25 @@ configuring the tracer with a filters list. For instance, to filter out all traces of incoming requests to a specific url:: from ddtrace import tracer + from ddtrace.trace import TraceFilter + + class FilterbyName(TraceFilter): + def process_trace(self, trace): + for span in trace: + if span.name == "some_name" + # drop the full trace chunk + return None + return trace tracer.configure(settings={ 'FILTERS': [ - FilterRequestsOnUrl(r'http://test\.example\.com'), + FilterbyName(), ], }) The filters in the filters list will be applied sequentially to each trace and the resulting trace will either be sent to the Agent or discarded. -**Built-in filters** - -The library comes with a ``FilterRequestsOnUrl`` filter that can be used to -filter out incoming requests to specific urls: - -.. autoclass:: ddtrace.trace.FilterRequestsOnUrl - :members: - **Writing a custom filter** Create a filter by implementing a class with a ``process_trace`` method and diff --git a/docs/api.rst b/docs/api.rst index 4c52e37808f..d4b4e80674a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -19,7 +19,7 @@ Tracing .. autoclass:: ddtrace.Span :members: -.. autoclass:: ddtrace.Pin +.. autoclass:: ddtrace.trace.Pin :members: .. autoclass:: ddtrace.trace.Context diff --git a/lib-injection/sources/sitecustomize.py b/lib-injection/sources/sitecustomize.py index 0f87b770edd..32ab1c31ff3 100644 --- a/lib-injection/sources/sitecustomize.py +++ b/lib-injection/sources/sitecustomize.py @@ -45,6 +45,7 @@ def parse_version(version): TELEMETRY_ENABLED = "DD_INJECTION_ENABLED" in os.environ DEBUG_MODE = os.environ.get("DD_TRACE_DEBUG", "").lower() in ("true", "1", "t") INSTALLED_PACKAGES = {} +DDTRACE_VERSION = "unknown" PYTHON_VERSION = "unknown" PYTHON_RUNTIME = "unknown" PKGS_ALLOW_LIST = {} @@ -133,7 +134,7 @@ def create_count_metric(metric, tags=None): } -def gen_telemetry_payload(telemetry_events, ddtrace_version="unknown"): +def gen_telemetry_payload(telemetry_events, ddtrace_version): return { "metadata": { "language_name": "python", @@ -233,6 +234,7 @@ def get_first_incompatible_sysarg(): def _inject(): + global DDTRACE_VERSION global INSTALLED_PACKAGES global PYTHON_VERSION global PYTHON_RUNTIME @@ -353,10 +355,7 @@ def _inject(): if not os.path.exists(site_pkgs_path): _log("ddtrace site-packages not found in %r, aborting" % site_pkgs_path, level="error") TELEMETRY_DATA.append( - gen_telemetry_payload( - [create_count_metric("library_entrypoint.abort", ["reason:missing_" + site_pkgs_path])], - DDTRACE_VERSION, - ) + create_count_metric("library_entrypoint.abort", ["reason:missing_" + site_pkgs_path]), ) return @@ -369,14 +368,9 @@ def _inject(): except BaseException as e: _log("failed to load ddtrace module: %s" % e, level="error") TELEMETRY_DATA.append( - gen_telemetry_payload( - [ - create_count_metric( - "library_entrypoint.error", ["error_type:import_ddtrace_" + type(e).__name__.lower()] - ) - ], - DDTRACE_VERSION, - ) + create_count_metric( + "library_entrypoint.error", ["error_type:import_ddtrace_" + type(e).__name__.lower()] + ), ) return @@ -408,28 +402,18 @@ def _inject(): _log("successfully configured ddtrace package, python path is %r" % os.environ["PYTHONPATH"]) TELEMETRY_DATA.append( - gen_telemetry_payload( + create_count_metric( + "library_entrypoint.complete", [ - create_count_metric( - "library_entrypoint.complete", - [ - "injection_forced:" + str(runtime_incomp or integration_incomp).lower(), - ], - ) + "injection_forced:" + str(runtime_incomp or integration_incomp).lower(), ], - DDTRACE_VERSION, - ) + ), ) except Exception as e: TELEMETRY_DATA.append( - gen_telemetry_payload( - [ - create_count_metric( - "library_entrypoint.error", ["error_type:init_ddtrace_" + type(e).__name__.lower()] - ) - ], - DDTRACE_VERSION, - ) + create_count_metric( + "library_entrypoint.error", ["error_type:init_ddtrace_" + type(e).__name__.lower()] + ), ) _log("failed to load ddtrace.bootstrap.sitecustomize: %s" % e, level="error") return @@ -451,12 +435,11 @@ def _inject(): _inject() except Exception as e: TELEMETRY_DATA.append( - gen_telemetry_payload( - [create_count_metric("library_entrypoint.error", ["error_type:main_" + type(e).__name__.lower()])] - ) + create_count_metric("library_entrypoint.error", ["error_type:main_" + type(e).__name__.lower()]) ) finally: if TELEMETRY_DATA: - send_telemetry(TELEMETRY_DATA) + payload = gen_telemetry_payload(TELEMETRY_DATA, DDTRACE_VERSION) + send_telemetry(payload) except Exception: pass # absolutely never allow exceptions to propagate to the app diff --git a/releasenotes/notes/ddtrace-resourcefilter-deprecated-52b1c92d388b0518.yaml b/releasenotes/notes/ddtrace-resourcefilter-deprecated-52b1c92d388b0518.yaml new file mode 100644 index 00000000000..183249aa688 --- /dev/null +++ b/releasenotes/notes/ddtrace-resourcefilter-deprecated-52b1c92d388b0518.yaml @@ -0,0 +1,4 @@ +--- +deprecations: + - | + tracing: Deprecates ``ddtrace.filters.FilterRequestsOnUrl``. Spans should be filtered/sampled using DD_TRACE_SAMPLING_RULES configuration. diff --git a/releasenotes/notes/fix-ssi-telemetry-events-a0a01ad0b6ef63b5.yaml b/releasenotes/notes/fix-ssi-telemetry-events-a0a01ad0b6ef63b5.yaml new file mode 100644 index 00000000000..a1eba938bb8 --- /dev/null +++ b/releasenotes/notes/fix-ssi-telemetry-events-a0a01ad0b6ef63b5.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + lib-injection: Fixes incorrect telemetry data payload format. diff --git a/releasenotes/notes/remove-multi-tracer-support-from-pin-f2f20ca3fa731929.yaml b/releasenotes/notes/remove-multi-tracer-support-from-pin-f2f20ca3fa731929.yaml new file mode 100644 index 00000000000..18c70a15b04 --- /dev/null +++ b/releasenotes/notes/remove-multi-tracer-support-from-pin-f2f20ca3fa731929.yaml @@ -0,0 +1,4 @@ +--- +deprecations: + - | + tracer: Deprecates the ability to use multiple tracer instances with ddtrace.Pin. In v3.0.0 pin objects will only use the global tracer. diff --git a/tests/appsec/appsec/test_processor.py b/tests/appsec/appsec/test_processor.py index 3fec599237b..ad3c9e6827a 100644 --- a/tests/appsec/appsec/test_processor.py +++ b/tests/appsec/appsec/test_processor.py @@ -638,7 +638,23 @@ def test_asm_context_registration(tracer): "name": "test required", "tags": {"category": "attack_attempt", "custom": "1", "type": "custom"}, "transformers": [], - } + }, + { + "conditions": [ + { + "operator": "match_regex", + "parameters": { + "inputs": [{"address": "usr.login"}], + "options": {"case_sensitive": False}, + "regex": "GET", + }, + } + ], + "id": "32b243c7-26eb-4046-bbbb-custom", + "name": "test required", + "tags": {"category": "attack_attempt", "custom": "1", "type": "custom"}, + "transformers": [], + }, ] } @@ -672,6 +688,7 @@ def test_required_addresses(): "server.request.query", "server.response.headers.no_cookies", "usr.id", + "usr.login", } diff --git a/tests/appsec/contrib_appsec/utils.py b/tests/appsec/contrib_appsec/utils.py index e614e33a51b..6400cacb625 100644 --- a/tests/appsec/contrib_appsec/utils.py +++ b/tests/appsec/contrib_appsec/utils.py @@ -1480,6 +1480,8 @@ def test_auto_user_events( assert get_tag("_dd.appsec.events.users.login.success.sdk") is None if mode == "identification": assert get_tag("_dd.appsec.usr.login") == user + elif mode == "anonymization": + assert get_tag("_dd.appsec.usr.login") == _hash_user_id(user) else: assert get_tag("appsec.events.users.login.success.track") == "true" assert get_tag("usr.id") == user_id_hash diff --git a/tests/appsec/iast/_ast/test_ast_patching.py b/tests/appsec/iast/_ast/test_ast_patching.py index d014496942b..213737ecbce 100644 --- a/tests/appsec/iast/_ast/test_ast_patching.py +++ b/tests/appsec/iast/_ast/test_ast_patching.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +import os import sys import astunparse @@ -20,6 +21,15 @@ _PREFIX = IAST.PATCH_ADDED_SYMBOL_PREFIX +@pytest.fixture(autouse=True, scope="module") +def clear_iast_env_vars(): + if IAST.PATCH_MODULES in os.environ: + os.environ.pop("_DD_IAST_PATCH_MODULES") + if IAST.DENY_MODULES in os.environ: + os.environ.pop("_DD_IAST_DENY_MODULES") + yield + + @pytest.mark.parametrize( "source_text, module_path, module_name", [ @@ -148,15 +158,34 @@ def test_astpatch_source_unchanged(module_name): assert ("", None) == astpatch_module(__import__(module_name, fromlist=[None])) -def test_module_should_iast_patch(): +def test_should_iast_patch_allow_first_party(): + assert _should_iast_patch("tests.appsec.iast.integration.main") + assert _should_iast_patch("tests.appsec.iast.integration.print_str") + + +def test_should_not_iast_patch_if_vendored(): + assert not _should_iast_patch("foobar.vendor.requests") + assert not _should_iast_patch(("vendored.foobar.requests")) + + +def test_should_iast_patch_deny_by_default_if_third_party(): + # note that modules here must be in the ones returned by get_package_distributions() + # but not in ALLOWLIST or DENYLIST. So please don't put astunparse there :) + assert not _should_iast_patch("astunparse.foo.bar.not.in.deny.or.allow.list") + + +def test_should_not_iast_patch_if_in_denylist(): assert not _should_iast_patch("ddtrace.internal.module") assert not _should_iast_patch("ddtrace.appsec._iast") + assert not _should_iast_patch("pip.foo.bar") + + +def test_should_not_iast_patch_if_stdlib(): assert not _should_iast_patch("base64") - assert not _should_iast_patch("envier") assert not _should_iast_patch("itertools") assert not _should_iast_patch("http") - assert _should_iast_patch("tests.appsec.iast.integration.main") - assert _should_iast_patch("tests.appsec.iast.integration.print_str") + assert not _should_iast_patch("os.path") + assert not _should_iast_patch("sys.platform") @pytest.mark.parametrize( diff --git a/tests/appsec/iast_packages/test_packages.py b/tests/appsec/iast_packages/test_packages.py index 6da439dbeb5..83e53ae92c9 100644 --- a/tests/appsec/iast_packages/test_packages.py +++ b/tests/appsec/iast_packages/test_packages.py @@ -216,24 +216,29 @@ def uninstall(self, python_cmd): import_module_to_validate="boto3.session", ), PackageForTesting("botocore", "1.34.110", "", "", "", test_e2e=False), - PackageForTesting( - "cffi", "1.16.0", "", 30, "", import_module_to_validate="cffi.model", extras=[("setuptools", "72.1.0")] - ), - PackageForTesting( - "certifi", "2024.2.2", "", "The path to the CA bundle is", "", import_module_to_validate="certifi.core" - ), - PackageForTesting( - "charset-normalizer", - "3.3.2", - "my-bytes-string", - "my-bytes-string", - "", - import_name="charset_normalizer", - import_module_to_validate="charset_normalizer.api", - test_propagation=True, - fixme_propagation_fails=True, - ), - PackageForTesting("click", "8.1.7", "", "Hello World!\nHello World!\n", "", import_module_to_validate="click.core"), + ## Skip due to cffi added to the denylist + # PackageForTesting( + # "cffi", "1.16.0", "", 30, "", import_module_to_validate="cffi.model", extras=[("setuptools", "72.1.0")] + # ), + ## Skip due to certifi added to the denylist + # PackageForTesting( + # "certifi", "2024.2.2", "", "The path to the CA bundle is", "", import_module_to_validate="certifi.core" + # ), + ## Skip due to charset-normalizer added to the denylist + # PackageForTesting( + # "charset-normalizer", + # "3.3.2", + # "my-bytes-string", + # "my-bytes-string", + # "", + # import_name="charset_normalizer", + # import_module_to_validate="charset_normalizer.api", + # test_propagation=True, + # fixme_propagation_fails=True, + # ), + ## Skip due to click added to the denylist + # PackageForTesting("click", "8.1.7", "", "Hello World!\nHello World!\n", "", + # import_module_to_validate="click.core"), PackageForTesting( "cryptography", "42.0.7", @@ -247,15 +252,16 @@ def uninstall(self, python_cmd): PackageForTesting( "distlib", "0.3.8", "", "Name: example-package\nVersion: 0.1", "", import_module_to_validate="distlib.util" ), - PackageForTesting( - "exceptiongroup", - "1.2.1", - "foobar", - "ValueError: First error with foobar\nTypeError: Second error with foobar", - "", - import_module_to_validate="exceptiongroup._formatting", - test_propagation=True, - ), + ## Skip due to docopt added to the denylist + # PackageForTesting( + # "exceptiongroup", + # "1.2.1", + # "foobar", + # "ValueError: First error with foobar\nTypeError: Second error with foobar", + # "", + # import_module_to_validate="exceptiongroup._formatting", + # test_propagation=True, + # ), PackageForTesting( "filelock", "3.14.0", @@ -327,14 +333,15 @@ def uninstall(self, python_cmd): "", import_module_to_validate="isodate.duration", ), - PackageForTesting( - "itsdangerous", - "2.2.0", - "foobar", - "Signed value: foobar.generated_signature\nUnsigned value: foobar", - "", - import_module_to_validate="itsdangerous.serializer", - ), + ## Skip due to itsdangerous added to the denylist + # PackageForTesting( + # "itsdangerous", + # "2.2.0", + # "foobar", + # "Signed value: foobar.generated_signature\nUnsigned value: foobar", + # "", + # import_module_to_validate="itsdangerous.serializer", + # ), PackageForTesting( "jinja2", "3.1.4", @@ -424,13 +431,15 @@ def uninstall(self, python_cmd): PackageForTesting( "openpyxl", "3.1.2", "foobar", "Written value: foobar", "", import_module_to_validate="openpyxl.chart.axis" ), - PackageForTesting( - "packaging", - "24.0", - "", - {"is_version_valid": True, "requirement": "example-package>=1.0.0", "specifier": ">=1.0.0", "version": "1.2.3"}, - "", - ), + ## Skip due to packaging added to the denylist + # PackageForTesting( + # "packaging", + # "24.0", + # "", + # {"is_version_valid": True, "requirement": "example-package>=1.0.0", + # "specifier": ">=1.0.0", "version": "1.2.3"}, + # "", + # ), ## Skip due to pandas added to the denylist # Pandas dropped Python 3.8 support in pandas>2.0.3 # PackageForTesting("pandas", "2.2.2", "foobar", "Written value: foobar", "", skip_python_version=[(3, 8)]), @@ -443,14 +452,15 @@ def uninstall(self, python_cmd): import_module_to_validate="platformdirs.unix", test_propagation=True, ), - PackageForTesting( - "pluggy", - "1.5.0", - "foobar", - "Hook result: Plugin received: foobar", - "", - import_module_to_validate="pluggy._hooks", - ), + ## Skip due to pluggy added to the denylist + # PackageForTesting( + # "pluggy", + # "1.5.0", + # "foobar", + # "Hook result: Plugin received: foobar", + # "", + # import_module_to_validate="pluggy._hooks", + # ), PackageForTesting( "pyasn1", "0.6.0", @@ -461,7 +471,8 @@ def uninstall(self, python_cmd): test_propagation=True, fixme_propagation_fails=True, ), - PackageForTesting("pycparser", "2.22", "", "", ""), + ## Skip due to pygments added to the denylist + # PackageForTesting("pycparser", "2.22", "", "", ""), PackageForTesting( "pydantic", "2.7.1", @@ -619,15 +630,16 @@ def uninstall(self, python_cmd): test_propagation=True, fixme_propagation_fails=True, ), - PackageForTesting( - "werkzeug", - "3.0.3", - "your-password", - "Original password: your-password\nHashed password: replaced_hashed\nPassword match: True", - "", - import_module_to_validate="werkzeug.http", - skip_python_version=[(3, 6), (3, 7), (3, 8)], - ), + ## Skip due to werkzeug added to the denylist + # PackageForTesting( + # "werkzeug", + # "3.0.3", + # "your-password", + # "Original password: your-password\nHashed password: replaced_hashed\nPassword match: True", + # "", + # import_module_to_validate="werkzeug.http", + # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # ), PackageForTesting( "yarl", "1.9.4", @@ -640,24 +652,26 @@ def uninstall(self, python_cmd): test_propagation=True, fixme_propagation_fails=True, ), - PackageForTesting( - "zipp", - "3.18.2", - "example.zip", - "Contents of example.zip: ['example.zip/example.txt']", - "", - skip_python_version=[(3, 6), (3, 7), (3, 8)], - ), - PackageForTesting( - "typing-extensions", - "4.11.0", - "", - "", - "", - import_name="typing_extensions", - test_e2e=False, - skip_python_version=[(3, 6), (3, 7), (3, 8)], - ), + ## Skip due to zipp added to the denylist + # PackageForTesting( + # "zipp", + # "3.18.2", + # "example.zip", + # "Contents of example.zip: ['example.zip/example.txt']", + # "", + # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # ), + ## Skip due to typing-extensions added to the denylist + # PackageForTesting( + # "typing-extensions", + # "4.11.0", + # "", + # "", + # "", + # import_name="typing_extensions", + # test_e2e=False, + # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # ), PackageForTesting( "six", "1.16.0", @@ -687,14 +701,15 @@ def uninstall(self, python_cmd): "", import_name="jwt", ), - PackageForTesting( - "wrapt", - "1.16.0", - "some-value", - "Function executed with param: some-value", - "", - test_propagation=True, - ), + ## Skip due to pyarrow added to the denylist + # PackageForTesting( + # "wrapt", + # "1.16.0", + # "some-value", + # "Function executed with param: some-value", + # "", + # test_propagation=True, + # ), PackageForTesting( "cachetools", "5.3.3", @@ -804,16 +819,17 @@ def uninstall(self, python_cmd): "", import_name="OpenSSL.SSL", ), - PackageForTesting( - "moto[s3]", - "5.0.11", - "some_bucket", - "right_result", - "", - import_name="moto.s3.models", - test_e2e=True, - extras=[("boto3", "1.34.143")], - ), + ## Skip due to pyarrow added to the denylist + # PackageForTesting( + # "moto[s3]", + # "5.0.11", + # "some_bucket", + # "right_result", + # "", + # import_name="moto.s3.models", + # test_e2e=True, + # extras=[("boto3", "1.34.143")], + # ), PackageForTesting("decorator", "5.1.1", "World", "Decorated result: Hello, World!", ""), # TODO: e2e implemented but fails unpatched: "RateLimiter object has no attribute _is_allowed" PackageForTesting( diff --git a/tests/appsec/integrations/django_tests/test_django_appsec.py b/tests/appsec/integrations/django_tests/test_django_appsec.py index 3c5cb399739..2a00657e14a 100644 --- a/tests/appsec/integrations/django_tests/test_django_appsec.py +++ b/tests/appsec/integrations/django_tests/test_django_appsec.py @@ -235,7 +235,9 @@ def test_django_login_sucess_anonymization(client, test_spans, tracer, use_login assert login_span.get_tag(user.ID) == "1" assert login_span.get_tag("appsec.events.users.login.success.track") == "true" assert login_span.get_tag(APPSEC.AUTO_LOGIN_EVENTS_SUCCESS_MODE) == LOGIN_EVENTS_MODE.ANON - assert login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX + ".success.login") is None + assert login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX + ".success.login") == ( + "anon_d1ad1f735a4381c2e8dbed0222db1136" if use_login else None + ) assert login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX + ".success.email") is None assert login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX + ".success.username") is None @@ -368,7 +370,10 @@ def test_django_login_sucess_anonymization_but_user_set_login(client, test_spans assert login_span.get_tag(user.ID) == "anon_d1ad1f735a4381c2e8dbed0222db1136" assert login_span.get_tag("appsec.events.users.login.success.track") == "true" assert login_span.get_tag(APPSEC.AUTO_LOGIN_EVENTS_SUCCESS_MODE) == LOGIN_EVENTS_MODE.ANON - assert not login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX + ".success.login") + assert ( + login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX + ".success.login") + == "anon_d1ad1f735a4381c2e8dbed0222db1136" + ) assert not login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX_PUBLIC + ".success.email") assert not login_span.get_tag(APPSEC.USER_LOGIN_EVENT_PREFIX_PUBLIC + ".success.username") diff --git a/tests/profiling_v2/gunicorn.conf.py b/tests/profiling_v2/gunicorn.conf.py new file mode 100644 index 00000000000..c45f27ce11c --- /dev/null +++ b/tests/profiling_v2/gunicorn.conf.py @@ -0,0 +1,67 @@ +from datetime import datetime +from datetime import timezone +import logging + + +def post_fork(server, worker): + """Log the startup time of each worker.""" + logging.info("Worker %s started", worker.pid) + + +def post_worker_init(worker): + logging.info("Worker %s initialized", worker.pid) + + +class CustomFormatter(logging.Formatter): + """Custom formatter to include timezone offset in the log message.""" + + def formatTime(self, record, datefmt=None): + dt = datetime.fromtimestamp(record.created, tz=timezone.utc).astimezone() + milliseconds = int(record.msecs) + offset = dt.strftime("%z") # Get timezone offset in the form +0530 + if datefmt: + formatted_time = dt.strftime(datefmt) + else: + formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S") + + # Add milliseconds and timezone offset + offset = dt.strftime("%z") # Timezone offset in the form +0530 + return f"{formatted_time}.{milliseconds:03d} {offset}" + + +logconfig_dict = { + "version": 1, + "formatters": { + "default": { + "()": CustomFormatter, # Use the custom formatter + "format": "[%(asctime)s] [%(process)d] [%(levelname)s] %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + }, + }, + "loggers": { + "": { # root logger + "handlers": ["console"], + "level": "INFO", + }, + "gunicorn.error": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + "gunicorn.access": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + }, + "root": { + "level": "INFO", + "handlers": ["console"], + }, +} diff --git a/tests/profiling_v2/test_gunicorn.py b/tests/profiling_v2/test_gunicorn.py index 4d7adbf6c95..90141445d3a 100644 --- a/tests/profiling_v2/test_gunicorn.py +++ b/tests/profiling_v2/test_gunicorn.py @@ -13,7 +13,7 @@ # DEV: gunicorn tests are hard to debug, so keeping these print statements for # future debugging -DEBUG_PRINT = False +DEBUG_PRINT = True def debug_print(*args): @@ -37,6 +37,8 @@ def _run_gunicorn(*args): "127.0.0.1:7644", "--worker-tmp-dir", "/dev/shm", + "-c", + os.path.dirname(__file__) + "/gunicorn.conf.py", "--chdir", os.path.dirname(__file__), ] diff --git a/tests/tracer/test_filters.py b/tests/tracer/test_filters.py index 871405517b7..d632ceb4998 100644 --- a/tests/tracer/test_filters.py +++ b/tests/tracer/test_filters.py @@ -2,9 +2,9 @@ import pytest +from ddtrace._trace.filters import FilterRequestsOnUrl from ddtrace._trace.span import Span from ddtrace.ext.http import URL -from ddtrace.trace import FilterRequestsOnUrl from ddtrace.trace import TraceFilter diff --git a/tests/tracer/test_propagation.py b/tests/tracer/test_propagation.py index c15439ae825..07c72e02ddb 100644 --- a/tests/tracer/test_propagation.py +++ b/tests/tracer/test_propagation.py @@ -1936,15 +1936,6 @@ def test_extract_tracecontext(headers, expected_context): B3_SINGLE_HEADERS_VALID, CONTEXT_EMPTY, ), - ( - "baggage_case_insensitive", - None, - None, - {"BAgGage": "key1=val1,key2=val2"}, - { - "baggage": {"key1": "val1", "key2": "val2"}, - }, - ), # All valid headers ( "valid_all_headers_default_style", @@ -2139,20 +2130,6 @@ def test_extract_tracecontext(headers, expected_context): "dd_origin": None, }, ), - ( - # name, styles, headers, expected_context, - "none_and_other_prop_style_still_extracts", - [PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_NONE], - None, - ALL_HEADERS, - { - "trace_id": 13088165645273925489, - "span_id": 5678, - "sampling_priority": 1, - "dd_origin": "synthetics", - "meta": {"_dd.p.dm": "-3"}, - }, - ), # Testing that order matters ( "order_matters_B3_SINGLE_HEADER_first", @@ -2413,6 +2390,15 @@ def test_extract_tracecontext(headers, expected_context): ], }, ), + ( + "baggage_case_insensitive", + None, + None, + {"BAgGage": "key1=val1,key2=val2"}, + { + "baggage": {"key1": "val1", "key2": "val2"}, + }, + ), ] # Only add fixtures here if they can't pass both test_propagation_extract_env @@ -2452,6 +2438,19 @@ def test_extract_tracecontext(headers, expected_context): }, }, ), + ( + "none_and_other_prop_style_still_extracts", + [PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_NONE], + None, + ALL_HEADERS, + { + "trace_id": 13088165645273925489, + "span_id": 5678, + "sampling_priority": 1, + "dd_origin": "synthetics", + "meta": {"_dd.p.dm": "-3"}, + }, + ), # Only works for env since config is modified at startup to set # propagation_style_extract to [None] if DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT is set to ignore ( @@ -2461,6 +2460,20 @@ def test_extract_tracecontext(headers, expected_context): DATADOG_HEADERS_VALID, CONTEXT_EMPTY, ), + ( + # name, styles, headers, expected_context, + "none_and_other_prop_style_still_extracts", + [PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_NONE], + None, + ALL_HEADERS, + { + "trace_id": 13088165645273925489, + "span_id": 5678, + "sampling_priority": 1, + "dd_origin": "synthetics", + "meta": {"_dd.p.dm": "-3"}, + }, + ), ]