diff --git a/ddtrace/appsec/_api_security/api_manager.py b/ddtrace/appsec/_api_security/api_manager.py index 9f4e4530909..2937f0a2137 100644 --- a/ddtrace/appsec/_api_security/api_manager.py +++ b/ddtrace/appsec/_api_security/api_manager.py @@ -8,6 +8,7 @@ from ddtrace._trace._limits import MAX_SPAN_META_VALUE_LEN from ddtrace._trace.processor.resource_renaming import SimplifiedEndpointComputer +from ddtrace.appsec._asm_request_context import _WAF_CALL from ddtrace.appsec._asm_request_context import ASM_Environment from ddtrace.appsec._constants import API_SECURITY from ddtrace.appsec._constants import SPAN_DATA_NAMES @@ -196,7 +197,8 @@ def _schema_callback(self, env): value = transform(value) waf_payload[address] = value - result = self._asm_context.call_waf_callback(waf_payload) + callback = env.callbacks[_WAF_CALL] + result = callback(waf_payload) if result is None: return nb_schemas = 0 diff --git a/ddtrace/appsec/_asm_request_context.py b/ddtrace/appsec/_asm_request_context.py index 8ba2b03b9d0..3077add5100 100644 --- a/ddtrace/appsec/_asm_request_context.py +++ b/ddtrace/appsec/_asm_request_context.py @@ -283,6 +283,7 @@ def finalize_asm_env(env: ASM_Environment) -> None: flush_waf_triggers(env) for function in env.callbacks[_CONTEXT_CALL]: function(env) + env.callbacks.clear() entry_span = env.entry_span if entry_span: if env.waf_info: @@ -332,11 +333,17 @@ def set_body_response(body_response): # local import to avoid circular import from ddtrace.appsec._utils import parse_response_body + env = _get_asm_context() + if env is None: + extra = {"product": "appsec", "more_info": "::set_body_response", "stack_limit": 4} + logger.debug("asm_context::set_body_response::no_active_context", extra=extra, stack_info=True) + return + set_waf_address( SPAN_DATA_NAMES.RESPONSE_BODY, lambda: parse_response_body( body_response, - get_waf_address(SPAN_DATA_NAMES.RESPONSE_HEADERS_NO_COOKIES), + env.waf_addresses.get(SPAN_DATA_NAMES.RESPONSE_HEADERS_NO_COOKIES, None), ), ) @@ -355,7 +362,7 @@ def set_waf_address(address: str, value: Any) -> None: def get_value(category: str, address: str, default: Any = None) -> Any: env = _get_asm_context() - if env is None: + if env is None or env.finalized: extra = {"product": "appsec", "more_info": f"::{category}::{address}", "stack_limit": 4} logger.debug("asm_context::get_value::no_active_context", extra=extra, stack_info=True) return default diff --git a/ddtrace/appsec/_ddwaf/__init__.py b/ddtrace/appsec/_ddwaf/__init__.py index d232f97ac93..e554ca3f31b 100644 --- a/ddtrace/appsec/_ddwaf/__init__.py +++ b/ddtrace/appsec/_ddwaf/__init__.py @@ -1,28 +1,28 @@ +from typing import Optional + from ddtrace.appsec._ddwaf.waf_stubs import WAF from ddtrace.appsec._ddwaf.waf_stubs import DDWafRulesType from ddtrace.appsec._utils import DDWaf_info from ddtrace.appsec._utils import DDWaf_result from ddtrace.internal.logger import get_logger -from ddtrace.internal.settings.asm import config as asm_config -__all__ = ["DDWaf", "DDWaf_info", "DDWaf_result", "version", "DDWafRulesType"] +__all__ = ["WAF", "DDWaf_info", "DDWaf_result", "version", "DDWafRulesType"] LOGGER = get_logger(__name__) _DDWAF_LOADED: bool = False +version: str = "unloaded" + -if asm_config._asm_libddwaf_available: +def waf_module() -> Optional[type[WAF]]: try: import ddtrace.appsec._ddwaf.waf as waf_module + global _DDWAF_LOADED, version _DDWAF_LOADED = True + version = waf_module.version() + return waf_module.DDWaf except Exception: - import ddtrace.appsec._ddwaf.waf_mock as waf_module # type: ignore[no-redef] - LOGGER.warning("DDWaf features disabled. WARNING: Dynamic Library not loaded", exc_info=True) -else: - import ddtrace.appsec._ddwaf.waf_mock as waf_module # type: ignore[no-redef] - -DDWaf: type[WAF] = waf_module.DDWaf -version = waf_module.version + return None diff --git a/ddtrace/appsec/_ddwaf/waf_mock.py b/ddtrace/appsec/_ddwaf/waf_mock.py deleted file mode 100644 index 6c96e56aa88..00000000000 --- a/ddtrace/appsec/_ddwaf/waf_mock.py +++ /dev/null @@ -1,76 +0,0 @@ -from typing import Any -from typing import Optional -from typing import Sequence - -from ddtrace.appsec._constants import DEFAULT -from ddtrace.appsec._ddwaf.waf_stubs import WAF -from ddtrace.appsec._ddwaf.waf_stubs import DDWaf_info -from ddtrace.appsec._ddwaf.waf_stubs import DDWaf_result -from ddtrace.appsec._ddwaf.waf_stubs import DDWafRulesType -from ddtrace.appsec._ddwaf.waf_stubs import PayloadType -from ddtrace.appsec._ddwaf.waf_stubs import ddwaf_context_capsule -from ddtrace.appsec._utils import _observator -from ddtrace.internal.logger import get_logger - - -DDWAF_ERR_INTERNAL = -3 -DDWAF_ERR_INVALID_OBJECT = -2 -DDWAF_ERR_INVALID_ARGUMENT = -1 -DDWAF_OK = 0 -DDWAF_MATCH = 1 - - -LOGGER = get_logger(__name__) - - -# Mockup of the DDWaf class doing nothing -class DDWaf(WAF): - empty_observator = _observator() - - def __init__( - self, - rules: dict[str, Any], - obfuscation_parameter_key_regexp: bytes, - obfuscation_parameter_value_regexp: bytes, - metrics, - ): - self._handle = None - - def run( - self, - ctx: ddwaf_context_capsule, - data: DDWafRulesType, - ephemeral_data: Optional[DDWafRulesType] = None, - timeout_ms: float = DEFAULT.WAF_TIMEOUT, - ) -> DDWaf_result: - LOGGER.debug("DDWaf features disabled. dry run") - return DDWaf_result(0, [], {}, 0.0, 0.0, False, self.empty_observator, {}) - - def update_rules( - self, removals: Sequence[tuple[str, str]], updates: Sequence[tuple[str, str, PayloadType]] - ) -> bool: - LOGGER.debug("DDWaf features disabled. dry update") - return False - - def _at_request_start(self) -> None: - return None - - def _at_request_end(self) -> None: - pass - - @property - def required_data(self): - return [] - - @property - def info(self): - return DDWaf_info(0, 0, "", "") - - @property - def initialized(self) -> bool: - return False - - -def version() -> str: - LOGGER.debug("DDWaf features disabled. null version") - return "0.0.0" diff --git a/ddtrace/appsec/_processor.py b/ddtrace/appsec/_processor.py index 165191a8bf9..9fa30541de6 100644 --- a/ddtrace/appsec/_processor.py +++ b/ddtrace/appsec/_processor.py @@ -2,21 +2,12 @@ import errno from json.decoder import JSONDecodeError import os -from typing import TYPE_CHECKING from typing import Any from typing import ClassVar from typing import Optional from typing import Sequence from typing import Union -from ddtrace.ext import SpanTypes -from ddtrace.internal import core - - -if TYPE_CHECKING: - import ddtrace.appsec._ddwaf as ddwaf - - from ddtrace._trace.processor import SpanProcessor from ddtrace._trace.span import Span from ddtrace.appsec import _asm_request_context @@ -27,6 +18,8 @@ from ddtrace.appsec._constants import STACK_TRACE from ddtrace.appsec._constants import WAF_ACTIONS from ddtrace.appsec._constants import WAF_DATA_NAMES +import ddtrace.appsec._ddwaf.ddwaf_types as ddwaf_types +from ddtrace.appsec._ddwaf.waf_stubs import WAF from ddtrace.appsec._exploit_prevention.stack_traces import report_stack from ddtrace.appsec._trace_utils import _asm_manual_keep from ddtrace.appsec._utils import Binding_error @@ -35,6 +28,8 @@ from ddtrace.appsec._utils import is_inferred_span from ddtrace.constants import _ORIGIN_KEY from ddtrace.constants import _RUNTIME_FAMILY +from ddtrace.ext import SpanTypes +from ddtrace.internal import core from ddtrace.internal._unpatched import unpatched_open as open # noqa: A004 from ddtrace.internal.logger import get_logger from ddtrace.internal.rate_limiter import RateLimiter @@ -134,21 +129,30 @@ def __post_init__(self) -> None: def delayed_init(self) -> None: try: if self._rules is not None and not hasattr(self, "_ddwaf"): - from ddtrace.appsec._ddwaf import DDWaf # noqa: E402 + from ddtrace.appsec._ddwaf import waf_module # noqa: E402 import ddtrace.appsec._metrics as metrics # noqa: E402 + DDWaf = waf_module() + if DDWaf is None: + log.warning("DDWaf features disabled. WARNING: Dynamic Library not loaded") + self._ddwaf: Optional[WAF] = None + return self.metrics = metrics self._ddwaf = DDWaf( self._rules, self.obfuscation_parameter_key_regexp, self.obfuscation_parameter_value_regexp, metrics ) - self.metrics._set_waf_init_metric(self._ddwaf.info, self._ddwaf.initialized) + if self._ddwaf: + self.metrics._set_waf_init_metric(self._ddwaf.info, self._ddwaf.initialized) except Exception: # Partial of DDAS-0005-00 log.warning("[DDAS-0005-00] WAF initialization failed", exc_info=True) + self._ddwaf = None self._update_required() def _update_required(self): + if self._ddwaf is None: + return self._addresses_to_keep.clear() for address in self._ddwaf.required_data: self._addresses_to_keep.add(address) @@ -162,6 +166,8 @@ def _update_rules( ) -> bool: if not hasattr(self, "_ddwaf"): self.delayed_init() + if self._ddwaf is None: + return False result = False if asm_config._asm_static_rule_file is not None: return result @@ -195,6 +201,8 @@ def on_span_start(self, span: Span) -> None: if not hasattr(self, "_ddwaf"): self.delayed_init() + if self._ddwaf is None: + return if span.span_type not in asm_config._asm_processed_span_types: return @@ -246,7 +254,7 @@ def waf_callable(custom_data=None, **kwargs): def _waf_action( self, entry_span: Span, - ctx: "ddwaf.ddwaf_types.ddwaf_context_capsule", + ctx: ddwaf_types.ddwaf_context_capsule, custom_data: Optional[dict[str, Any]] = None, crop_trace: Optional[str] = None, rule_type: Optional[str] = None, @@ -263,6 +271,9 @@ def _waf_action( be retrieved from the `core`. This can be used when you don't want to store the value in the `core` before checking the `WAF`. """ + if not hasattr(self, "_ddwaf") or self._ddwaf is None: + return None + if _asm_request_context.get_blocked(): # We still must run the waf if we need to extract schemas for API SECURITY if not custom_data or not custom_data.get("PROCESSOR_SETTINGS", {}).get("extract-schema", False): @@ -310,7 +321,7 @@ def _waf_action( except Exception: log.debug("appsec::processor::waf::run", exc_info=True) waf_results = Binding_error - _asm_request_context.set_waf_info(lambda: self._ddwaf.info) + _asm_request_context.set_waf_info(lambda: self._ddwaf.info) # type: ignore if waf_results.return_code < 0: error_tag = APPSEC.RASP_ERROR if rule_type else APPSEC.WAF_ERROR previous = entry_span.get_tag(error_tag) @@ -390,9 +401,12 @@ def _is_needed(self, address: str) -> bool: return address in self._addresses_to_keep def on_span_finish(self, span: Span) -> None: + ddwaf = getattr(self, "_ddwaf", None) + if ddwaf is None: + return if span.span_type in asm_config._asm_processed_span_types: _asm_request_context.call_waf_callback_no_instrumentation() - self._ddwaf._at_request_end() + ddwaf._at_request_end() _asm_request_context.end_context(span) @classmethod diff --git a/tests/appsec/appsec/api_security/test_api_security_manager.py b/tests/appsec/appsec/api_security/test_api_security_manager.py index 3a011f2f081..9f72bfe2f62 100644 --- a/tests/appsec/appsec/api_security/test_api_security_manager.py +++ b/tests/appsec/appsec/api_security/test_api_security_manager.py @@ -5,6 +5,7 @@ from ddtrace._trace.span import Span from ddtrace.appsec._api_security.api_manager import APIManager +from ddtrace.appsec._asm_request_context import _WAF_CALL from ddtrace.appsec._constants import SPAN_DATA_NAMES from ddtrace.constants import AUTO_KEEP from ddtrace.constants import AUTO_REJECT @@ -44,25 +45,26 @@ def mock_environment(self): env.span.context.sampling_priority = None entry_span.context.sampling_priority = None env.waf_addresses = {} + env.callbacks = {_WAF_CALL: MagicMock()} env.blocked = None return env - def test_schema_callback_no_span(self, api_manager, tracer): + def test_schema_callback_no_span(self, api_manager, mock_environment, tracer): """Test that _schema_callback exits early when environment has no span. Expects that _should_collect_schema is not called. """ - env = MagicMock() + env = mock_environment env.span = None api_manager._schema_callback(env) api_manager._should_collect_schema.assert_not_called() - def test_schema_callback_feature_not_active(self, api_manager): + def test_schema_callback_feature_not_active(self, api_manager, mock_environment): """Test that _schema_callback exits early when API security feature is not active. Expects that _should_collect_schema is not called. """ with override_global_config(values=dict(_api_security_enabled=False)): - env = MagicMock() + env = mock_environment env.span = MagicMock(spec=Span) api_manager._schema_callback(env) @@ -90,7 +92,7 @@ def test_schema_callback_sampling_priority_reject(self, api_manager, mock_enviro api_manager._schema_callback(mock_environment) api_manager._should_collect_schema.assert_called_once() - api_manager._asm_context.call_waf_callback.assert_not_called() + mock_environment.callbacks[_WAF_CALL].assert_not_called() @pytest.mark.parametrize("sampling_priority", [USER_KEEP, AUTO_KEEP]) def test_schema_callback_sampling_priority_keep(self, api_manager, mock_environment, sampling_priority): @@ -102,7 +104,7 @@ def test_schema_callback_sampling_priority_keep(self, api_manager, mock_environm mock_waf_result = MagicMock() mock_waf_result.api_security = {"_dd.appsec.s.req.body": {"type": "object"}} - api_manager._asm_context.call_waf_callback.return_value = mock_waf_result + mock_environment.callbacks[_WAF_CALL].return_value = mock_waf_result mock_environment.waf_addresses = { SPAN_DATA_NAMES.REQUEST_HEADERS_NO_COOKIES: {"Content-Type": "application/json"}, @@ -112,7 +114,7 @@ def test_schema_callback_sampling_priority_keep(self, api_manager, mock_environm api_manager._schema_callback(mock_environment) api_manager._should_collect_schema.assert_called_once() - api_manager._asm_context.call_waf_callback.assert_called_once() + mock_environment.callbacks[_WAF_CALL].assert_called_once() api_manager._metrics._report_api_security.assert_called_with(True, 1) assert len(entry_span._meta) == 1 @@ -136,7 +138,7 @@ def test_schema_callback_apm_tracing_disabled( "_dd.appsec.s.res.headers": {"type": "object"}, "_dd.appsec.s.res.body": {"type": "object", "properties": {"status": {"type": "string"}}}, } - api_manager._asm_context.call_waf_callback.return_value = mock_waf_result + mock_environment.callbacks[_WAF_CALL].return_value = mock_waf_result api_manager._should_collect_schema.return_value = should_collect_return mock_environment.entry_span.context.sampling_priority = sampling_priority @@ -165,7 +167,7 @@ def test_schema_callback_with_valid_waf_addresses(self, api_manager, mock_enviro "_dd.appsec.s.res.headers": {"type": "object"}, "_dd.appsec.s.res.body": {"type": "object", "properties": {"status": {"type": "string"}}}, } - api_manager._asm_context.call_waf_callback.return_value = mock_waf_result + mock_environment.callbacks[_WAF_CALL].return_value = mock_waf_result mock_environment.waf_addresses = { SPAN_DATA_NAMES.REQUEST_HEADERS_NO_COOKIES: {"Content-Type": "application/json"}, @@ -180,10 +182,10 @@ def test_schema_callback_with_valid_waf_addresses(self, api_manager, mock_enviro api_manager._schema_callback(mock_environment) api_manager._should_collect_schema.assert_called_once() - api_manager._asm_context.call_waf_callback.assert_called_once() + mock_environment.callbacks[_WAF_CALL].assert_called_once() # Verify WAF payload includes all addresses - call_args = api_manager._asm_context.call_waf_callback.call_args[0][0] + call_args = mock_environment.callbacks[_WAF_CALL].call_args[0][0] for call_arg in [ "PROCESSOR_SETTINGS", "REQUEST_HEADERS_NO_COOKIES", @@ -219,7 +221,7 @@ def test_schema_callback_oversized_schema(self, api_manager, mock_environment): mock_waf_result = MagicMock() large_schema = {"type": "object", "properties": {f"prop_{i}": {"type": "string"} for i in range(10000)}} mock_waf_result.api_security = {"_dd.appsec.s.req.body": large_schema} - api_manager._asm_context.call_waf_callback.return_value = mock_waf_result + mock_environment.callbacks[_WAF_CALL].return_value = mock_waf_result with patch("gzip.compress") as mock_compress: mock_compress.return_value = b"x" * 100000 # data exceeding MAX_SPAN_META_VALUE_LEN @@ -240,14 +242,14 @@ def test_schema_callback_parse_response_body_disabled(self, api_manager, mock_en with override_global_config(values=dict(_api_security_parse_response_body=False)): mock_waf_result = MagicMock() - api_manager._asm_context.call_waf_callback.return_value = mock_waf_result + mock_environment.callbacks[_WAF_CALL].return_value = mock_waf_result mock_environment.waf_addresses = { SPAN_DATA_NAMES.RESPONSE_BODY: {"status": "success"}, # This should be ignored } api_manager._schema_callback(mock_environment) - call_args = api_manager._asm_context.call_waf_callback.call_args[0][0] + call_args = mock_environment.callbacks[_WAF_CALL].call_args[0][0] assert "RESPONSE_BODY" not in call_args assert len(mock_environment.entry_span._meta) == 0 diff --git a/tests/appsec/appsec/api_security/test_schema_fuzz.py b/tests/appsec/appsec/api_security/test_schema_fuzz.py index 952275d2e60..de4b6afc107 100644 --- a/tests/appsec/appsec/api_security/test_schema_fuzz.py +++ b/tests/appsec/appsec/api_security/test_schema_fuzz.py @@ -10,7 +10,9 @@ def build_schema(obj): with open(constants.DEFAULT.RULES, "br") as f_apisec: rules = f_apisec.read() - waf = ddwaf.DDWaf(rules, b"", b"", _metrics) + waf_module = ddwaf.waf_module() + assert waf_module is not None, "DDWAF module failed to load" + waf = waf_module(rules, b"", b"", _metrics) ctx = waf._at_request_start() if ctx is None: raise RuntimeError("Failed to create WAF context") diff --git a/tests/appsec/appsec/test_processor.py b/tests/appsec/appsec/test_processor.py index 461e0a7ec9d..34133d80302 100644 --- a/tests/appsec/appsec/test_processor.py +++ b/tests/appsec/appsec/test_processor.py @@ -11,7 +11,7 @@ from ddtrace.appsec._constants import DEFAULT from ddtrace.appsec._constants import FINGERPRINTING from ddtrace.appsec._constants import WAF_DATA_NAMES -from ddtrace.appsec._ddwaf import DDWaf +from ddtrace.appsec._ddwaf import waf_module from ddtrace.appsec._ddwaf.ddwaf_types import py_ddwaf_builder_get_config_paths from ddtrace.appsec._processor import AppSecSpanProcessor from ddtrace.appsec._processor import _transform_headers @@ -35,6 +35,7 @@ # handling python 2.X import error JSONDecodeError = ValueError # type: ignore +DDWaf = waf_module() APPSEC_JSON_TAG = f"meta.{APPSEC.JSON}" config_asm = {"_asm_enabled": True} @@ -783,7 +784,7 @@ def test_required_addresses(): "persistent", [key for key, value in WAF_DATA_NAMES if value in WAF_DATA_NAMES.PERSISTENT_ADDRESSES] ) @pytest.mark.parametrize("ephemeral", ["LFI_ADDRESS", "PROCESSOR_SETTINGS"]) -@mock.patch("ddtrace.appsec._ddwaf.DDWaf.run") +@mock.patch("ddtrace.appsec._ddwaf.waf.DDWaf.run") def test_ephemeral_addresses(mock_run, persistent, ephemeral): from ddtrace.appsec._ddwaf.waf_stubs import DDWaf_result from ddtrace.appsec._utils import _observator @@ -796,18 +797,24 @@ def test_ephemeral_addresses(mock_run, persistent, ephemeral): assert processor # first call must send all data to the waf processor._waf_action(span, None, {persistent: {"key_1": "value_1"}, ephemeral: {"key_2": "value_2"}}) + assert mock_run.call_args + assert mock_run.call_args[0] assert mock_run.call_args[0][1] == {WAF_DATA_NAMES[persistent]: {"key_1": "value_1"}} + assert mock_run.call_args[1] assert mock_run.call_args[1]["ephemeral_data"] == {WAF_DATA_NAMES[ephemeral]: {"key_2": "value_2"}} # second call must only send ephemeral data to the waf, not persistent data again processor._waf_action(span, None, {persistent: {"key_1": "value_1"}, ephemeral: {"key_2": "value_3"}}) + assert mock_run.call_args + assert mock_run.call_args[0] assert mock_run.call_args[0][1] == {} + assert mock_run.call_args[1] assert mock_run.call_args[1]["ephemeral_data"] == { WAF_DATA_NAMES[ephemeral]: {"key_2": "value_3"}, } assert (span._local_root or span).get_tag(APPSEC.RC_PRODUCTS) == "[ASM:1] u:1 r:1" -@mock.patch("ddtrace.appsec._ddwaf.DDWaf.run") +@mock.patch("ddtrace.appsec._ddwaf.waf.DDWaf.run") def test_waf_action_null_ephemeral_addresses(mock_run): from ddtrace.appsec._ddwaf.waf_stubs import DDWaf_result from ddtrace.appsec._utils import _observator @@ -820,7 +827,10 @@ def test_waf_action_null_ephemeral_addresses(mock_run): assert processor # None value for ephemeral addresses should not be discarded processor._waf_action(span, None, {"LOGIN_FAILURE": None}) + assert mock_run.call_args + assert mock_run.call_args[0] assert mock_run.call_args[0][1] == {} + assert mock_run.call_args[1] assert mock_run.call_args[1]["ephemeral_data"] == {WAF_DATA_NAMES.LOGIN_FAILURE: None} diff --git a/tests/appsec/appsec/test_telemetry.py b/tests/appsec/appsec/test_telemetry.py index 8bf98a49cdc..e33eb082854 100644 --- a/tests/appsec/appsec/test_telemetry.py +++ b/tests/appsec/appsec/test_telemetry.py @@ -7,8 +7,8 @@ import ddtrace.appsec._asm_request_context as asm_request_context from ddtrace.appsec._constants import APPSEC -from ddtrace.appsec._ddwaf import version import ddtrace.appsec._ddwaf.ddwaf_types +import ddtrace.appsec._ddwaf.waf from ddtrace.appsec._deduplications import deduplication from ddtrace.appsec._processor import AppSecSpanProcessor from ddtrace.appsec._remoteconfiguration import enable_asm @@ -32,6 +32,8 @@ def _assert_generate_metrics(metrics_result, is_rule_triggered=False, is_blocked_request=False, expected_name=[]): + from ddtrace.appsec._ddwaf import version + metric_update = 0 # Since the appsec.enabled metric is emitted on each telemetry worker interval, it can cause random errors in # this function and make the tests flaky. That's why we exclude the "enabled" metric from this assert @@ -50,17 +52,17 @@ def _assert_generate_metrics(metrics_result, is_rule_triggered=False, is_blocked assert f"request_blocked:{str(is_blocked_request).lower()}" in metric["tags"] # assert not any(tag.startswith("request_truncated") for tag in metric.["tags"]) assert "waf_timeout:false" in metric["tags"] - assert f"waf_version:{version()}" in metric["tags"] + assert f"waf_version:{version}" in metric["tags"] assert any("event_rules_version:" in t for t in metric["tags"]) elif metric_name == "waf.init": assert len(metric["points"]) == 1 - assert f"waf_version:{version()}" in metric["tags"] + assert f"waf_version:{version}" in metric["tags"] assert "success:true" in metric["tags"] assert any("event_rules_version" in t for t in metric["tags"]) assert len(metric["tags"]) == 3 elif metric_name == "waf.updates": assert len(metric["points"]) == 1 - assert f"waf_version:{version()}" in metric["tags"] + assert f"waf_version:{version}" in metric["tags"] assert "success:true" in metric["tags"] assert any("event_rules_version" in t for t in metric["tags"]) assert len(metric["tags"]) == 3 @@ -74,6 +76,8 @@ def _assert_generate_metrics(metrics_result, is_rule_triggered=False, is_blocked def _assert_distributions_metrics(metrics_result, is_rule_triggered=False, is_blocked_request=False): + from ddtrace.appsec._ddwaf import version + distributions_metrics = metrics_result[TELEMETRY_EVENT_TYPE.DISTRIBUTIONS][TELEMETRY_NAMESPACE.APPSEC.value] assert len(distributions_metrics) == 2, "Expected 2 distributions_metrics" @@ -83,7 +87,7 @@ def _assert_distributions_metrics(metrics_result, is_rule_triggered=False, is_bl assert isinstance(metric["points"][0], float) assert f"rule_triggered:{str(is_rule_triggered).lower()}" in metric["tags"] assert f"request_blocked:{str(is_blocked_request).lower()}" in metric["tags"] - assert f"waf_version:{version()}" in metric["tags"] + assert f"waf_version:{version}" in metric["tags"] assert any("event_rules_version" in t for t in metric["tags"]) else: pytest.fail("Unexpected distributions_metrics {}".format(metric["metric"])) @@ -162,6 +166,8 @@ def test_metrics_when_appsec_block_custom(telemetry_writer, tracer): def test_log_metric_error_ddwaf_init(telemetry_writer): + from ddtrace.appsec._ddwaf import version + with override_global_config( dict( _asm_enabled=True, @@ -178,7 +184,7 @@ def test_log_metric_error_ddwaf_init(telemetry_writer): list_metrics_logs[0]["message"] == "appsec.waf.error::init::rules::" """{"missing key 'conditions'": ['crs-913-110'], "missing key 'tags'": ['crs-942-100']}""" ) - assert "waf_version:{}".format(version()) in list_metrics_logs[0]["tags"] + assert "waf_version:{}".format(version) in list_metrics_logs[0]["tags"] def test_log_metric_error_ddwaf_timeout(telemetry_writer, tracer): @@ -210,6 +216,8 @@ def test_log_metric_error_ddwaf_timeout(telemetry_writer, tracer): def test_log_metric_error_ddwaf_update(telemetry_writer): + from ddtrace.appsec._ddwaf import version + with override_global_config(dict(_asm_enabled=True, _asm_deduplication_enabled=False)): span_processor = AppSecSpanProcessor() span_processor._update_rules([], invalid_rule_update) @@ -217,7 +225,7 @@ def test_log_metric_error_ddwaf_update(telemetry_writer): list_metrics_logs = list(telemetry_writer._logs) assert len(list_metrics_logs) == 1 assert list_metrics_logs[0]["message"] == invalid_error - assert "waf_version:{}".format(version()) in list_metrics_logs[0]["tags"] + assert "waf_version:{}".format(version) in list_metrics_logs[0]["tags"] unpatched_run = ddtrace.appsec._ddwaf.ddwaf_types.ddwaf_run @@ -231,6 +239,8 @@ def _wrapped_run(*args, **kwargs): @mock.patch.object(ddtrace.appsec._ddwaf.waf, "ddwaf_run", new=_wrapped_run) def test_log_metric_error_ddwaf_internal_error(telemetry_writer): """Test that an internal error is logged when the WAF returns an internal error.""" + from ddtrace.appsec._ddwaf import version + with override_global_config(dict(_asm_enabled=True, _asm_deduplication_enabled=False)): with tracer.trace("test", span_type=SpanTypes.WEB, service="test") as span: span_processor = AppSecSpanProcessor() @@ -246,7 +256,7 @@ def test_log_metric_error_ddwaf_internal_error(telemetry_writer): error_metrics = [m for m in list_telemetry_metrics if m["metric"] == "waf.error"] assert len(error_metrics) == 1, error_metrics assert len(error_metrics[0]["tags"]) == 3 - assert f"waf_version:{version()}" in error_metrics[0]["tags"] + assert f"waf_version:{version}" in error_metrics[0]["tags"] assert "waf_error:-3" in error_metrics[0]["tags"] assert any(tag.startswith("event_rules_version:") for tag in error_metrics[0]["tags"]) diff --git a/tests/appsec/contrib_appsec/utils.py b/tests/appsec/contrib_appsec/utils.py index c759c53ac70..365236a4807 100644 --- a/tests/appsec/contrib_appsec/utils.py +++ b/tests/appsec/contrib_appsec/utils.py @@ -135,7 +135,7 @@ def update_tracer(self, interface): # if interface.tracer._appsec_processor: # interface.printer( # f"""ASM enabled: {asm_config._asm_enabled} - # {ddtrace.appsec._ddwaf.version()} + # {ddtrace.appsec._ddwaf.version} # {interface.tracer._appsec_processor._ddwaf.info} # {asm_config._asm_libddwaf} # """ diff --git a/tests/smoke_test.py b/tests/smoke_test.py index 086341ba2ac..48c41829ce3 100644 --- a/tests/smoke_test.py +++ b/tests/smoke_test.py @@ -68,7 +68,7 @@ def emit(self, record): print("Running WAF module load test...") # Proceed with the WAF module load test - ddtrace.appsec._ddwaf.version() + ddtrace.appsec._ddwaf.waf_module() assert ddtrace.appsec._ddwaf._DDWAF_LOADED assert module.loaded print("WAF module load test completed successfully")