Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
befa9ac
decouple monkey
christophe-papazian Feb 4, 2025
f2d19ba
Merge remote-tracking branch 'origin/3.x-staging' into christophe-pap…
christophe-papazian Feb 4, 2025
abf96da
fix patch logic
christophe-papazian Feb 4, 2025
27bee95
import iast only if enabled in asm_request_context
christophe-papazian Feb 4, 2025
9e14f11
Merge remote-tracking branch 'origin/3.x-staging' into christophe-pap…
christophe-papazian Feb 4, 2025
ee6950d
factor logic
christophe-papazian Feb 4, 2025
bdca801
fix subprocess tests
christophe-papazian Feb 4, 2025
517659d
ensure patch is working on windows
christophe-papazian Feb 4, 2025
f85e7d6
ensure unpatch is guarded
christophe-papazian Feb 4, 2025
c0a5e17
fix test
christophe-papazian Feb 4, 2025
c460b93
fix selenium test
christophe-papazian Feb 5, 2025
1582b13
disable subprocess patches when asm disabled
christophe-papazian Feb 5, 2025
4532d04
enable subprocess patches when iast enabled
christophe-papazian Feb 5, 2025
5ec6f3b
ensure IAST is enabled at start for appsec_integrations_flask tests
christophe-papazian Feb 5, 2025
05de3c1
disable iast explicitely when required
christophe-papazian Feb 5, 2025
31739c8
Merge branch '3.x-staging' into christophe-papazian/decouple_appsec_f…
christophe-papazian Feb 5, 2025
15a2561
more iast guards on pytest and langchain. remove dead code
christophe-papazian Feb 5, 2025
a265dd0
complete decoupling
christophe-papazian Feb 5, 2025
20dbe8d
fix lint
christophe-papazian Feb 5, 2025
fed3053
unpatch less strict
christophe-papazian Feb 5, 2025
0da9bea
revert test_serverless
christophe-papazian Feb 6, 2025
47e11f9
Merge branch '3.x-staging' into christophe-papazian/decouple_appsec_f…
christophe-papazian Feb 6, 2025
e2d8767
fix subprocess test
christophe-papazian Feb 6, 2025
e74b6f9
Merge branch 'christophe-papazian/decouple_appsec_from_tracer' of git…
christophe-papazian Feb 6, 2025
cfa82c9
fix subprocess test
christophe-papazian Feb 6, 2025
2d9d36c
add minimal test for loading module
christophe-papazian Feb 6, 2025
6cad0b7
add release note
christophe-papazian Feb 6, 2025
72b3bbc
make sure env vars are updated as expected for load_module test
christophe-papazian Feb 6, 2025
01ef338
Merge branch 'main' into christophe-papazian/decouple_appsec_from_tracer
christophe-papazian Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions ddtrace/appsec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# this module must not load any other unsafe appsec module directly

from ddtrace.internal import core
from ddtrace.settings.asm import config as asm_config


_APPSEC_TO_BE_LOADED = True
Expand Down Expand Up @@ -28,7 +29,9 @@ def load_iast():

def load_common_appsec_modules():
"""Lazily load the common module patches."""
if (asm_config._ep_enabled and asm_config._asm_enabled) or asm_config._iast_enabled:
from ddtrace.settings.asm import config as asm_config

if asm_config._load_modules:
from ddtrace.appsec._common_module_patches import patch_common_modules

patch_common_modules()
Expand Down
13 changes: 10 additions & 3 deletions ddtrace/appsec/_asm_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
from ddtrace.appsec._constants import APPSEC
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
from ddtrace.appsec._constants import SPAN_DATA_NAMES
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled
from ddtrace.appsec._iast._taint_tracking import OriginType
from ddtrace.appsec._iast._taint_tracking._taint_objects import taint_pyobject
from ddtrace.appsec._utils import add_context_log
from ddtrace.appsec._utils import get_triggers
from ddtrace.internal import core
Expand All @@ -28,6 +25,16 @@
from ddtrace.trace import Span


if asm_config._iast_enabled:
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled
from ddtrace.appsec._iast._taint_tracking import OriginType
from ddtrace.appsec._iast._taint_tracking._taint_objects import taint_pyobject
else:

def is_iast_request_enabled() -> bool:
return False


if TYPE_CHECKING:
from ddtrace.appsec._ddwaf import DDWaf_info
from ddtrace.appsec._ddwaf import DDWaf_result
Expand Down
8 changes: 4 additions & 4 deletions ddtrace/appsec/_common_module_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def is_iast_request_enabled() -> bool:

