diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 5c8cc270194..17376bc59c1 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -333,6 +333,7 @@ def configure( context_provider: Optional[BaseContextProvider] = None, compute_stats_enabled: Optional[bool] = None, appsec_enabled: Optional[bool] = None, + appsec_enabled_origin: Optional[str] = "", iast_enabled: Optional[bool] = None, apm_tracing_disabled: Optional[bool] = None, trace_processors: Optional[List[TraceProcessor]] = None, @@ -351,6 +352,8 @@ def configure( if appsec_enabled is not None: asm_config._asm_enabled = appsec_enabled + if appsec_enabled_origin: + asm_config._asm_enabled_origin = appsec_enabled_origin # type: ignore[assignment] if iast_enabled is not None: asm_config._iast_enabled = iast_enabled diff --git a/ddtrace/appsec/_constants.py b/ddtrace/appsec/_constants.py index f6073ed0a05..92aa748997a 100644 --- a/ddtrace/appsec/_constants.py +++ b/ddtrace/appsec/_constants.py @@ -52,6 +52,9 @@ class APPSEC(metaclass=Constant_Class): APM_TRACING_ENV: Literal["DD_APM_TRACING_ENABLED"] = "DD_APM_TRACING_ENABLED" RULE_FILE: Literal["DD_APPSEC_RULES"] = "DD_APPSEC_RULES" ENABLED: Literal["_dd.appsec.enabled"] = "_dd.appsec.enabled" + ENABLED_ORIGIN_UNKNOWN: Literal["unknown"] = "unknown" + ENABLED_ORIGIN_RC: Literal["remote_config"] = "remote_config" + ENABLED_ORIGIN_ENV: Literal["env_var"] = "env_var" JSON: Literal["_dd.appsec.json"] = "_dd.appsec.json" STRUCT: Literal["appsec"] = "appsec" EVENT_RULE_VERSION: Literal["_dd.appsec.event_rules.version"] = "_dd.appsec.event_rules.version" diff --git a/ddtrace/appsec/_handlers.py b/ddtrace/appsec/_handlers.py index 547831743f3..b238486e86b 100644 --- a/ddtrace/appsec/_handlers.py +++ b/ddtrace/appsec/_handlers.py @@ -19,8 +19,10 @@ from ddtrace.contrib.internal.trace_utils_base import _set_url_tag from ddtrace.ext import http from ddtrace.internal import core +from ddtrace.internal import telemetry from ddtrace.internal.constants import RESPONSE_HEADERS from ddtrace.internal.logger import get_logger +from ddtrace.internal.telemetry import TELEMETRY_NAMESPACE from ddtrace.internal.utils import http as http_utils from ddtrace.internal.utils.http import parse_form_multipart from ddtrace.settings.asm import config as asm_config @@ -374,7 +376,16 @@ def _on_start_response_blocked(ctx, flask_config, response_headers, status): trace_utils.set_http_meta(ctx["req_span"], flask_config, status_code=status, response_headers=response_headers) +def _on_telemetry_periodic(): + if asm_config._asm_enabled: + telemetry.telemetry_writer.add_gauge_metric( + TELEMETRY_NAMESPACE.APPSEC, "enabled", 2, (("origin", asm_config.asm_enabled_origin),) + ) + + def listen(): + core.on("telemetry.periodic", _on_telemetry_periodic) + core.on("set_http_meta_for_asm", _on_set_http_meta) core.on("flask.request_call_modifier", _on_request_span_modifier, "request_body") diff --git a/ddtrace/appsec/_remoteconfiguration.py b/ddtrace/appsec/_remoteconfiguration.py index bb567bab604..a508299c8d6 100644 --- a/ddtrace/appsec/_remoteconfiguration.py +++ b/ddtrace/appsec/_remoteconfiguration.py @@ -8,6 +8,7 @@ from ddtrace.appsec._capabilities import _asm_feature_is_required from ddtrace.appsec._capabilities import _rc_capabilities +from ddtrace.appsec._constants import APPSEC from ddtrace.appsec._constants import PRODUCTS from ddtrace.internal.logger import get_logger from ddtrace.internal.remoteconfig import Payload @@ -165,7 +166,7 @@ def disable_asm(local_tracer: Tracer): def enable_asm(local_tracer: Tracer): if not asm_config._asm_enabled: - local_tracer.configure(appsec_enabled=True) + local_tracer.configure(appsec_enabled=True, appsec_enabled_origin=APPSEC.ENABLED_ORIGIN_RC) def _preprocess_results_appsec_1click_activation( diff --git a/ddtrace/internal/telemetry/writer.py b/ddtrace/internal/telemetry/writer.py index 387a53c357f..c32df6fdd72 100644 --- a/ddtrace/internal/telemetry/writer.py +++ b/ddtrace/internal/telemetry/writer.py @@ -622,10 +622,17 @@ def _generate_logs_event(self, logs): log.debug("%s request payload", TELEMETRY_TYPE_LOGS) self.add_event({"logs": list(logs)}, TELEMETRY_TYPE_LOGS) + def _dispatch(self): + # moved core here to avoid circular import + from ddtrace.internal import core + + core.dispatch("telemetry.periodic") + def periodic(self, force_flush=False, shutting_down=False): # ensure app_started is called at least once in case traces weren't flushed self._app_started() self._app_product_change() + self._dispatch() namespace_metrics = self._namespace.flush(float(self.interval)) if namespace_metrics: diff --git a/ddtrace/settings/asm.py b/ddtrace/settings/asm.py index 1df56ba7f8b..98843f237af 100644 --- a/ddtrace/settings/asm.py +++ b/ddtrace/settings/asm.py @@ -61,6 +61,7 @@ def build_libddwaf_filename() -> str: class ASMConfig(DDConfig): _asm_enabled = DDConfig.var(bool, APPSEC_ENV, default=False) + _asm_enabled_origin = APPSEC.ENABLED_ORIGIN_UNKNOWN _asm_static_rule_file = DDConfig.var(Optional[str], APPSEC.RULE_FILE, default=None) # prevent empty string if _asm_static_rule_file == "": @@ -261,6 +262,12 @@ def __init__(self): # Is one click available? self._eval_asm_can_be_enabled() + @property + def asm_enabled_origin(self): + if APPSEC_ENV in os.environ: + return APPSEC.ENABLED_ORIGIN_ENV + return self._asm_enabled_origin + def reset(self): """For testing purposes, reset the configuration to its default values given current environment variables.""" self.__init__() diff --git a/tests/appsec/appsec/test_telemetry.py b/tests/appsec/appsec/test_telemetry.py index 41c2e33b388..8ea006ff314 100644 --- a/tests/appsec/appsec/test_telemetry.py +++ b/tests/appsec/appsec/test_telemetry.py @@ -5,10 +5,13 @@ import pytest 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 from ddtrace.appsec._deduplications import deduplication from ddtrace.appsec._processor import AppSecSpanProcessor +from ddtrace.appsec._remoteconfiguration import enable_asm +from ddtrace.constants import APPSEC_ENV from ddtrace.contrib.internal.trace_utils import set_http_meta from ddtrace.ext import SpanTypes from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE @@ -18,6 +21,7 @@ import tests.appsec.rules as rules from tests.appsec.utils import asm_context from tests.appsec.utils import build_payload +from tests.utils import override_env from tests.utils import override_global_config @@ -258,3 +262,40 @@ def test_log_metric_error_ddwaf_update_deduplication_timelapse(telemetry_writer) assert len(list_metrics_logs) == 1 finally: deduplication._time_lapse = old_value + + +@pytest.mark.parametrize( + "environment,appsec_enabled,rc_enabled,expected_result,expected_origin", + ( + ({}, False, False, 0, ""), + ({APPSEC_ENV: "true"}, True, False, 1, APPSEC.ENABLED_ORIGIN_ENV), + ({}, True, False, 1, APPSEC.ENABLED_ORIGIN_UNKNOWN), + ({}, True, True, 1, APPSEC.ENABLED_ORIGIN_UNKNOWN), + ({}, False, True, 1, APPSEC.ENABLED_ORIGIN_RC), + ({APPSEC_ENV: "true"}, False, True, 1, APPSEC.ENABLED_ORIGIN_ENV), + ), +) +def test_appsec_enabled_metric( + environment, appsec_enabled, rc_enabled, expected_result, expected_origin, telemetry_writer, tracer +): + """Test that an internal error is logged when the WAF returns an internal error.""" + with override_env(environment), override_global_config(dict(_asm_enabled=appsec_enabled)): + tracer.configure(appsec_enabled=appsec_enabled) + AppSecSpanProcessor() + if rc_enabled: + enable_asm(tracer) + + telemetry_writer._dispatch() + + metrics_result = telemetry_writer._namespace.flush(0.1) + list_telemetry_metrics = metrics_result.get(TELEMETRY_TYPE_GENERATE_METRICS, {}).get( + TELEMETRY_NAMESPACE.APPSEC.value, {} + ) + metrics = [m for m in list_telemetry_metrics if m["metric"] == "enabled"] + assert len(metrics) == expected_result, metrics + if expected_result > 0: + assert len(metrics[0]["tags"]) == 1 + assert f"origin:{expected_origin}" in metrics[0]["tags"] + + # Restore defaults + tracer.configure(appsec_enabled=appsec_enabled, appsec_enabled_origin=APPSEC.ENABLED_ORIGIN_UNKNOWN)