Skip to content

Commit

Permalink
Merge branch 'main' into refactor/config-telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
mabdinur authored Jan 10, 2025
2 parents 74c150e + d7927e6 commit af0f1c9
Show file tree
Hide file tree
Showing 86 changed files with 3,173 additions and 2,340 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ benchmarks/base/aspects_benchmarks_generate.py @DataDog/asm-python
ddtrace/appsec/ @DataDog/asm-python
ddtrace/settings/asm.py @DataDog/asm-python
ddtrace/contrib/subprocess/ @DataDog/asm-python
ddtrace/contrib/internal/subprocess/ @DataDog/asm-python
ddtrace/contrib/flask_login/ @DataDog/asm-python
ddtrace/contrib/webbrowser @DataDog/asm-python
ddtrace/contrib/urllib @DataDog/asm-python
Expand Down
32 changes: 32 additions & 0 deletions .riot/requirements/16562eb.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# pip-compile --allow-unsafe --config=pyproject.toml --no-annotate --resolver=backtracking .riot/requirements/16562eb.in
#
attrs==24.2.0
coverage[toml]==7.2.7
exceptiongroup==1.2.2
hypothesis==6.45.0
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
mock==5.1.0
multidict==6.0.5
opentracing==2.4.0
packaging==24.0
pluggy==1.2.0
pytest==7.4.4
pytest-asyncio==0.21.1
pytest-cov==4.1.0
pytest-mock==3.11.1
pyyaml==6.0.1
six==1.17.0
sortedcontainers==2.4.0
tomli==2.0.1
typing-extensions==4.7.1
urllib3==1.26.20
vcrpy==4.4.0
wrapt==1.16.0
yarl==1.9.4
zipp==3.15.0
24 changes: 16 additions & 8 deletions ddtrace/_trace/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from ddtrace.internal.atexit import register_on_exit_signal
from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY
from ddtrace.internal.constants import SPAN_API_DATADOG
from ddtrace.internal.core import dispatch
from ddtrace.internal.dogstatsd import get_dogstatsd_client
from ddtrace.internal.logger import get_logger
from ddtrace.internal.peer_service.processor import PeerServiceProcessor
Expand Down Expand Up @@ -849,7 +850,7 @@ def _start_span(
for p in chain(self._span_processors, SpanProcessor.__processors__, self._deferred_processors):
p.on_span_start(span)
self._hooks.emit(self.__class__.start_span, span)

dispatch("trace.span_start", (span,))
return span

start_span = _start_span
Expand All @@ -866,6 +867,8 @@ def _on_span_finish(self, span: Span) -> None:
for p in chain(self._span_processors, SpanProcessor.__processors__, self._deferred_processors):
p.on_span_finish(span)

dispatch("trace.span_finish", (span,))

if log.isEnabledFor(logging.DEBUG):
log.debug("finishing span %s (enabled:%s)", span._pprint(), self.enabled)

Expand Down Expand Up @@ -940,18 +943,23 @@ def trace(
)

def current_root_span(self) -> Optional[Span]:
"""Returns the root span of the current execution.
"""Returns the local root span of the current execution/process.
Note: This cannot be used to access the true root span of the trace
in a distributed tracing setup if the actual root span occurred in
another execution/process.
This is useful for attaching information related to the trace as a
whole without needing to add to child spans.
This is useful for attaching information to the local root span
of the current execution/process, which is often also service
entry span.
For example::
# get the root span
root_span = tracer.current_root_span()
# get the local root span
local_root_span = tracer.current_root_span()
# set the host just once on the root span
if root_span:
root_span.set_tag('host', '127.0.0.1')
if local_root_span:
local_root_span.set_tag('host', '127.0.0.1')
"""
span = self.current_span()
if span is None:
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/appsec/_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Flags(enum.IntFlag):
ASM_SESSION_FINGERPRINT = 1 << 33
ASM_NETWORK_FINGERPRINT = 1 << 34
ASM_HEADER_FINGERPRINT = 1 << 35
ASM_RASP_CMDI = 1 << 37


_ALL_ASM_BLOCKING = (
Expand All @@ -49,7 +50,7 @@ class Flags(enum.IntFlag):
| Flags.ASM_HEADER_FINGERPRINT
)

_ALL_RASP = Flags.ASM_RASP_SQLI | Flags.ASM_RASP_LFI | Flags.ASM_RASP_SSRF | Flags.ASM_RASP_SHI
_ALL_RASP = Flags.ASM_RASP_SQLI | Flags.ASM_RASP_LFI | Flags.ASM_RASP_SSRF | Flags.ASM_RASP_SHI | Flags.ASM_RASP_CMDI
_FEATURE_REQUIRED = Flags.ASM_ACTIVATION | Flags.ASM_AUTO_USER


Expand Down
119 changes: 75 additions & 44 deletions ddtrace/appsec/_common_module_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import Union

from wrapt import FunctionWrapper
from wrapt import resolve_path

import ddtrace
from ddtrace.appsec._asm_request_context import get_blocked
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
from ddtrace.appsec._constants import WAF_ACTIONS
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_PATH_TRAVERSAL
import ddtrace.contrib.internal.subprocess.patch as subprocess_patch
from ddtrace.internal import core
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal._unpatched import _gc as gc
Expand All @@ -30,6 +34,9 @@

_is_patched = False

_RASP_SYSTEM = "rasp_os.system"
_RASP_POPEN = "rasp_Popen"


def patch_common_modules():
global _is_patched
Expand All @@ -39,7 +46,10 @@ def patch_common_modules():
try_wrap_function_wrapper("urllib.request", "OpenerDirector.open", wrapped_open_ED4CF71136E15EBF)
try_wrap_function_wrapper("_io", "BytesIO.read", wrapped_read_F3E51D71B4EC16EF)
try_wrap_function_wrapper("_io", "StringIO.read", wrapped_read_F3E51D71B4EC16EF)
try_wrap_function_wrapper("os", "system", wrapped_system_5542593D237084A7)
# ensure that the subprocess patch is applied even after one click activation
subprocess_patch.patch()
subprocess_patch.add_str_callback(_RASP_SYSTEM, wrapped_system_5542593D237084A7)
subprocess_patch.add_lst_callback(_RASP_POPEN, popen_FD233052260D8B4D)
core.on("asm.block.dbapi.execute", execute_4C9BAC8E228EB347)
if asm_config._iast_enabled:
_set_metric_iast_instrumented_sink(VULN_PATH_TRAVERSAL)
Expand All @@ -54,6 +64,8 @@ def unpatch_common_modules():
try_unwrap("urllib.request", "OpenerDirector.open")
try_unwrap("_io", "BytesIO.read")
try_unwrap("_io", "StringIO.read")
subprocess_patch.del_str_callback(_RASP_SYSTEM)
subprocess_patch.del_lst_callback(_RASP_POPEN)
_is_patched = False


Expand Down Expand Up @@ -106,7 +118,6 @@ def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -124,7 +135,9 @@ def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs
rule_type=EXPLOIT_PREVENTION.TYPE.LFI,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "lfi", filename)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.LFI, filename
)
try:
return original_open_callable(*args, **kwargs)
except Exception as e:
Expand All @@ -151,7 +164,6 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -168,7 +180,9 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
rule_type=EXPLOIT_PREVENTION.TYPE.SSRF,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "ssrf", url)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, url
)
return original_open_callable(*args, **kwargs)


Expand All @@ -191,7 +205,6 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -206,50 +219,67 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
rule_type=EXPLOIT_PREVENTION.TYPE.SSRF,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "ssrf", url)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, url
)

return original_request_callable(*args, **kwargs)


def wrapped_system_5542593D237084A7(original_command_callable, instance, args, kwargs):
def wrapped_system_5542593D237084A7(command: str) -> None:
"""
wrapper for os.system function
"""
command = args[0] if args else kwargs.get("command", None)
if command is not None:
if asm_config._iast_enabled and is_iast_request_enabled():
from ddtrace.appsec._iast.taint_sinks.command_injection import _iast_report_cmdi

_iast_report_cmdi(command)

if (
asm_config._asm_enabled
and asm_config._ep_enabled
and ddtrace.tracer._appsec_processor is not None
and ddtrace.tracer._appsec_processor.rasp_cmdi_enabled
):
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
return original_command_callable(*args, **kwargs)

if in_asm_context():
res = call_waf_callback(
{EXPLOIT_PREVENTION.ADDRESS.CMDI: command},
crop_trace="wrapped_system_5542593D237084A7",
rule_type=EXPLOIT_PREVENTION.TYPE.CMDI,
if (
asm_config._asm_enabled
and asm_config._ep_enabled
and ddtrace.tracer._appsec_processor is not None
and ddtrace.tracer._appsec_processor.rasp_shi_enabled
):
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
except ImportError:
return

if in_asm_context():
res = call_waf_callback(
{EXPLOIT_PREVENTION.ADDRESS.SHI: command},
crop_trace="wrapped_system_5542593D237084A7",
rule_type=EXPLOIT_PREVENTION.TYPE.SHI,
)
if res and _must_block(res.actions):
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SHI, command
)


def popen_FD233052260D8B4D(arg_list: Union[List[str], str]) -> None:
"""
listener for subprocess.Popen class
"""
if (
asm_config._asm_enabled
and asm_config._ep_enabled
and ddtrace.tracer._appsec_processor is not None
and ddtrace.tracer._appsec_processor.rasp_cmdi_enabled
):
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
except ImportError:
return

if in_asm_context():
res = call_waf_callback(
{EXPLOIT_PREVENTION.ADDRESS.CMDI: arg_list if isinstance(arg_list, list) else [arg_list]},
crop_trace="popen_FD233052260D8B4D",
rule_type=EXPLOIT_PREVENTION.TYPE.CMDI,
)
if res and _must_block(res.actions):
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.CMDI, arg_list
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "cmdi", command)
try:
return original_command_callable(*args, **kwargs)
except Exception as e:
previous_frame = e.__traceback__.tb_frame.f_back
raise e.with_traceback(
e.__traceback__.__class__(None, previous_frame, previous_frame.f_lasti, previous_frame.f_lineno)
)


_DB_DIALECTS = {
Expand Down Expand Up @@ -279,7 +309,6 @@ def execute_4C9BAC8E228EB347(instrument_self, query, args, kwargs) -> None:
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# execute is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -296,7 +325,9 @@ def execute_4C9BAC8E228EB347(instrument_self, query, args, kwargs) -> None:
rule_type=EXPLOIT_PREVENTION.TYPE.SQLI,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "sqli", query)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SQLI, query
)


def try_unwrap(module, name):
Expand Down
6 changes: 5 additions & 1 deletion ddtrace/appsec/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ class WAF_DATA_NAMES(metaclass=Constant_Class):

# EPHEMERAL ADDRESSES
PROCESSOR_SETTINGS: Literal["waf.context.processor"] = "waf.context.processor"
CMDI_ADDRESS: Literal["server.sys.shell.cmd"] = "server.sys.shell.cmd"
CMDI_ADDRESS: Literal["server.sys.exec.cmd"] = "server.sys.exec.cmd"
SHI_ADDRESS: Literal["server.sys.shell.cmd"] = "server.sys.shell.cmd"
LFI_ADDRESS: Literal["server.io.fs.file"] = "server.io.fs.file"
SSRF_ADDRESS: Literal["server.io.net.url"] = "server.io.net.url"
SQLI_ADDRESS: Literal["server.db.statement"] = "server.db.statement"
Expand Down Expand Up @@ -328,6 +329,7 @@ class DEFAULT(metaclass=Constant_Class):


class EXPLOIT_PREVENTION(metaclass=Constant_Class):
BLOCKING: Literal["exploit_prevention"] = "exploit_prevention"
STACK_TRACE_ID: Literal["stack_id"] = "stack_id"
EP_ENABLED: Literal["DD_APPSEC_RASP_ENABLED"] = "DD_APPSEC_RASP_ENABLED"
STACK_TRACE_ENABLED: Literal["DD_APPSEC_STACK_TRACE_ENABLED"] = "DD_APPSEC_STACK_TRACE_ENABLED"
Expand All @@ -339,13 +341,15 @@ class EXPLOIT_PREVENTION(metaclass=Constant_Class):

class TYPE(metaclass=Constant_Class):
CMDI: Literal["command_injection"] = "command_injection"
SHI: Literal["shell_injection"] = "shell_injection"
LFI: Literal["lfi"] = "lfi"
SSRF: Literal["ssrf"] = "ssrf"
SQLI: Literal["sql_injection"] = "sql_injection"

class ADDRESS(metaclass=Constant_Class):
CMDI: Literal["CMDI_ADDRESS"] = "CMDI_ADDRESS"
LFI: Literal["LFI_ADDRESS"] = "LFI_ADDRESS"
SHI: Literal["SHI_ADDRESS"] = "SHI_ADDRESS"
SSRF: Literal["SSRF_ADDRESS"] = "SSRF_ADDRESS"
SQLI: Literal["SQLI_ADDRESS"] = "SQLI_ADDRESS"
SQLI_TYPE: Literal["SQLI_SYSTEM_ADDRESS"] = "SQLI_SYSTEM_ADDRESS"
Expand Down
Loading

0 comments on commit af0f1c9

Please sign in to comment.