def patch_common_modules():
global _is_patched
# 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)
if _is_patched:
return
# for testing purposes, we need to update is_iast_request_enabled
Expand All @@ -60,10 +64,6 @@ def is_iast_request_enabled() -> bool:
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)
# 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:
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
Expand Down
2 changes: 2 additions & 0 deletions ddtrace/appsec/_constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# this module must not load any other unsafe appsec module directly

import os
from re import Match
import sys
Expand Down
4 changes: 3 additions & 1 deletion ddtrace/appsec/_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# this module must not load any other unsafe appsec module directly

import logging
import sys
from typing import Any
import uuid

from ddtrace.appsec._constants import API_SECURITY
from ddtrace.appsec._constants import APPSEC
from ddtrace.appsec._constants import SPAN_DATA_NAMES
from ddtrace.internal._unpatched import unpatched_json_loads
from ddtrace.internal.compat import to_unicode
from ddtrace.internal.logger import get_logger
Expand All @@ -21,7 +24,6 @@ def parse_response_body(raw_body):
import xmltodict

from ddtrace.appsec import _asm_request_context
from ddtrace.appsec._constants import SPAN_DATA_NAMES
from ddtrace.contrib.internal.trace_utils import _get_header_value_case_insensitive

if not raw_body:
Expand Down
5 changes: 3 additions & 2 deletions ddtrace/contrib/internal/httplib/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import wrapt

from ddtrace import config
from ddtrace.appsec._common_module_patches import wrapped_request_D8CB81E472AF98A2 as _wrap_request_asm
from ddtrace.constants import _ANALYTICS_SAMPLE_RATE_KEY
from ddtrace.constants import SPAN_KIND
from ddtrace.contrib import trace_utils
Expand Down Expand Up @@ -77,12 +76,14 @@ def _wrap_getresponse(func, instance, args, kwargs):


def _call_asm_wrap(func, instance, *args, **kwargs):
from ddtrace.appsec._common_module_patches import wrapped_request_D8CB81E472AF98A2 as _wrap_request_asm

_wrap_request_asm(func, instance, args, kwargs)


def _wrap_request(func, instance, args, kwargs):
# Use any attached tracer if available, otherwise use the global tracer
if asm_config._iast_enabled or asm_config._asm_enabled:
if asm_config._iast_enabled or (asm_config._asm_enabled and asm_config._ep_enabled):
func_to_call = functools.partial(_call_asm_wrap, func, instance)
else:
func_to_call = func
Expand Down
23 changes: 0 additions & 23 deletions ddtrace/contrib/internal/langchain/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1090,29 +1090,6 @@ def unpatch():
delattr(langchain, "_datadog_integration")


def taint_outputs(instance, inputs, outputs):
from ddtrace.appsec._iast._metrics import _set_iast_error_metric
from ddtrace.appsec._iast._taint_tracking._taint_objects import get_tainted_ranges
from ddtrace.appsec._iast._taint_tracking._taint_objects import taint_pyobject

try:
ranges = None
for key in filter(lambda x: x in inputs, instance.input_keys):
input_val = inputs.get(key)
if input_val:
ranges = get_tainted_ranges(input_val)
if ranges:
break

if ranges:
source = ranges[0].source
for key in filter(lambda x: x in outputs, instance.output_keys):
output_value = outputs[key]
outputs[key] = taint_pyobject(output_value, source.name, source.value, source.origin)
except Exception as e:
_set_iast_error_metric("IAST propagation error. langchain taint_outputs. {}".format(e))


def taint_parser_output(func, instance, args, kwargs):
from ddtrace.appsec._iast._metrics import _set_iast_error_metric
from ddtrace.appsec._iast._taint_tracking._taint_objects import get_tainted_ranges
Expand Down
5 changes: 3 additions & 2 deletions ddtrace/contrib/internal/mysql/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import wrapt

from ddtrace import config
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION
from ddtrace.contrib.dbapi import TracedConnection
from ddtrace.contrib.internal.trace_utils import _convert_to_string
from ddtrace.ext import db
Expand Down Expand Up @@ -51,6 +49,9 @@ def patch():
mysql.connector.Connect = mysql.connector.connect

if asm_config._iast_enabled:
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION

_set_metric_iast_instrumented_sink(VULN_SQL_INJECTION)
mysql.connector._datadog_patch = True

