diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 690af19dfd5..b0fd8796b2f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -47,6 +47,7 @@ tests/utils.py @DataDog/python-guild tests/suitespec.yml @DataDog/python-guild @DataDog/apm-core-python tests/suitespec.py @DataDog/python-guild @DataDog/apm-core-python scripts/bump_ddtrace.py @DataDog/python-guild +tests/smoke_test.py @DataDog/python-guild # Core / Language Platform tests/internal @DataDog/apm-core-python @@ -125,10 +126,9 @@ ddtrace/internal/iast/ @DataDog/asm-python tests/appsec/ @DataDog/asm-python tests/contrib/subprocess @DataDog/asm-python tests/snapshots/tests*appsec*.json @DataDog/asm-python -tests/contrib/*/test*appsec*.py @DataDog/asm-python -tests/contrib/*/test*iast*.py @DataDog/asm-python scripts/iast/* @DataDog/asm-python + # Profiling ddtrace/profiling @DataDog/profiling-python ddtrace/internal/settings/profiling.py @DataDog/profiling-python @@ -240,7 +240,7 @@ tests/contrib/azure_functions @DataDog/serverless tests/contrib/azure_functions_eventhubs @DataDog/serverless @DataDog/apm-serverless tests/contrib/azure_functions_servicebus @DataDog/serverless @DataDog/apm-serverless tests/contrib/azure_servicebus @DataDog/serverless @DataDog/apm-serverless -tests/internal/test_serverless.py @DataDog/apm-core-python @DataDog/apm-serverless +tests/internal/test_serverless.py @DataDog/apm-core-python @DataDog/apm-serverless @DataDog/asm-python tests/snapshots/tests.contrib.aws_lambda.*. @DataDog/apm-serverless tests/snapshots/tests.contrib.azure_eventhubs.* @DataDog/serverless @DataDog/apm-serverless tests/snapshots/tests.contrib.azure_functions.* @DataDog/serverless @DataDog/apm-serverless @@ -251,3 +251,8 @@ tests/snapshots/tests.contrib.azure_servicebus.* @DataDog/serverless # Data Streams Monitoring ddtrace/internal/datastreams @DataDog/data-streams-monitoring tests/datastreams @DataDog/data-streams-monitoring + +# ASM (order matters) +tests/**/*appsec* @DataDog/asm-python +tests/**/*iast* @DataDog/asm-python +tests/tracer/test_propagation.py @DataDog/apm-sdk-capabilities-python @DataDog/asm-python diff --git a/tests/integration/test_integration_snapshots.py b/tests/integration/test_integration_snapshots.py index 2047386cbe2..e5ddc7c2e03 100644 --- a/tests/integration/test_integration_snapshots.py +++ b/tests/integration/test_integration_snapshots.py @@ -46,8 +46,8 @@ def test_flush_spans_before_writer_recreate(): long_running_span = tracer.trace("long_running_operation") writer = tracer._span_aggregator.writer - # Enable appsec to trigger the recreation of the agent writer - tracer.configure(appsec_enabled=True) + # Enable compute stats to trigger the recreation of the agent writer + tracer._recreate(reset_buffer=False) assert tracer._span_aggregator.writer is not writer, "Writer should be recreated" # Finish the long running span after the writer has been recreated long_running_span.finish() diff --git a/tests/internal/remoteconfig/test_remoteconfig_client.py b/tests/internal/remoteconfig/test_remoteconfig_appsec_client.py similarity index 100% rename from tests/internal/remoteconfig/test_remoteconfig_client.py rename to tests/internal/remoteconfig/test_remoteconfig_appsec_client.py diff --git a/tests/internal/remoteconfig/test_remoteconfig_client_e2e.py b/tests/internal/remoteconfig/test_remoteconfig_appsec_client_e2e.py similarity index 100% rename from tests/internal/remoteconfig/test_remoteconfig_client_e2e.py rename to tests/internal/remoteconfig/test_remoteconfig_appsec_client_e2e.py diff --git a/tests/tracer/test_endpoint_config.py b/tests/tracer/test_endpoint_config.py index a2eb6061bd4..18194636d99 100644 --- a/tests/tracer/test_endpoint_config.py +++ b/tests/tracer/test_endpoint_config.py @@ -23,7 +23,7 @@ def mock_getresponse_enabled_after_4_retries(self): response.status = 500 response.reason = "KO" else: - response.read.return_value = b'{"dd_iast_enabled": true}' + response.read.return_value = b'{"dd_product_enabled": true}' response.status = 200 response.reason = "OK" response.fp = BytesIO(response.read.return_value) @@ -34,7 +34,7 @@ def mock_getresponse_enabled_after_4_retries(self): def mock_getresponse_enabled(self): response = mock.Mock(spec=HTTPResponse) - response.read.return_value = b'{"dd_iast_enabled": true}' + response.read.return_value = b'{"dd_product_enabled": true}' response.status = 200 response.reason = "OK" response.chunked = False @@ -46,7 +46,7 @@ def mock_getresponse_enabled(self): def mock_getresponse_403(self): response = mock.Mock(spec=HTTPResponse) - response.read.return_value = b'{"dd_iast_enabled": true}' + response.read.return_value = b'{"dd_product_enabled": true}' response.status = 403 response.reason = "KO" response.chunked = False @@ -58,7 +58,7 @@ def mock_getresponse_403(self): def mock_getresponse_500(self): response = mock.Mock(spec=HTTPResponse) - response.read.return_value = b'{"dd_iast_enabled": true}' + response.read.return_value = b'{"dd_product_enabled": true}' response.status = 500 response.reason = "KO" response.chunked = False @@ -99,7 +99,7 @@ def test_set_config_endpoint_enabled(caplog): ), mock.patch.object( HTTPConnection, "getresponse", new=mock_getresponse_enabled ): - assert fetch_config_from_endpoint() == {"dd_iast_enabled": True} + assert fetch_config_from_endpoint() == {"dd_product_enabled": True} if caplog.text: assert "Configuration endpoint not set. Skipping fetching configuration." not in caplog.text assert "Failed to fetch configuration from endpoint" not in caplog.text @@ -181,4 +181,4 @@ def test_set_config_endpoint_retries(caplog): ), mock.patch( "ddtrace.internal.settings.endpoint_config._get_retries", return_value=5 ): - assert fetch_config_from_endpoint() == {"dd_iast_enabled": True} + assert fetch_config_from_endpoint() == {"dd_product_enabled": True} diff --git a/tests/tracer/test_processors.py b/tests/tracer/test_processors.py index a737da92a47..e6b96dc0f41 100644 --- a/tests/tracer/test_processors.py +++ b/tests/tracer/test_processors.py @@ -178,7 +178,7 @@ def test_aggregator_reset_apm_opt_out_preserves_sampling(): def test_aggregator_reset_with_args(writer_class): """ Validates that the span aggregator can reset trace buffers, sampling processor, - user processors/filters and trace api version (when ASM is enabled) + user processors/filters. """ dd_proc = DummyProcessor() @@ -204,12 +204,12 @@ def test_aggregator_reset_with_args(writer_class): assert aggr.sampling_processor.apm_opt_out is False assert aggr.sampling_processor._compute_stats_enabled is False # Reset the aggregator with new args and new user processors and expect the new values to be set - aggr.reset(user_processors=[], compute_stats=True, apm_opt_out=True, appsec_enabled=True, reset_buffer=False) + aggr.reset(user_processors=[], compute_stats=True, reset_buffer=False) assert aggr.user_processors == [] assert dd_proc in aggr.dd_processors - assert aggr.sampling_processor.apm_opt_out is True + assert aggr.sampling_processor.apm_opt_out is False assert aggr.sampling_processor._compute_stats_enabled is True - assert aggr.writer._api_version == "v0.4" + assert aggr.writer._api_version == "v0.5" assert span.trace_id in aggr._traces assert len(aggr._span_metrics["spans_created"]) == 1 diff --git a/tests/tracer/test_trace_utils.py b/tests/tracer/test_trace_utils.py index b56dc4e3eca..36d900f5b5d 100644 --- a/tests/tracer/test_trace_utils.py +++ b/tests/tracer/test_trace_utils.py @@ -17,9 +17,7 @@ from ddtrace._trace.pin import Pin from ddtrace.contrib.internal import trace_utils from ddtrace.contrib.internal.trace_utils import _get_request_header_client_ip -from ddtrace.ext import SpanTypes from ddtrace.ext import http -from ddtrace.ext import net from ddtrace.internal.compat import ensure_text from ddtrace.internal.settings._config import Config from ddtrace.internal.settings.integration import IntegrationConfig @@ -28,7 +26,6 @@ from ddtrace.propagation.http import HTTPPropagator from ddtrace.trace import Context from ddtrace.trace import Span -from tests.appsec.utils import asm_context from tests.utils import override_global_config @@ -389,128 +386,6 @@ def test_set_http_meta_with_http_header_tags_config(): assert response_span.get_tag("third-header") == "value3" -@pytest.mark.parametrize("appsec_enabled", [False, True]) -@pytest.mark.parametrize("span_type", [SpanTypes.WEB, SpanTypes.HTTP, None]) -@pytest.mark.parametrize( - "method,url,status_code,status_msg,query,request_headers,response_headers,uri,path_params,cookies,target_host", - [ - ("GET", "http://localhost/", 0, None, None, None, None, None, None, None, "localhost"), - ("GET", "http://localhost/", 200, "OK", None, None, None, None, None, None, "localhost"), - (None, None, None, None, None, None, None, None, None, None, None), - ( - "GET", - "http://localhost/", - 200, - "OK", - None, - {"my-header": "value1"}, - {"resp-header": "val"}, - "http://localhost/", - None, - None, - "localhost", - ), - ( - "GET", - "http://localhost/", - 200, - "OK", - "q=test+query&q2=val", - {"my-header": "value1"}, - {"resp-header": "val"}, - "http://localhost/search?q=test+query&q2=val", - {"id": "val", "name": "vlad"}, - None, - "localhost", - ), - ("GET", "http://user:pass@localhost/", 0, None, None, None, None, None, None, None, None), - ("GET", "http://user@localhost/", 0, None, None, None, None, None, None, None, None), - ("GET", "http://user:pass@localhost/api?q=test", 0, None, None, None, None, None, None, None, None), - ("GET", "http://localhost/api@test", 0, None, None, None, None, None, None, None, None), - ("GET", "http://localhost/?api@test", 0, None, None, None, None, None, None, None, None), - ], -) -def test_set_http_meta( - span, - int_config, - method, - url, - target_host, - status_code, - status_msg, - query, - request_headers, - response_headers, - uri, - path_params, - cookies, - appsec_enabled, - span_type, -): - int_config.myint.http.trace_headers(["my-header"]) - int_config.myint.http.trace_query_string = True - span.span_type = span_type - with asm_context(config={"_asm_enabled": appsec_enabled}): - trace_utils.set_http_meta( - span, - int_config.myint, - method=method, - url=url, - target_host=target_host, - status_code=status_code, - status_msg=status_msg, - query=query, - raw_uri=uri, - request_headers=request_headers, - response_headers=response_headers, - request_cookies=cookies, - request_path_params=path_params, - ) - if method is not None: - assert span.get_tag(http.METHOD) == method - else: - assert http.METHOD not in span.get_tags() - - if target_host is not None: - assert span.get_tag(net.TARGET_HOST) == target_host - else: - assert net.TARGET_HOST not in span.get_tags() - - if url is not None: - if url.startswith("http://user"): - # Remove any userinfo that may be in the original url - expected_url = url[: url.index(":")] + "://" + url[url.index("@") + 1 :] - else: - expected_url = url - - if query and int_config.myint.http.trace_query_string: - assert span.get_tag(http.URL) == str(expected_url + "?" + query) - else: - assert span.get_tag(http.URL) == str(expected_url) - else: - assert http.URL not in span.get_tags() - - if status_code is not None: - assert span.get_tag(http.STATUS_CODE) == str(status_code) - if 500 <= int(status_code) < 600: - assert span.error == 1 - else: - assert span.error == 0 - else: - assert http.STATUS_CODE not in span.get_tags() - - if status_msg is not None: - assert span.get_tag(http.STATUS_MSG) == str(status_msg) - - if query is not None and int_config.myint.http.trace_query_string: - assert span.get_tag(http.QUERY_STRING) == query - - if request_headers is not None: - for header, value in request_headers.items(): - tag = "http.request.headers." + header - assert span.get_tag(tag) == value - - @mock.patch("ddtrace.internal.settings._config.log") @pytest.mark.parametrize( "error_codes,status_code,error,log_call", diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py index 2833f1c3778..9b725db20f5 100644 --- a/tests/tracer/test_tracer.py +++ b/tests/tracer/test_tracer.py @@ -31,7 +31,6 @@ from ddtrace.ext import user import ddtrace.internal from ddtrace.internal.compat import PYTHON_VERSION_INFO -from ddtrace.internal.rate_limiter import RateLimiter from ddtrace.internal.serverless import has_aws_lambda_agent_extension from ddtrace.internal.serverless import in_aws_lambda from ddtrace.internal.settings._config import Config @@ -51,7 +50,6 @@ class TracerTestCases(TracerTestCase): @pytest.fixture(autouse=True) def inject_fixtures(self, tracer, caplog): self._caplog = caplog - self._tracer_appsec = tracer def test_tracer_vars(self): span = self.trace("a", service="s", resource="r", span_type="t") @@ -1842,33 +1840,6 @@ def test_top_level(tracer): assert child_span2._is_top_level -@pytest.mark.parametrize("sca_enabled", ["true", "false"]) -@pytest.mark.parametrize("appsec_enabled", [True, False]) -@pytest.mark.parametrize("iast_enabled", [True, False]) -def test_asm_standalone_configuration(sca_enabled, appsec_enabled, iast_enabled): - if not appsec_enabled and not iast_enabled and sca_enabled == "false": - pytest.skip("SCA, AppSec or IAST must be enabled") - - with override_env({"DD_APPSEC_SCA_ENABLED": sca_enabled}): - ddtrace.config._reset() - tracer = DummyTracer() - tracer.configure(appsec_enabled=appsec_enabled, iast_enabled=iast_enabled, apm_tracing_disabled=True) - if sca_enabled == "true": - assert bool(ddtrace.config._sca_enabled) is True - assert tracer.enabled is False - - assert isinstance(tracer._sampler.limiter, RateLimiter) - assert tracer._sampler.limiter.rate_limit == 1 - assert tracer._sampler.limiter.time_window == 60e9 - - assert tracer._span_aggregator.sampling_processor._compute_stats_enabled is False - - # reset tracer values - with override_env({"DD_APPSEC_SCA_ENABLED": "false"}): - ddtrace.config._reset() - tracer.configure(appsec_enabled=False, iast_enabled=False, apm_tracing_disabled=False) - - def test_gc_not_used_on_root_spans(): gc.freeze() diff --git a/tests/tracer/test_tracer_appsec.py b/tests/tracer/test_tracer_appsec.py new file mode 100644 index 00000000000..d99c573a36c --- /dev/null +++ b/tests/tracer/test_tracer_appsec.py @@ -0,0 +1,225 @@ +import pytest + +import ddtrace +from ddtrace._trace.processor import SpanAggregator +from ddtrace._trace.processor import TraceProcessor +from ddtrace.contrib.internal import trace_utils +from ddtrace.ext import SpanTypes +from ddtrace.ext import http +from ddtrace.ext import net +from ddtrace.internal.rate_limiter import RateLimiter +from ddtrace.internal.settings._config import Config +from ddtrace.internal.settings.integration import IntegrationConfig +from ddtrace.internal.writer import AgentWriter +from ddtrace.internal.writer import NativeWriter +from ddtrace.trace import Span +from tests.appsec.utils import asm_context +from tests.utils import DummyTracer +from tests.utils import override_env + + +class DummyProcessor(TraceProcessor): + def process_trace(self, trace): + return trace + + +@pytest.fixture +def int_config(): + c = Config() + c.myint = IntegrationConfig(c, "myint") + return c + + +@pytest.fixture +def span(tracer): + with tracer.trace(name="myint") as span: + yield span + + +@pytest.mark.parametrize("writer_class", (AgentWriter, NativeWriter)) +def test_aggregator_reset_with_args(writer_class): + """ + Validates that the span aggregator can reset trace buffers, sampling processor, + user processors/filters and trace api version (when ASM is enabled) + """ + + dd_proc = DummyProcessor() + user_proc = DummyProcessor() + aggr = SpanAggregator( + partial_flush_enabled=False, + partial_flush_min_spans=1, + dd_processors=[dd_proc], + user_processors=[user_proc], + ) + + aggr.writer = writer_class("http://localhost:8126", api_version="v0.5") + span = Span("span", on_finish=[aggr.on_span_finish]) + aggr.on_span_start(span) + + # Expect SpanAggregator to have the expected processors, api_version and span in _traces + assert dd_proc in aggr.dd_processors + assert user_proc in aggr.user_processors + assert span.trace_id in aggr._traces + assert len(aggr._span_metrics["spans_created"]) == 1 + assert aggr.writer._api_version == "v0.5" + # Expect the default value of apm_opt_out and compute_stats to be False + assert aggr.sampling_processor.apm_opt_out is False + assert aggr.sampling_processor._compute_stats_enabled is False + # Reset the aggregator with new args and new user processors and expect the new values to be set + aggr.reset(user_processors=[], compute_stats=True, apm_opt_out=True, appsec_enabled=True, reset_buffer=False) + assert aggr.user_processors == [] + assert dd_proc in aggr.dd_processors + assert aggr.sampling_processor.apm_opt_out is True + assert aggr.sampling_processor._compute_stats_enabled is True + assert aggr.writer._api_version == "v0.4" + assert span.trace_id in aggr._traces + assert len(aggr._span_metrics["spans_created"]) == 1 + + +@pytest.mark.parametrize("appsec_enabled", [False, True]) +@pytest.mark.parametrize("span_type", [SpanTypes.WEB, SpanTypes.HTTP, None]) +@pytest.mark.parametrize( + "method,url,status_code,status_msg,query,request_headers,response_headers,uri,path_params,cookies,target_host", + [ + ("GET", "http://localhost/", 0, None, None, None, None, None, None, None, "localhost"), + ("GET", "http://localhost/", 200, "OK", None, None, None, None, None, None, "localhost"), + (None, None, None, None, None, None, None, None, None, None, None), + ( + "GET", + "http://localhost/", + 200, + "OK", + None, + {"my-header": "value1"}, + {"resp-header": "val"}, + "http://localhost/", + None, + None, + "localhost", + ), + ( + "GET", + "http://localhost/", + 200, + "OK", + "q=test+query&q2=val", + {"my-header": "value1"}, + {"resp-header": "val"}, + "http://localhost/search?q=test+query&q2=val", + {"id": "val", "name": "vlad"}, + None, + "localhost", + ), + ("GET", "http://user:pass@localhost/", 0, None, None, None, None, None, None, None, None), + ("GET", "http://user@localhost/", 0, None, None, None, None, None, None, None, None), + ("GET", "http://user:pass@localhost/api?q=test", 0, None, None, None, None, None, None, None, None), + ("GET", "http://localhost/api@test", 0, None, None, None, None, None, None, None, None), + ("GET", "http://localhost/?api@test", 0, None, None, None, None, None, None, None, None), + ], +) +def test_set_http_meta( + span, + int_config, + method, + url, + target_host, + status_code, + status_msg, + query, + request_headers, + response_headers, + uri, + path_params, + cookies, + appsec_enabled, + span_type, +): + int_config.myint.http.trace_headers(["my-header"]) + int_config.myint.http.trace_query_string = True + span.span_type = span_type + with asm_context(config={"_asm_enabled": appsec_enabled}): + trace_utils.set_http_meta( + span, + int_config.myint, + method=method, + url=url, + target_host=target_host, + status_code=status_code, + status_msg=status_msg, + query=query, + raw_uri=uri, + request_headers=request_headers, + response_headers=response_headers, + request_cookies=cookies, + request_path_params=path_params, + ) + if method is not None: + assert span.get_tag(http.METHOD) == method + else: + assert http.METHOD not in span.get_tags() + + if target_host is not None: + assert span.get_tag(net.TARGET_HOST) == target_host + else: + assert net.TARGET_HOST not in span.get_tags() + + if url is not None: + if url.startswith("http://user"): + # Remove any userinfo that may be in the original url + expected_url = url[: url.index(":")] + "://" + url[url.index("@") + 1 :] + else: + expected_url = url + + if query and int_config.myint.http.trace_query_string: + assert span.get_tag(http.URL) == str(expected_url + "?" + query) + else: + assert span.get_tag(http.URL) == str(expected_url) + else: + assert http.URL not in span.get_tags() + + if status_code is not None: + assert span.get_tag(http.STATUS_CODE) == str(status_code) + if 500 <= int(status_code) < 600: + assert span.error == 1 + else: + assert span.error == 0 + else: + assert http.STATUS_CODE not in span.get_tags() + + if status_msg is not None: + assert span.get_tag(http.STATUS_MSG) == str(status_msg) + + if query is not None and int_config.myint.http.trace_query_string: + assert span.get_tag(http.QUERY_STRING) == query + + if request_headers is not None: + for header, value in request_headers.items(): + tag = "http.request.headers." + header + assert span.get_tag(tag) == value + + +@pytest.mark.parametrize("sca_enabled", ["true", "false"]) +@pytest.mark.parametrize("appsec_enabled", [True, False]) +@pytest.mark.parametrize("iast_enabled", [True, False]) +def test_asm_standalone_configuration(sca_enabled, appsec_enabled, iast_enabled): + if not appsec_enabled and not iast_enabled and sca_enabled == "false": + pytest.skip("SCA, AppSec or IAST must be enabled") + + with override_env({"DD_APPSEC_SCA_ENABLED": sca_enabled}): + ddtrace.config._reset() + tracer = DummyTracer() + tracer.configure(appsec_enabled=appsec_enabled, iast_enabled=iast_enabled, apm_tracing_disabled=True) + if sca_enabled == "true": + assert bool(ddtrace.config._sca_enabled) is True + assert tracer.enabled is False + + assert isinstance(tracer._sampler.limiter, RateLimiter) + assert tracer._sampler.limiter.rate_limit == 1 + assert tracer._sampler.limiter.time_window == 60e9 + + assert tracer._span_aggregator.sampling_processor._compute_stats_enabled is False + + # reset tracer values + with override_env({"DD_APPSEC_SCA_ENABLED": "false"}): + ddtrace.config._reset() + tracer.configure(appsec_enabled=False, iast_enabled=False, apm_tracing_disabled=False)