diff --git a/ddtrace/appsec/_asm_request_context.py b/ddtrace/appsec/_asm_request_context.py index 6e03dae6bb4..ae8597300c9 100644 --- a/ddtrace/appsec/_asm_request_context.py +++ b/ddtrace/appsec/_asm_request_context.py @@ -8,6 +8,7 @@ from typing import List from typing import Optional from typing import Set +from typing import Union from urllib import parse from ddtrace._trace.span import Span @@ -109,9 +110,15 @@ def unregister(span: Span) -> None: env.must_call_globals = False +def update_span_metrics(span: Span, name: str, value: Union[float, int]) -> None: + span.set_metric(name, value + (span.get_metric(name) or 0.0)) + + def flush_waf_triggers(env: ASM_Environment) -> None: - if env.waf_triggers and env.span: - root_span = env.span._local_root or env.span + if not env.span: + return + root_span = env.span._local_root or env.span + if env.waf_triggers: report_list = get_triggers(root_span) if report_list is not None: report_list.extend(env.waf_triggers) @@ -122,6 +129,18 @@ def flush_waf_triggers(env: ASM_Environment) -> None: else: root_span.set_tag(APPSEC.JSON, json.dumps({"triggers": report_list}, separators=(",", ":"))) env.waf_triggers = [] + telemetry_results = get_value(_TELEMETRY, _TELEMETRY_WAF_RESULTS) + if telemetry_results: + from ddtrace.appsec._metrics import DDWAF_VERSION + + root_span.set_tag_str(APPSEC.WAF_VERSION, DDWAF_VERSION) + if telemetry_results["total_duration"]: + update_span_metrics(root_span, APPSEC.WAF_DURATION, telemetry_results["duration"]) + update_span_metrics(root_span, APPSEC.WAF_DURATION_EXT, telemetry_results["total_duration"]) + if telemetry_results["rasp"]["sum_eval"]: + update_span_metrics(root_span, APPSEC.RASP_DURATION, telemetry_results["rasp"]["duration"]) + update_span_metrics(root_span, APPSEC.RASP_DURATION_EXT, telemetry_results["rasp"]["total_duration"]) + update_span_metrics(root_span, APPSEC.RASP_RULE_EVAL, telemetry_results["rasp"]["sum_eval"]) GLOBAL_CALLBACKS[_CONTEXT_CALL] = [flush_waf_triggers] @@ -150,8 +169,12 @@ def __init__(self): "triggered": False, "timeout": False, "version": None, + "duration": 0.0, + "total_duration": 0.0, "rasp": { - "called": False, + "sum_eval": 0, + "duration": 0.0, + "total_duration": 0.0, "eval": {t: 0 for _, t in EXPLOIT_PREVENTION.TYPE}, "match": {t: 0 for _, t in EXPLOIT_PREVENTION.TYPE}, "timeout": {t: 0 for _, t in EXPLOIT_PREVENTION.TYPE}, @@ -344,6 +367,8 @@ def set_waf_telemetry_results( is_blocked: bool, is_timeout: bool, rule_type: Optional[str], + duration: float, + total_duration: float, ) -> None: result = get_value(_TELEMETRY, _TELEMETRY_WAF_RESULTS) if result is not None: @@ -354,12 +379,16 @@ def set_waf_telemetry_results( result["timeout"] |= is_timeout if rules_version is not None: result["version"] = rules_version + result["duration"] += duration + result["total_duration"] += total_duration else: # Exploit Prevention telemetry - result["rasp"]["called"] = True + result["rasp"]["sum_eval"] += 1 result["rasp"]["eval"][rule_type] += 1 result["rasp"]["match"][rule_type] += int(is_triggered) result["rasp"]["timeout"][rule_type] += int(is_timeout) + result["rasp"]["duration"] += duration + result["rasp"]["total_duration"] += total_duration def get_waf_telemetry_results() -> Optional[Dict[str, Any]]: diff --git a/ddtrace/appsec/_constants.py b/ddtrace/appsec/_constants.py index 59f90a335dc..5ce16dd3130 100644 --- a/ddtrace/appsec/_constants.py +++ b/ddtrace/appsec/_constants.py @@ -53,6 +53,9 @@ class APPSEC(metaclass=Constant_Class): WAF_DURATION_EXT = "_dd.appsec.waf.duration_ext" WAF_TIMEOUTS = "_dd.appsec.waf.timeouts" WAF_VERSION = "_dd.appsec.waf.version" + RASP_DURATION = "_dd.appsec.rasp.duration" + RASP_DURATION_EXT = "_dd.appsec.rasp.duration_ext" + RASP_RULE_EVAL = "_dd.appsec.rasp.rule.eval" ORIGIN_VALUE = "appsec" CUSTOM_EVENT_PREFIX = "appsec.events" USER_LOGIN_EVENT_PREFIX = "_dd.appsec.events.users.login" diff --git a/ddtrace/appsec/_metrics.py b/ddtrace/appsec/_metrics.py index 28d712cebf7..e36212e6a93 100644 --- a/ddtrace/appsec/_metrics.py +++ b/ddtrace/appsec/_metrics.py @@ -106,7 +106,7 @@ def _set_waf_request_metrics(*args): tags=tags_request, ) rasp = result["rasp"] - if rasp["called"]: + if rasp["sum_eval"]: for t, n in [("eval", "rasp.rule.eval"), ("match", "rasp.rule.match"), ("timeout", "rasp.timeout")]: for rule_type, value in rasp[t].items(): if value: diff --git a/ddtrace/appsec/_processor.py b/ddtrace/appsec/_processor.py index a3d0518b6a4..db0b83d6eb4 100644 --- a/ddtrace/appsec/_processor.py +++ b/ddtrace/appsec/_processor.py @@ -345,6 +345,8 @@ def _waf_action( bool(blocked), waf_results.timeout, rule_type, + waf_results.runtime, + waf_results.total_runtime, ) if blocked: core.set_item(WAF_CONTEXT_NAMES.BLOCKED, blocked, span=span) @@ -357,21 +359,8 @@ def _waf_action( span.set_tag_str(APPSEC.EVENT_RULE_ERRORS, errors) log.debug("Error in ASM In-App WAF: %s", errors) span.set_tag_str(APPSEC.EVENT_RULE_VERSION, info.version) - from ddtrace.appsec._ddwaf import version - - span.set_tag_str(APPSEC.WAF_VERSION, version()) - - def update_metric(name, value): - old_value = span.get_metric(name) - if old_value is None: - old_value = 0.0 - span.set_metric(name, value + old_value) - span.set_metric(APPSEC.EVENT_RULE_LOADED, info.loaded) span.set_metric(APPSEC.EVENT_RULE_ERROR_COUNT, info.failed) - if waf_results.runtime: - update_metric(APPSEC.WAF_DURATION, waf_results.runtime) - update_metric(APPSEC.WAF_DURATION_EXT, waf_results.total_runtime) except (JSONDecodeError, ValueError): log.warning("Error parsing data ASM In-App WAF metrics report %s", info.errors) except Exception: diff --git a/ddtrace/settings/asm.py b/ddtrace/settings/asm.py index 91fb9417807..462a704a9f0 100644 --- a/ddtrace/settings/asm.py +++ b/ddtrace/settings/asm.py @@ -92,7 +92,7 @@ class ASMConfig(Env): _deduplication_enabled = Env.var(bool, "_DD_APPSEC_DEDUPLICATION_ENABLED", default=True) # default will be set to True once the feature is GA. For now it's always False - _ep_enabled = Env.var(bool, EXPLOIT_PREVENTION.EP_ENABLED, default=False) + _ep_enabled = Env.var(bool, EXPLOIT_PREVENTION.EP_ENABLED, default=True) _ep_stack_trace_enabled = Env.var(bool, EXPLOIT_PREVENTION.STACK_TRACE_ENABLED, default=True) # for max_stack_traces, 0 == unlimited _ep_max_stack_traces = Env.var( diff --git a/releasenotes/notes/exploit_prevention_enabled-ae26036621f6140c.yaml b/releasenotes/notes/exploit_prevention_enabled-ae26036621f6140c.yaml new file mode 100644 index 00000000000..2600693a8f1 --- /dev/null +++ b/releasenotes/notes/exploit_prevention_enabled-ae26036621f6140c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + ASM: This introduces full support for exploit prevention in the python tracer. + - LFI (via standard API open) + - SSRF (via standard API urllib or third party requests) + with monitoring and blocking feature, telemetry and span metrics reports. diff --git a/tests/appsec/contrib_appsec/conftest.py b/tests/appsec/contrib_appsec/conftest.py index 65b8fcc1893..356c2fced5c 100644 --- a/tests/appsec/contrib_appsec/conftest.py +++ b/tests/appsec/contrib_appsec/conftest.py @@ -68,6 +68,11 @@ def get_tag(root_span): yield lambda name: root_span().get_tag(name) +@pytest.fixture +def get_metric(root_span): + yield lambda name: root_span().get_metric(name) + + def no_op(msg: str) -> None: # noqa: ARG001 """Do nothing.""" diff --git a/tests/appsec/contrib_appsec/utils.py b/tests/appsec/contrib_appsec/utils.py index a1f6441db56..850c3d91fa9 100644 --- a/tests/appsec/contrib_appsec/utils.py +++ b/tests/appsec/contrib_appsec/utils.py @@ -1202,6 +1202,7 @@ def test_exploit_prevention( interface, root_span, get_tag, + get_metric, asm_enabled, ep_enabled, endpoint, @@ -1215,6 +1216,7 @@ def test_exploit_prevention( from ddtrace.appsec._common_module_patches import patch_common_modules from ddtrace.appsec._common_module_patches import unpatch_common_modules + from ddtrace.appsec._constants import APPSEC from ddtrace.appsec._metrics import DDWAF_VERSION from ddtrace.contrib.requests import patch as patch_requests from ddtrace.contrib.requests import unpatch as unpatch_requests @@ -1260,6 +1262,11 @@ def test_exploit_prevention( assert get_tag("rasp.request.done") is None else: assert get_tag("rasp.request.done") == endpoint + assert get_metric(APPSEC.RASP_DURATION) is not None + assert get_metric(APPSEC.RASP_DURATION_EXT) is not None + assert get_metric(APPSEC.RASP_RULE_EVAL) is not None + assert float(get_metric(APPSEC.RASP_DURATION_EXT)) >= float(get_metric(APPSEC.RASP_DURATION)) + assert int(get_metric(APPSEC.RASP_RULE_EVAL)) > 0 else: assert get_triggers(root_span()) is None assert self.check_for_stack_trace(root_span) == [] diff --git a/tests/contrib/django/test_django_appsec_snapshots.py b/tests/contrib/django/test_django_appsec_snapshots.py index df82a47963d..bc8a1d20dcb 100644 --- a/tests/contrib/django/test_django_appsec_snapshots.py +++ b/tests/contrib/django/test_django_appsec_snapshots.py @@ -71,6 +71,9 @@ def daphne_client(django_asgi, additional_env=None): "meta_struct", "metrics._dd.appsec.waf.duration", "metrics._dd.appsec.waf.duration_ext", + "metrics._dd.appsec.rasp.duration", + "metrics._dd.appsec.rasp.duration_ext", + "metrics._dd.appsec.rasp.rule.eval", APPSEC_JSON_TAG, ] ) @@ -93,6 +96,9 @@ def test_appsec_enabled(): "meta_struct", "metrics._dd.appsec.waf.duration", "metrics._dd.appsec.waf.duration_ext", + "metrics._dd.appsec.rasp.duration", + "metrics._dd.appsec.rasp.duration_ext", + "metrics._dd.appsec.rasp.rule.eval", APPSEC_JSON_TAG, ] ) @@ -113,6 +119,9 @@ def test_appsec_enabled_attack(): "meta_struct", "metrics._dd.appsec.waf.duration", "metrics._dd.appsec.waf.duration_ext", + "metrics._dd.appsec.rasp.duration", + "metrics._dd.appsec.rasp.duration_ext", + "metrics._dd.appsec.rasp.rule.eval", APPSEC_JSON_TAG, "metrics._dd.appsec.event_rules.loaded", ] @@ -145,6 +154,9 @@ def test_request_ipblock_nomatch_200(): "meta_struct", "metrics._dd.appsec.waf.duration", "metrics._dd.appsec.waf.duration_ext", + "metrics._dd.appsec.rasp.duration", + "metrics._dd.appsec.rasp.duration_ext", + "metrics._dd.appsec.rasp.rule.eval", "metrics._dd.appsec.event_rules.loaded", ] ) @@ -182,6 +194,9 @@ def test_request_ipblock_match_403(): "meta_struct", "metrics._dd.appsec.waf.duration", "metrics._dd.appsec.waf.duration_ext", + "metrics._dd.appsec.rasp.duration", + "metrics._dd.appsec.rasp.duration_ext", + "metrics._dd.appsec.rasp.rule.eval", "metrics._dd.appsec.event_rules.loaded", ] )