Expand Down
5 changes: 3 additions & 2 deletions ddtrace/contrib/internal/mysqldb/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from wrapt import wrap_function_wrapper as _w

from ddtrace import config
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION
from ddtrace.constants import _SPAN_MEASURED_KEY
from ddtrace.constants import SPAN_KIND
from ddtrace.contrib.dbapi import TracedConnection
Expand Down Expand Up @@ -67,6 +65,9 @@ def patch():
_w("MySQLdb", "connect", _connect)

if asm_config._iast_enabled:
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION

_set_metric_iast_instrumented_sink(VULN_SQL_INJECTION)


Expand Down
6 changes: 4 additions & 2 deletions ddtrace/contrib/internal/pytest/_plugin_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from ddtrace.internal.test_visibility.api import InternalTestSession
from ddtrace.internal.test_visibility.api import InternalTestSuite
from ddtrace.internal.test_visibility.coverage_lines import CoverageLines
from ddtrace.settings.asm import config as asm_config
from ddtrace.vendor.debtcollector import deprecate


Expand Down Expand Up @@ -574,9 +575,10 @@ def _pytest_terminal_summary_post_yield(terminalreporter, failed_reports_initial
def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""Report flaky or failed tests"""
try:
from ddtrace.appsec._iast._pytest_plugin import print_iast_report
if asm_config._iast_enabled:
from ddtrace.appsec._iast._pytest_plugin import print_iast_report

print_iast_report(terminalreporter)
print_iast_report(terminalreporter)
except Exception: # noqa: E722
log.debug("Encountered error during code security summary", exc_info=True)

Expand Down
5 changes: 4 additions & 1 deletion ddtrace/contrib/internal/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
import pytest

from ddtrace import config
from ddtrace.appsec._iast._pytest_plugin import ddtrace_iast # noqa:F401
from ddtrace.contrib.internal.pytest._utils import _USE_PLUGIN_V2
from ddtrace.contrib.internal.pytest._utils import _extract_span
from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_itr
from ddtrace.settings.asm import config as asm_config


if asm_config._iast_enabled:
from ddtrace.appsec._iast._pytest_plugin import ddtrace_iast # noqa:F401


# pytest default settings
config._add(
"pytest",
Expand Down
23 changes: 17 additions & 6 deletions ddtrace/contrib/internal/requests/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
from wrapt import wrap_function_wrapper as _w

from ddtrace import config
from ddtrace.appsec._common_module_patches import wrapped_request_D8CB81E472AF98A2 as _wrap_request
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SSRF
from ddtrace.contrib.internal.trace_utils import unwrap as _u
from ddtrace.internal.schema import schematize_service_name
from ddtrace.internal.utils.formats import asbool
Expand Down Expand Up @@ -46,10 +43,16 @@ def patch():

_w("requests", "Session.send", _wrap_send)
# IAST needs to wrap this function because `Session.send` is too late
_w("requests", "Session.request", _wrap_request)
if asm_config._load_modules:
from ddtrace.appsec._common_module_patches import wrapped_request_D8CB81E472AF98A2 as _wrap_request

_w("requests", "Session.request", _wrap_request)
Pin(_config=config.requests).onto(requests.Session)

if asm_config._iast_enabled:
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SSRF

_set_metric_iast_instrumented_sink(VULN_SSRF)


Expand All @@ -59,5 +62,13 @@ def unpatch():
return
requests.__datadog_patch = False

_u(requests.Session, "request")
_u(requests.Session, "send")
try:
_u(requests.Session, "request")
except AttributeError:
# It was not patched
pass
try:
_u(requests.Session, "send")
except AttributeError:
# It was not patched
pass
5 changes: 3 additions & 2 deletions ddtrace/contrib/internal/sqlalchemy/patch.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import sqlalchemy
from wrapt import wrap_function_wrapper as _w

from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION
from ddtrace.contrib.internal.trace_utils import unwrap
from ddtrace.settings.asm import config as asm_config

Expand All @@ -24,6 +22,9 @@ def patch():
_w("sqlalchemy.engine", "create_engine", _wrap_create_engine)

if asm_config._iast_enabled:
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION

_set_metric_iast_instrumented_sink(VULN_SQL_INJECTION)


Expand Down
5 changes: 3 additions & 2 deletions ddtrace/contrib/internal/sqlite3/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import wrapt

from ddtrace import config
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION
from ddtrace.contrib.dbapi import FetchTracedCursor
from ddtrace.contrib.dbapi import TracedConnection
from ddtrace.contrib.dbapi import TracedCursor
Expand Down Expand Up @@ -47,6 +45,9 @@ def patch():
sqlite3.dbapi2.connect = wrapped

if asm_config._iast_enabled:
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION

_set_metric_iast_instrumented_sink(VULN_SQL_INJECTION)


Expand Down
23 changes: 14 additions & 9 deletions ddtrace/contrib/internal/subprocess/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ def del_lst_callback(name: str):


def patch() -> List[str]:
if not (asm_config._asm_enabled or asm_config._iast_enabled):
if not asm_config._load_modules:
return []
patched: List[str] = []

import os # nosec
import subprocess # nosec

should_patch_system = not trace_utils.iswrapped(os.system)
should_patch_fork = not trace_utils.iswrapped(os.fork)
should_patch_fork = (not trace_utils.iswrapped(os.fork)) if hasattr(os, "fork") else False
spawnvef = getattr(os, "_spawnvef", None)
should_patch_spawnvef = spawnvef is not None and not trace_utils.iswrapped(spawnvef)

Expand Down Expand Up @@ -316,18 +316,19 @@ def unpatch() -> None:
import os # nosec
import subprocess # nosec

trace_utils.unwrap(os, "system")
trace_utils.unwrap(os, "_spawnvef")
trace_utils.unwrap(subprocess.Popen, "__init__")
trace_utils.unwrap(subprocess.Popen, "wait")
for obj, attr in [(os, "system"), (os, "_spawnvef"), (subprocess.Popen, "__init__"), (subprocess.Popen, "wait")]:
try:
trace_utils.unwrap(obj, attr)
except AttributeError:
pass

SubprocessCmdLine._clear_cache()


@trace_utils.with_traced_module
def _traced_ossystem(module, pin, wrapped, instance, args, kwargs):
try:
if asm_config._bypass_instrumentation_for_waf:
if asm_config._bypass_instrumentation_for_waf or not (asm_config._asm_enabled or asm_config._iast_enabled):
return wrapped(*args, **kwargs)
if isinstance(args[0], str):
for callback in _STR_CALLBACKS.values():
Expand All @@ -351,6 +352,8 @@ def _traced_ossystem(module, pin, wrapped, instance, args, kwargs):

@trace_utils.with_traced_module
def _traced_fork(module, pin, wrapped, instance, args, kwargs):
if not (asm_config._asm_enabled or asm_config._iast_enabled):
return wrapped(*args, **kwargs)
try:
with pin.tracer.trace(COMMANDS.SPAN_NAME, resource="fork", span_type=SpanTypes.SYSTEM) as span:
span.set_tag(COMMANDS.EXEC, ["os.fork"])
Expand All @@ -366,6 +369,8 @@ def _traced_fork(module, pin, wrapped, instance, args, kwargs):

@trace_utils.with_traced_module
def _traced_osspawn(module, pin, wrapped, instance, args, kwargs):
if not (asm_config._asm_enabled or asm_config._iast_enabled):
return wrapped(*args, **kwargs)
try:
mode, file, func_args, _, _ = args
if isinstance(func_args, (list, tuple, str)):
Expand Down Expand Up @@ -395,7 +400,7 @@ def _traced_osspawn(module, pin, wrapped, instance, args, kwargs):
@trace_utils.with_traced_module
def _traced_subprocess_init(module, pin, wrapped, instance, args, kwargs):
try:
if asm_config._bypass_instrumentation_for_waf:
if asm_config._bypass_instrumentation_for_waf or not (asm_config._asm_enabled or asm_config._iast_enabled):
return wrapped(*args, **kwargs)
cmd_args = args[0] if len(args) else kwargs["args"]
if isinstance(cmd_args, (list, tuple, str)):
Expand Down Expand Up @@ -429,7 +434,7 @@ def _traced_subprocess_init(module, pin, wrapped, instance, args, kwargs):
@trace_utils.with_traced_module
def _traced_subprocess_wait(module, pin, wrapped, instance, args, kwargs):
try:
if asm_config._bypass_instrumentation_for_waf:
if asm_config._bypass_instrumentation_for_waf or not (asm_config._asm_enabled or asm_config._iast_enabled):
return wrapped(*args, **kwargs)
binary = core.get_item("subprocess_popen_binary")

Expand Down
Loading
Loading