diff --git a/.github/workflows/run-end-to-end.yml b/.github/workflows/run-end-to-end.yml index 128b80b2f95..ace3e4302aa 100644 --- a/.github/workflows/run-end-to-end.yml +++ b/.github/workflows/run-end-to-end.yml @@ -183,6 +183,18 @@ jobs: - name: Run TRACE_STATS_COMPUTATION scenario if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"TRACE_STATS_COMPUTATION"') run: ./run.sh TRACE_STATS_COMPUTATION + - name: Run TRACE_STATS_COMPUTATION_OBFUSCATION_DISABLED scenario + if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"TRACE_STATS_COMPUTATION_OBFUSCATION_DISABLED"') + run: ./run.sh TRACE_STATS_COMPUTATION_OBFUSCATION_DISABLED + - name: Run TRACE_STATS_COMPUTATION_FUTURE_OBFUSCATION_VERSION scenario + if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"TRACE_STATS_COMPUTATION_FUTURE_OBFUSCATION_VERSION"') + run: ./run.sh TRACE_STATS_COMPUTATION_FUTURE_OBFUSCATION_VERSION + - name: Run TRACE_STATS_COMPUTATION_MISSING_OBFUSCATION_VERSION scenario + if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"TRACE_STATS_COMPUTATION_MISSING_OBFUSCATION_VERSION"') + run: ./run.sh TRACE_STATS_COMPUTATION_MISSING_OBFUSCATION_VERSION + - name: Run TRACE_STATS_COMPUTATION_OBFUSCATION_VERSION_ZERO scenario + if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"TRACE_STATS_COMPUTATION_OBFUSCATION_VERSION_ZERO"') + run: ./run.sh TRACE_STATS_COMPUTATION_OBFUSCATION_VERSION_ZERO - name: Run IAST_STANDALONE scenario if: steps.build.outcome == 'success' && !cancelled() && contains(inputs.scenarios, '"IAST_STANDALONE"') run: ./run.sh IAST_STANDALONE diff --git a/manifests/cpp.yml b/manifests/cpp.yml index e862f3df202..342e7d3f080 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -305,6 +305,11 @@ manifest: tests/parametric/test_tracer_flare.py::TestTracerFlareV1::test_tracer_flare_content_with_debug: missing_feature # Created by easy win activation script tests/parametric/test_tracer_flare.py::TestTracerFlareV1::test_tracer_flare_with_debug: missing_feature # Created by easy win activation script tests/parametric/test_tracer_flare.py::TestTracerFlareV1::test_tracer_profiling_notracing_flare_content: missing_feature # Created by easy win activation script + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_library_conf.py::Test_ExtractBehavior_Default::test_multiple_tracecontexts: missing_feature (baggage is not implemented, also remove DD_TRACE_PROPAGATION_STYLE_EXTRACT workaround in containers.py) tests/test_library_conf.py::Test_ExtractBehavior_Default::test_single_tracecontext: missing_feature (baggage is not implemented, also remove DD_TRACE_PROPAGATION_STYLE_EXTRACT workaround in containers.py) diff --git a/manifests/cpp_httpd.yml b/manifests/cpp_httpd.yml index 2063df71491..9a153512a35 100644 --- a/manifests/cpp_httpd.yml +++ b/manifests/cpp_httpd.yml @@ -109,6 +109,11 @@ manifest: tests/stats/test_stats.py::Test_Client_Stats::test_is_trace_root: missing_feature # Created by easy win activation script tests/stats/test_stats.py::Test_Client_Stats::test_obfuscation: missing_feature # Created by easy win activation script tests/stats/test_stats.py::Test_Client_Stats::test_top_level_service: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Peer_Tags: missing_feature # Created by easy win activation script tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/stats/test_stats.py::Test_Time_Bucketing::test_client_side_stats: missing_feature # Created by easy win activation script diff --git a/manifests/cpp_nginx.yml b/manifests/cpp_nginx.yml index 64349ae1e92..cecb5a7c6e7 100644 --- a/manifests/cpp_nginx.yml +++ b/manifests/cpp_nginx.yml @@ -359,6 +359,11 @@ manifest: tests/stats/test_stats.py::Test_Client_Stats::test_is_trace_root: missing_feature # Created by easy win activation script tests/stats/test_stats.py::Test_Client_Stats::test_obfuscation: missing_feature # Created by easy win activation script tests/stats/test_stats.py::Test_Client_Stats::test_top_level_service: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Peer_Tags: missing_feature # Created by easy win activation script tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/stats/test_stats.py::Test_Time_Bucketing::test_client_side_stats: missing_feature # Created by easy win activation script diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 22c5339f1cc..851b15f9972 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -1067,6 +1067,11 @@ manifest: - weblog_declaration: uds: '>=3.43.0' poc: '>=3.43.0' + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Peer_Tags: - weblog_declaration: uds: '>=3.43.0' diff --git a/manifests/golang.yml b/manifests/golang.yml index 055e42241da..50a2051d546 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -1290,6 +1290,11 @@ manifest: tests/stats/test_stats.py::Test_Client_Drop_P0s::test_client_drop_p0s_false: v2.6.0 tests/stats/test_stats.py::Test_Client_Stats::test_grpc_status_code: irrelevant (variant has no gRPC endpoint) tests/stats/test_stats.py::Test_Client_Stats::test_top_level_service: missing_feature (Go does not set top-level Service field in stats payload) + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_baggage.py::Test_Baggage_Headers_Api_Datadog: incomplete_test_app (/otel_drop_in_baggage_api_datadog endpoint is not implemented) tests/test_baggage.py::Test_Baggage_Headers_Api_OTel: incomplete_test_app (/otel_drop_in_baggage_api_otel endpoint is not implemented) diff --git a/manifests/java.yml b/manifests/java.yml index 8e154daece3..7a07ba002e7 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -3925,6 +3925,11 @@ manifest: - weblog_declaration: "*": v1.54.0 spring-boot-3-native: missing_feature (rasp endpoint not implemented) + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: - weblog_declaration: "*": v0.0.0 diff --git a/manifests/java_otel.yml b/manifests/java_otel.yml index 53f0e95905d..d060e27ace3 100644 --- a/manifests/java_otel.yml +++ b/manifests/java_otel.yml @@ -60,6 +60,11 @@ manifest: tests/parametric/test_tracer.py::Test_ProcessTags_ServiceName: missing_feature tests/parametric/test_tracer.py::Test_TracerServiceNameSource: irrelevant tests/parametric/test_tracer.py::Test_TracerUniversalServiceTagging::test_tracer_service_name_environment_variable: "missing_feature (FIXME: library test client sets empty string as the service name)" + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_library_conf.py::Test_HeaderTags_DynamicConfig::test_tracing_client_http_header_tags_apm_multiconfig: missing_feature (APM_TRACING_MULTICONFIG is not supported in any language yet) tests/test_library_logs.py::Test_NoExceptions::test_dotnet: irrelevant (only for .NET) diff --git a/manifests/nodejs_otel.yml b/manifests/nodejs_otel.yml index 548e9d850a5..e0b22eb08e8 100644 --- a/manifests/nodejs_otel.yml +++ b/manifests/nodejs_otel.yml @@ -77,6 +77,11 @@ manifest: tests/parametric/test_tracer.py::Test_ProcessTags_ServiceName: missing_feature tests/parametric/test_tracer.py::Test_TracerServiceNameSource: irrelevant tests/parametric/test_tracer.py::Test_TracerUniversalServiceTagging::test_tracer_service_name_environment_variable: "missing_feature (FIXME: library test client sets empty string as the service name)" + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_library_conf.py::Test_HeaderTags_DynamicConfig::test_tracing_client_http_header_tags_apm_multiconfig: missing_feature (APM_TRACING_MULTICONFIG is not supported in any language yet) tests/test_library_logs.py::Test_NoExceptions::test_dotnet: irrelevant (only for .NET) diff --git a/manifests/php.yml b/manifests/php.yml index b713c86a8c2..e86360588cb 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -967,6 +967,11 @@ manifest: tests/stats/test_stats.py::Test_Client_Stats::test_disable: v1.17.0 tests/stats/test_stats.py::Test_Client_Stats::test_grpc_status_code: missing_feature (PHP does not support gRPC) tests/stats/test_stats.py::Test_Client_Stats::test_obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_baggage.py::Test_Baggage_Headers_Basic: incomplete_test_app (/make_distant_call endpoint is not correctly implemented) tests/test_baggage.py::Test_Baggage_Headers_Malformed: incomplete_test_app (/make_distant_call endpoint is not correctly implemented) diff --git a/manifests/python.yml b/manifests/python.yml index 14e69cb1207..187cf3d3594 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -2090,6 +2090,11 @@ manifest: - weblog_declaration: "*": v2.8.0 tests/stats/test_stats.py::Test_Client_Stats::test_top_level_service: missing_feature (Python does not set top-level Service field in stats payload) + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: '>=4.11.0' + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: '>=4.11.0' + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: '>=4.11.0' + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: '>=4.11.0' + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: '>=4.11.0' tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/stats/test_stats.py::Test_Time_Bucketing::test_client_side_stats_bucket_alignment: # Modified by easy win activation script - weblog_declaration: diff --git a/manifests/python_lambda.yml b/manifests/python_lambda.yml index 3dea2a9d4ef..ee4c9622137 100644 --- a/manifests/python_lambda.yml +++ b/manifests/python_lambda.yml @@ -322,6 +322,11 @@ manifest: tests/parametric/test_tracer.py::Test_ProcessTags_ServiceName: missing_feature tests/parametric/test_tracer.py::Test_TracerServiceNameSource: irrelevant tests/parametric/test_tracer.py::Test_TracerUniversalServiceTagging::test_tracer_service_name_environment_variable: "missing_feature (FIXME: library test client sets empty string as the service name)" + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_library_conf.py::Test_HeaderTags_DynamicConfig::test_tracing_client_http_header_tags_apm_multiconfig: missing_feature (APM_TRACING_MULTICONFIG is not supported in any language yet) tests/test_library_logs.py::Test_NoExceptions::test_dotnet: irrelevant (only for .NET) diff --git a/manifests/python_otel.yml b/manifests/python_otel.yml index 21e602d0de3..9a430ae11bf 100644 --- a/manifests/python_otel.yml +++ b/manifests/python_otel.yml @@ -69,6 +69,11 @@ manifest: tests/parametric/test_tracer.py::Test_ProcessTags_ServiceName: missing_feature tests/parametric/test_tracer.py::Test_TracerServiceNameSource: irrelevant tests/parametric/test_tracer.py::Test_TracerUniversalServiceTagging::test_tracer_service_name_environment_variable: "missing_feature (FIXME: library test client sets empty string as the service name)" + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/test_library_conf.py::Test_HeaderTags_DynamicConfig::test_tracing_client_http_header_tags_apm_multiconfig: missing_feature (APM_TRACING_MULTICONFIG is not supported in any language yet) tests/test_library_logs.py::Test_NoExceptions::test_dotnet: irrelevant (only for .NET) diff --git a/manifests/ruby.yml b/manifests/ruby.yml index 9bd32bb085a..8e406c4a7ba 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -2426,6 +2426,11 @@ manifest: sinatra41: missing_feature sinatra22: missing_feature tests/stats/test_stats.py::Test_Client_Stats::test_top_level_service: missing_feature (Ruby does not set top-level Service field in stats payload) + tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_Obfuscation_Version_Zero: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation: missing_feature + tests/stats/test_stats.py::Test_Client_Stats_With_Client_Obfuscation_Disabled: missing_feature tests/stats/test_stats.py::Test_Stats_Service_Source: irrelevant (Only implemented for Java) tests/stats/test_stats.py::Test_Time_Bucketing::test_client_side_stats: # Created by easy win activation script - weblog_declaration: diff --git a/tests/stats/test_stats.py b/tests/stats/test_stats.py index abbefc17756..4580c3972d4 100644 --- a/tests/stats/test_stats.py +++ b/tests/stats/test_stats.py @@ -1,5 +1,6 @@ import contextlib import pytest + from utils import features, interfaces, logger, scenarios, weblog """ @@ -68,20 +69,14 @@ def setup_obfuscation(self): weblog.get(f"/rasp/sqli?user_id={user_id}") def test_obfuscation(self): - stats_count = 0 hits = 0 top_hits = 0 - resource = "SELECT * FROM users WHERE id = ?" - # wait for 10 seconds to be sure all the buckets are flushed (better than be flaky) - for s in interfaces.agent.get_stats(resource): - stats_count += 1 + for s in interfaces.agent.get_stats(): + if s["Type"] != "sql" or "?" not in s["Resource"]: + continue logger.debug(f"asserting on {s}") hits += s["Hits"] top_hits += s["TopLevelHits"] - assert s["Type"] == "sql", "expect 'sql' type" - assert stats_count <= 4, ( - "expect <= 4 stats" - ) # Normally this is exactly 2 but in certain high load this can flake and result in additional payloads where hits are split across two payloads assert hits == top_hits >= 4, "expect at least 4 'OK' hits and top level hits across all payloads" def test_is_trace_root(self): @@ -134,6 +129,237 @@ def test_grpc_status_code(self): ) +@features.client_side_stats_supported +@scenarios.trace_stats_computation +class Test_Client_Stats_With_Client_Obfuscation: + """Test client-side stats do the obfuscation before-hand when available""" + + def setup_obfuscation(self): + """Setup for obfuscation test - generates SQL spans for obfuscation testing""" + test_user_ids = ["1", "2", "admin", "test"] + for user_id in test_user_ids: + weblog.get(f"/rasp/sqli?user_id={user_id}") + + def test_obfuscation(self): + """Test that SQL resources are obfuscated before stats aggregation. + + Validates: + - Datadog-Obfuscation-Version header is present on stats payloads + - SQL resource names are obfuscated (literals replaced with ?) + - All 4 distinct queries are aggregated into a single obfuscated resource bucket + """ + sql_stats = [] + obfuscation_header_found = False + + for data in interfaces.library.get_data("/v0.6/stats"): + headers = {h[0].lower(): h[1] for h in data["request"]["headers"]} + if "datadog-obfuscation-version" in headers: + obfuscation_header_found = True + assert int(headers["datadog-obfuscation-version"]) >= 1, ( + f"Expected obfuscation version to be >= 1, got '{headers['datadog-obfuscation-version']}'" + ) + + payload = data["request"]["content"] + for bucket in payload.get("Stats", []): + for stat in bucket.get("Stats", []): + if stat.get("Type") == "sql" and stat["Resource"].startswith("SELECT"): + sql_stats.append(stat) + + assert obfuscation_header_found, "Datadog-Obfuscation-Version header not found on any stats payload" + + assert len(sql_stats) >= 1, "Expected at least one SQL stats entry" + for stat in sql_stats: + assert "?" in stat["Resource"], f"Expected obfuscated resource (containing '?'), got '{stat['Resource']}'" + + +@features.client_side_stats_supported +@scenarios.trace_stats_computation_obfuscation_disabled +class Test_Client_Stats_With_Client_Obfuscation_Disabled: + """Test that libraries read the agent /info to respect the obfuscation config""" + + TEST_USER_IDS = ["1", "2", "admin", "test"] + + def setup_obfuscation(self): + """Setup for obfuscation test - generates SQL spans for obfuscation testing""" + for user_id in self.TEST_USER_IDS: + weblog.get(f"/rasp/sqli?user_id={user_id}") + + def test_obfuscation(self): + """Test that SQL resources are obfuscated before stats aggregation. + + Validates: + - Datadog-Obfuscation-Version header is present on stats payloads + - SQL resource names are not obfuscated, only normalized + """ + sql_stats = [] + obfuscation_header_found = False + + for data in interfaces.library.get_data("/v0.6/stats"): + headers = {h[0].lower(): h[1] for h in data["request"]["headers"]} + if "datadog-obfuscation-version" in headers: + obfuscation_header_found = True + assert int(headers["datadog-obfuscation-version"]) >= 1, ( + f"Expected obfuscation version to be >= 1, got '{headers['datadog-obfuscation-version']}'" + ) + + payload = data["request"]["content"] + for bucket in payload.get("Stats", []): + for stat in bucket.get("Stats", []): + if stat.get("Type") == "sql" and stat["Resource"].startswith("SELECT"): + sql_stats.append(stat) + + assert obfuscation_header_found, "Datadog-Obfuscation-Version header not found on any stats payload" + + unique_resources = {stat["Resource"] for stat in sql_stats} + assert len(unique_resources) >= 4, ( + "Expected at least 4 distinct SQL stats entries, because obfuscation was not applied client-side" + ) + + +@features.client_side_stats_supported +@scenarios.trace_stats_computation_future_obfuscation_version +class Test_Client_Stats_Future_Obfuscation_Version: + """Test that the SDK skips client-side obfuscation when the agent advertises a future/unknown obfuscation version""" + + def setup_no_obfuscation(self): + """Setup for future obfuscation version test - generates SQL spans""" + test_user_ids = ["1", "2", "admin", "test"] + for user_id in test_user_ids: + weblog.get(f"/rasp/sqli?user_id={user_id}") + + def test_no_obfuscation(self): + """Test that the SDK does not obfuscate stats and does not send the obfuscation header + when the agent reports an obfuscation_version higher than what the SDK supports (99). + + Validates: + - Datadog-Obfuscation-Version header is NOT present on any stats payload + - SQL resource names are NOT obfuscated (raw literals still present) + """ + sql_stats = [] + obfuscation_header_found = False + + for data in interfaces.library.get_data("/v0.6/stats"): + headers = {h[0].lower(): h[1] for h in data["request"]["headers"]} + if "datadog-obfuscation-version" in headers: + obfuscation_header_found = True + + payload = data["request"]["content"] + for bucket in payload.get("Stats", []): + for stat in bucket.get("Stats", []): + if stat.get("Type") == "sql" and "WHERE" in stat["Resource"]: + sql_stats.append(stat) + + assert not obfuscation_header_found, ( + "Datadog-Obfuscation-Version header should NOT be present when agent reports a future obfuscation version" + ) + + unique_resources = {stat["Resource"] for stat in sql_stats} + assert len(unique_resources) >= 4, ( + "Expected at least 4 distinct SQL stats entries because obfuscation was not applied client-side" + ) + for stat in sql_stats: + assert "?" not in stat["Resource"], ( + f"SQL resource should NOT be obfuscated when agent reports a future obfuscation version, " + f"but got: '{stat['Resource']}'" + ) + + +@features.client_side_stats_supported +@scenarios.trace_stats_computation_missing_obfuscation_version +class Test_Client_Stats_Missing_Obfuscation_Version: + """Test that the SDK skips client-side obfuscation when the agent does not advertise obfuscation_version""" + + def setup_no_obfuscation(self): + """Setup for missing obfuscation version test - generates SQL spans""" + test_user_ids = ["1", "2", "admin", "test"] + for user_id in test_user_ids: + weblog.get(f"/rasp/sqli?user_id={user_id}") + + def test_no_obfuscation(self): + """Test that the SDK does not obfuscate stats and does not send the obfuscation header + when the agent does not advertise obfuscation_version in /info. + + Validates: + - Datadog-Obfuscation-Version header is NOT present on any stats payload + - SQL resource names are NOT obfuscated (raw literals still present) + """ + sql_stats = [] + obfuscation_header_found = False + + for data in interfaces.library.get_data("/v0.6/stats"): + headers = {h[0].lower(): h[1] for h in data["request"]["headers"]} + if "datadog-obfuscation-version" in headers: + obfuscation_header_found = True + + payload = data["request"]["content"] + for bucket in payload.get("Stats", []): + for stat in bucket.get("Stats", []): + if stat.get("Type") == "sql" and "WHERE" in stat["Resource"]: + sql_stats.append(stat) + + assert not obfuscation_header_found, ( + "Datadog-Obfuscation-Version header should NOT be present when agent does not advertise obfuscation_version" + ) + + unique_resources = {stat["Resource"] for stat in sql_stats} + assert len(unique_resources) >= 4, ( + "Expected at least 4 distinct SQL stats entries because obfuscation was not applied client-side" + ) + for stat in sql_stats: + assert "?" not in stat["Resource"], ( + f"SQL resource should NOT be obfuscated when agent does not advertise obfuscation_version, " + f"but got: '{stat['Resource']}'" + ) + + +@features.client_side_stats_supported +@scenarios.trace_stats_computation_obfuscation_version_zero +class Test_Client_Stats_Obfuscation_Version_Zero: + """Test that the SDK skips client-side obfuscation when the agent advertises obfuscation_version=0""" + + def setup_no_obfuscation(self): + """Setup for obfuscation version zero test - generates SQL spans""" + test_user_ids = ["1", "2", "admin", "test"] + for user_id in test_user_ids: + weblog.get(f"/rasp/sqli?user_id={user_id}") + + def test_no_obfuscation(self): + """Test that the SDK does not obfuscate stats and does not send the obfuscation header + when the agent advertises obfuscation_version=0. + + Validates: + - Datadog-Obfuscation-Version header is NOT present on any stats payload + - SQL resource names are NOT obfuscated (raw literals still present) + """ + sql_stats = [] + obfuscation_header_found = False + + for data in interfaces.library.get_data("/v0.6/stats"): + headers = {h[0].lower(): h[1] for h in data["request"]["headers"]} + if "datadog-obfuscation-version" in headers: + obfuscation_header_found = True + + payload = data["request"]["content"] + for bucket in payload.get("Stats", []): + for stat in bucket.get("Stats", []): + if stat.get("Type") == "sql" and "WHERE" in stat["Resource"]: + sql_stats.append(stat) + + assert not obfuscation_header_found, ( + "Datadog-Obfuscation-Version header should NOT be present when agent advertises obfuscation_version=0" + ) + + unique_resources = {stat["Resource"] for stat in sql_stats} + assert len(unique_resources) >= 4, ( + "Expected at least 4 distinct SQL stats entries because obfuscation was not applied client-side" + ) + for stat in sql_stats: + assert "?" not in stat["Resource"], ( + f"SQL resource should NOT be obfuscated when agent advertises obfuscation_version=0, " + f"but got: '{stat['Resource']}'" + ) + + @features.service_override_source @scenarios.trace_stats_computation class Test_Stats_Service_Source: diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index 4ec12176bae..2a0883d099a 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -100,6 +100,7 @@ class _Scenarios: "DD_TRACE_COMPUTE_STATS": "true", "DD_TRACE_FEATURES": "discovery", "DD_TRACE_TRACER_METRICS_ENABLED": "true", # java + "_DD_TRACE_STATS_COMPUTATION_EXPERIMENTAL_CLIENT_OBFUSCATION_ENABLED": "true", }, doc=( "End to end testing with DD_TRACE_COMPUTE_STATS=1. This feature compute stats at tracer level, and" @@ -117,6 +118,7 @@ class _Scenarios: "DD_TRACE_COMPUTE_STATS": "true", "DD_TRACE_FEATURES": "discovery", "DD_TRACE_TRACER_METRICS_ENABLED": "true", # java + "_DD_TRACE_STATS_COMPUTATION_EXPERIMENTAL_CLIENT_OBFUSCATION_ENABLED": "true", }, client_drop_p0s=False, doc=( @@ -126,6 +128,85 @@ class _Scenarios: scenario_groups=[scenario_groups.appsec], ) + trace_stats_computation_future_obfuscation_version = EndToEndScenario( + name="TRACE_STATS_COMPUTATION_FUTURE_OBFUSCATION_VERSION", + # Same as trace_stats_computation but with the agent advertising an obfuscation_version + # higher than what any current SDK supports (99), to test that the SDK correctly falls + # back to no client-side obfuscation when it encounters an unknown/future version. + weblog_env={ + "DD_TRACE_STATS_COMPUTATION_ENABLED": "true", # default env var for CSS + "DD_TRACE_COMPUTE_STATS": "true", + "DD_TRACE_FEATURES": "discovery", + "DD_TRACE_TRACER_METRICS_ENABLED": "true", # java + "_DD_TRACE_STATS_COMPUTATION_EXPERIMENTAL_CLIENT_OBFUSCATION_ENABLED": "true", + }, + obfuscation_version=99, + doc=( + "End to end testing with DD_TRACE_COMPUTE_STATS=1 and agent reporting obfuscation_version: 99. " + "Tests that tracers correctly skip client-side obfuscation and omit the Datadog-Obfuscation-Version " + "header when the agent advertises an obfuscation version higher than what the SDK supports." + ), + scenario_groups=[scenario_groups.appsec], + ) + + trace_stats_computation_missing_obfuscation_version = EndToEndScenario( + name="TRACE_STATS_COMPUTATION_MISSING_OBFUSCATION_VERSION", + # Same as trace_stats_computation but with the agent not advertising obfuscation_version + # in /info, to test that the SDK correctly falls back to no client-side obfuscation. + weblog_env={ + "DD_TRACE_STATS_COMPUTATION_ENABLED": "true", # default env var for CSS + "DD_TRACE_COMPUTE_STATS": "true", + "DD_TRACE_FEATURES": "discovery", + "DD_TRACE_TRACER_METRICS_ENABLED": "true", # java + "_DD_TRACE_STATS_COMPUTATION_EXPERIMENTAL_CLIENT_OBFUSCATION_ENABLED": "true", + }, + obfuscation_version="MISSING", + doc=( + "End to end testing with DD_TRACE_COMPUTE_STATS=1 and agent not advertising obfuscation_version. " + "Tests that tracers correctly skip client-side obfuscation and omit the Datadog-Obfuscation-Version " + "header when the agent does not advertise any obfuscation version." + ), + scenario_groups=[scenario_groups.appsec], + ) + + trace_stats_computation_obfuscation_version_zero = EndToEndScenario( + name="TRACE_STATS_COMPUTATION_OBFUSCATION_VERSION_ZERO", + # Same as trace_stats_computation but with the agent advertising obfuscation_version=0, + # to test that the SDK treats version 0 as "not supported" and skips client-side obfuscation. + weblog_env={ + "DD_TRACE_STATS_COMPUTATION_ENABLED": "true", # default env var for CSS + "DD_TRACE_COMPUTE_STATS": "true", + "DD_TRACE_FEATURES": "discovery", + "DD_TRACE_TRACER_METRICS_ENABLED": "true", # java + "_DD_TRACE_STATS_COMPUTATION_EXPERIMENTAL_CLIENT_OBFUSCATION_ENABLED": "true", + }, + obfuscation_version=0, + doc=( + "End to end testing with DD_TRACE_COMPUTE_STATS=1 and agent reporting obfuscation_version: 0. " + "Tests that tracers correctly skip client-side obfuscation and omit the Datadog-Obfuscation-Version " + "header when the agent advertises obfuscation_version=0." + ), + scenario_groups=[scenario_groups.appsec], + ) + + trace_stats_computation_obfuscation_disabled = EndToEndScenario( + name="TRACE_STATS_COMPUTATION_OBFUSCATION_DISABLED", + # Same as trace_stats_computation but with the agent being configured with obfuscation disabled, to test that + # the SDK correctly reads the obfuscation config from agent's /info and respects it. + weblog_env={ + "DD_TRACE_STATS_COMPUTATION_ENABLED": "true", # default env var for CSS + "DD_TRACE_COMPUTE_STATS": "true", + "DD_TRACE_FEATURES": "discovery", + "DD_TRACE_TRACER_METRICS_ENABLED": "true", # java + "_DD_TRACE_STATS_COMPUTATION_EXPERIMENTAL_CLIENT_OBFUSCATION_ENABLED": "true", + }, + agent_env={ + "DD_APM_SQL_OBFUSCATION_MODE": "normalize_only", + }, + doc=("End to end testing with DD_TRACE_COMPUTE_STATS=1 and obfuscation disabled."), + scenario_groups=[scenario_groups.appsec], + ) + sampling = EndToEndScenario( "SAMPLING", tracer_sampling_rate=0.5, diff --git a/utils/_context/_scenarios/endtoend.py b/utils/_context/_scenarios/endtoend.py index b9f8cd4c677..718c4589541 100644 --- a/utils/_context/_scenarios/endtoend.py +++ b/utils/_context/_scenarios/endtoend.py @@ -1,3 +1,4 @@ +from typing import Literal import os import pytest @@ -46,6 +47,7 @@ def __init__( meta_structs_disabled: bool = False, span_events: bool = True, client_drop_p0s: bool | None = None, + obfuscation_version: int | None | Literal["MISSING"] = None, extra_containers: tuple[type[TestedContainer], ...] = (), ) -> None: super().__init__(name, doc=doc, github_workflow=github_workflow, scenario_groups=scenario_groups) @@ -57,6 +59,7 @@ def __init__( self.meta_structs_disabled = False self.span_events = span_events self.client_drop_p0s = client_drop_p0s + self.obfuscation_version = obfuscation_version if not self.use_proxy and self.rc_api_enabled: raise ValueError("rc_api_enabled requires use_proxy") @@ -74,6 +77,7 @@ def __init__( meta_structs_disabled=meta_structs_disabled, span_events=span_events, client_drop_p0s=client_drop_p0s, + obfuscation_version=obfuscation_version, enable_ipv6=enable_ipv6, mocked_backend=mocked_backend, ) @@ -201,6 +205,7 @@ def __init__( meta_structs_disabled: bool = False, span_events: bool = True, client_drop_p0s: bool | None = None, + obfuscation_version: int | None | Literal["MISSING"] = None, runtime_metrics_enabled: bool = False, backend_interface_timeout: int = 0, include_buddies: bool = False, @@ -226,6 +231,7 @@ def __init__( meta_structs_disabled=meta_structs_disabled, span_events=span_events, client_drop_p0s=client_drop_p0s, + obfuscation_version=obfuscation_version, ) self._use_proxy_for_agent = use_proxy_for_agent diff --git a/utils/_context/containers.py b/utils/_context/containers.py index 891da7f9b2d..a1501cdb374 100644 --- a/utils/_context/containers.py +++ b/utils/_context/containers.py @@ -4,7 +4,7 @@ import stat import sys import json -from typing import cast +from typing import cast, Literal from http import HTTPStatus from pathlib import Path import time @@ -28,6 +28,7 @@ MockedBackendResponse, SetSpanEventFlags, SetClientDropP0s, + SetObfuscationVersion, AddRemoteConfigEndpoint, StaticJsonMockedTracerResponse, ) @@ -589,6 +590,7 @@ def __init__( meta_structs_disabled: bool, span_events: bool, client_drop_p0s: bool | None = None, + obfuscation_version: int | None | Literal["MISSING"] = None, enable_ipv6: bool, mocked_backend: bool = True, ) -> None: @@ -635,6 +637,9 @@ def __init__( if client_drop_p0s is not None: self.internal_mocked_tracer_responses.append(SetClientDropP0s(client_drop_p0s=client_drop_p0s)) + if obfuscation_version is not None: + self.internal_mocked_tracer_responses.append(SetObfuscationVersion(obfuscation_version=obfuscation_version)) + if rc_api_enabled: # add the remote config endpoint on available agent endpoints self.internal_mocked_tracer_responses.append(AddRemoteConfigEndpoint()) diff --git a/utils/proxy/mocked_response.py b/utils/proxy/mocked_response.py index 1a8b83407de..0ada25dd257 100644 --- a/utils/proxy/mocked_response.py +++ b/utils/proxy/mocked_response.py @@ -7,7 +7,7 @@ import json import os import re -from typing import Self +from typing import Self, Literal import requests @@ -350,6 +350,34 @@ def to_json(self) -> dict: } +class SetObfuscationVersion(_InternalMockedTracerResponse): + """Override the obfuscation_version field in the agent's /info response. + + This controls which obfuscation version the agent advertises. When set to a version + higher than what the SDK supports, the SDK should skip client-side obfuscation and + omit the Datadog-Obfuscation-Version header from stats payloads. + """ + + def __init__(self, *, obfuscation_version: int | Literal["MISSING"]): + super().__init__(path="/info") + self.obfuscation_version = obfuscation_version + + def execute(self, flow: HTTPFlow) -> None: + if flow.response.status_code == HTTPStatus.OK: + c = json.loads(flow.response.content) + if self.obfuscation_version == "MISSING": + del c["obfuscation_version"] + else: + c["obfuscation_version"] = self.obfuscation_version + flow.response.content = json.dumps(c).encode() + + def to_json(self) -> dict: + return { + "type": self.__class__.__name__, + "obfuscation_version": self.obfuscation_version, + } + + class MockedBackendResponse(MockedResponse): """Base class for mocking responses from backend to agent.