From 63d1b6dc7d236bee52af19092c76ad5eaaed757a Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Fri, 17 Jan 2025 13:07:12 -0500 Subject: [PATCH 01/17] [dotnet] parametric app improvements (#3723) --- .../dotnet/parametric/Endpoints/ApmTestApi.cs | 368 ++++++++++-------- .../parametric/Endpoints/ApmTestApiOtel.cs | 17 +- .../build/docker/dotnet/parametric/Program.cs | 15 +- .../parametric/appsettings.Development.json | 4 +- .../docker/dotnet/parametric/appsettings.json | 4 +- 5 files changed, 218 insertions(+), 190 deletions(-) diff --git a/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApi.cs b/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApi.cs index 67e0e9d8b55..8c0abc37416 100644 --- a/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApi.cs +++ b/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApi.cs @@ -1,12 +1,13 @@ using Datadog.Trace; using System.Reflection; -using Newtonsoft.Json; +using System.Runtime.CompilerServices; +using System.Text.Json; namespace ApmTestApi.Endpoints; public abstract class ApmTestApi { - public static void MapApmTraceEndpoints(WebApplication app, ILogger logger) + public static void MapApmTraceEndpoints(WebApplication app, ILogger logger) { _logger = logger; // TODO: Remove when the Tracer sets the correct results in the SpanContextPropagator.Instance getter @@ -17,10 +18,10 @@ public static void MapApmTraceEndpoints(WebApplication app, ILogger app.MapGet("/trace/crash", Crash); app.MapGet("/trace/config", GetTracerConfig); app.MapPost("/trace/tracer/stop", StopTracer); + app.MapPost("/trace/span/start", StartSpan); app.MapPost("/trace/span/inject_headers", InjectHeaders); app.MapPost("/trace/span/extract_headers", ExtractHeaders); - app.MapPost("/trace/span/error", SpanSetError); app.MapPost("/trace/span/set_meta", SpanSetMeta); app.MapPost("/trace/span/set_metric", SpanSetMetric); @@ -28,123 +29,95 @@ public static void MapApmTraceEndpoints(WebApplication app, ILogger app.MapPost("/trace/span/flush", FlushSpans); } + private const BindingFlags CommonBindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; private static readonly Assembly DatadogTraceAssembly = Assembly.Load("Datadog.Trace"); - private const BindingFlags Instance = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; - // Core types - private static readonly Type TracerType = DatadogTraceAssembly.GetType("Datadog.Trace.Tracer", throwOnError: true)!; - private static readonly Type TracerManagerType = DatadogTraceAssembly.GetType("Datadog.Trace.TracerManager", throwOnError: true)!; - private static readonly Type GlobalSettingsType = DatadogTraceAssembly.GetType("Datadog.Trace.Configuration.GlobalSettings", throwOnError: true)!; + private static Type GetType(string name) => DatadogTraceAssembly.GetType(name, throwOnError: true)!; + + // reflected types + private static readonly Type TracerType = GetType("Datadog.Trace.Tracer"); + private static readonly Type TracerManagerType = GetType("Datadog.Trace.TracerManager"); + private static readonly Type GlobalSettingsType = GetType("Datadog.Trace.Configuration.GlobalSettings"); + private static readonly Type AgentWriterType = GetType("Datadog.Trace.Agent.AgentWriter"); + private static readonly Type StatsAggregatorType = GetType("Datadog.Trace.Agent.StatsAggregator"); // ImmutableTracerSettings was removed in 3.7.0 private static readonly Type TracerSettingsType = DatadogTraceAssembly.GetName().Version <= new Version(3, 6, 1, 0) ? - DatadogTraceAssembly.GetType("Datadog.Trace.Configuration.ImmutableTracerSettings", throwOnError: true)! : - DatadogTraceAssembly.GetType("Datadog.Trace.Configuration.TracerSettings", throwOnError: true)!; - - // Agent-related types - private static readonly Type AgentWriterType = DatadogTraceAssembly.GetType("Datadog.Trace.Agent.AgentWriter", throwOnError: true)!; - private static readonly Type StatsAggregatorType = DatadogTraceAssembly.GetType("Datadog.Trace.Agent.StatsAggregator", throwOnError: true)!; - - // Accessors for internal properties/fields accessors - private static readonly PropertyInfo GetGlobalSettingsInstance = GlobalSettingsType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic)!; - private static readonly PropertyInfo GetTracerManager = TracerType.GetProperty("TracerManager", BindingFlags.Instance | BindingFlags.NonPublic)!; - private static readonly MethodInfo GetAgentWriter = TracerManagerType.GetProperty("AgentWriter", BindingFlags.Instance | BindingFlags.Public)!.GetGetMethod()!; - private static readonly FieldInfo GetStatsAggregator = AgentWriterType.GetField("_statsAggregator", BindingFlags.Instance | BindingFlags.NonPublic)!; - - private static readonly PropertyInfo PropagationStyleInject = TracerSettingsType.GetProperty("PropagationStyleInject", Instance)!; - private static readonly PropertyInfo RuntimeMetricsEnabled = TracerSettingsType.GetProperty("RuntimeMetricsEnabled", Instance)!; - private static readonly PropertyInfo IsActivityListenerEnabled = TracerSettingsType.GetProperty("IsActivityListenerEnabled", Instance)!; - private static readonly PropertyInfo GetTracerInstance = TracerType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)!; - private static readonly PropertyInfo GetTracerSettings = TracerType.GetProperty("Settings", Instance)!; - private static readonly PropertyInfo GetDebugEnabled = GlobalSettingsType.GetProperty("DebugEnabled", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!; - - // StatsAggregator flush methods - private static readonly MethodInfo StatsAggregatorDisposeAsync = StatsAggregatorType.GetMethod("DisposeAsync", BindingFlags.Instance | BindingFlags.Public)!; - private static readonly MethodInfo StatsAggregatorFlush = StatsAggregatorType.GetMethod("Flush", BindingFlags.Instance | BindingFlags.NonPublic)!; - + GetType("Datadog.Trace.Configuration.ImmutableTracerSettings") : + GetType("Datadog.Trace.Configuration.TracerSettings"); + + // reflected members + private static readonly PropertyInfo GetGlobalSettingsInstance = GlobalSettingsType.GetProperty("Instance", CommonBindingFlags)!; + private static readonly PropertyInfo GetTracerManager = TracerType.GetProperty("TracerManager", CommonBindingFlags)!; + private static readonly PropertyInfo GetAgentWriter = TracerManagerType.GetProperty("AgentWriter", CommonBindingFlags)!; + private static readonly FieldInfo GetStatsAggregator = AgentWriterType.GetField("_statsAggregator", CommonBindingFlags)!; + private static readonly PropertyInfo PropagationStyleInject = TracerSettingsType.GetProperty("PropagationStyleInject", CommonBindingFlags)!; + private static readonly PropertyInfo RuntimeMetricsEnabled = TracerSettingsType.GetProperty("RuntimeMetricsEnabled", CommonBindingFlags)!; + private static readonly PropertyInfo IsActivityListenerEnabled = TracerSettingsType.GetProperty("IsActivityListenerEnabled", CommonBindingFlags)!; + private static readonly PropertyInfo GetTracerInstance = TracerType.GetProperty("Instance", CommonBindingFlags)!; + private static readonly PropertyInfo GetTracerSettings = TracerType.GetProperty("Settings", CommonBindingFlags)!; + private static readonly PropertyInfo GetDebugEnabled = GlobalSettingsType.GetProperty("DebugEnabled", CommonBindingFlags)!; + private static readonly MethodInfo StatsAggregatorDisposeAsync = StatsAggregatorType.GetMethod("DisposeAsync", CommonBindingFlags)!; + private static readonly MethodInfo StatsAggregatorFlush = StatsAggregatorType.GetMethod("Flush", CommonBindingFlags)!; + + // static state private static readonly Dictionary Spans = new(); - private static readonly Dictionary DDContexts = new(); - - private static readonly SpanContextInjector _spanContextInjector = new(); - private static readonly SpanContextExtractor _spanContextExtractor = new(); + private static readonly Dictionary SpanContexts = new(); + private static ILogger? _logger; - internal static ILogger? _logger; - - private static IEnumerable GetHeaderValues(string[][] headersList, string key) - { - var values = new List(); - - foreach (var kvp in headersList) - { - if (kvp.Length == 2 && string.Equals(key, kvp[0], StringComparison.OrdinalIgnoreCase)) - { - values.Add(kvp[1]); - } - } + // stateless singletons + private static readonly SpanContextInjector SpanContextInjector = new(); + private static readonly SpanContextExtractor SpanContextExtractor = new(); - return values.AsReadOnly(); - } - private static async Task StopTracer() + private static async Task StopTracer() { await Tracer.Instance.ForceFlushAsync(); + return Result(); } private static async Task StartSpan(HttpRequest request) { - var headerRequestBody = await new StreamReader(request.Body).ReadToEndAsync(); - var parsedDictionary = JsonConvert.DeserializeObject>(headerRequestBody); - - _logger?.LogInformation("StartSpan: {HeaderRequestBody}", headerRequestBody); + var requestJson = await ParseJsonAsync(request.Body); var creationSettings = new SpanCreationSettings { + Parent = FindParentSpanContext(requestJson), FinishOnClose = false, }; - if (parsedDictionary!.TryGetValue("parent_id", out var parentId) && parentId is not null) - { - var longParentId = Convert.ToUInt64(parentId); + string? operationName = null; - if (Spans.TryGetValue(longParentId, out var parentSpan)) - { - creationSettings.Parent = parentSpan.Context; - } - else if (DDContexts.TryGetValue(longParentId, out var ddContext)) - { - creationSettings.Parent = ddContext; - } - else - { - throw new Exception($"Parent span with id {longParentId} not found"); - } + if (requestJson.TryGetProperty("name", out var nameProperty)) + { + operationName = nameProperty.GetString(); } - parsedDictionary.TryGetValue("name", out var name); - using var scope = Tracer.Instance.StartActive(operationName: name!.ToString()!, creationSettings); + using var scope = Tracer.Instance.StartActive(operationName ?? "", creationSettings); var span = scope.Span; - if (parsedDictionary.TryGetValue("service", out var service) && service is not null) + if (requestJson.TryGetProperty("service", out var service) && service.ValueKind != JsonValueKind.Null) { - span.ServiceName = service.ToString(); + // TODO: setting service name to null causes an exception when the span is closed + span.ServiceName = service.GetString(); } - if (parsedDictionary.TryGetValue("resource", out var resource) && resource is not null) + if (requestJson.TryGetProperty("resource", out var resource) && service.ValueKind != JsonValueKind.Null) { - span.ResourceName = resource.ToString(); + span.ResourceName = resource.GetString(); } - if (parsedDictionary.TryGetValue("type", out var type) && type is not null) + if (requestJson.TryGetProperty("type", out var type) && type.ValueKind != JsonValueKind.Null) { - span.Type = type.ToString(); + span.Type = type.GetString(); } - if (parsedDictionary.TryGetValue("span_tags", out var tagsToken) && tagsToken is not null) + if (requestJson.TryGetProperty("span_tags", out var tags) && tags.ValueKind != JsonValueKind.Null) { - foreach (var tag in (Newtonsoft.Json.Linq.JArray)tagsToken) + foreach (var tag in tags.EnumerateArray()) { - var key = (string)tag[0]!; - var value = (string?)tag[1]; + var key = tag[0].GetString()!; + var value = tag[1].GetString(); span.SetTag(key, value); } @@ -152,107 +125,103 @@ private static async Task StartSpan(HttpRequest request) Spans[span.SpanId] = span; - return JsonConvert.SerializeObject(new + return Result(new { - span_id = span.SpanId.ToString(), - trace_id = span.TraceId.ToString(), + span_id = span.SpanId, + trace_id = span.TraceId, + trace_id_128 = span.GetTag("trace.id") }); } private static async Task SpanSetMeta(HttpRequest request) { - var headerBodyDictionary = await new StreamReader(request.Body).ReadToEndAsync(); - var parsedDictionary = JsonConvert.DeserializeObject>(headerBodyDictionary); - parsedDictionary!.TryGetValue("span_id", out var id); - parsedDictionary.TryGetValue("key", out var key); - parsedDictionary.TryGetValue("value", out var value); - - var span = Spans[Convert.ToUInt64(id)]; - span.SetTag(key!, value); + var requestJson = await ParseJsonAsync(request.Body); + var span = FindSpan(requestJson); + + var key = requestJson.GetProperty("key").GetString() ?? + throw new InvalidOperationException("key is null in request json."); + + var value = requestJson.GetProperty("value").GetString() ?? + throw new InvalidOperationException("value is null in request json."); + + span.SetTag(key, value); + _logger?.LogInformation("Set string span attribute {key}:{value} on span {spanId}.", key, value, span.SpanId); } private static async Task SpanSetMetric(HttpRequest request) { - var headerBodyDictionary = await new StreamReader(request.Body).ReadToEndAsync(); - var parsedDictionary = JsonConvert.DeserializeObject>(headerBodyDictionary)!; - parsedDictionary.TryGetValue("span_id", out var id); - parsedDictionary.TryGetValue("key", out var key); - parsedDictionary.TryGetValue("value", out var value); - - var span = Spans[Convert.ToUInt64(id)]; - span.SetTag(key!, Convert.ToDouble(value)); + var requestJson = await ParseJsonAsync(request.Body); + var span = FindSpan(requestJson); + + var key = requestJson.GetProperty("key").GetString() ?? + throw new InvalidOperationException("key is null in request json."); + + var value = requestJson.GetProperty("value").GetDouble(); + + span.SetTag(key, value); + _logger?.LogInformation("Set numeric span attribute {key}:{value} on span {spanId}.", key, value, span.SpanId); } private static async Task SpanSetError(HttpRequest request) { - var span = Spans[Convert.ToUInt64(await FindBodyKeyValueAsync(request, "span_id"))]; - span.Error = true; + var requestJson = await ParseJsonAsync(request.Body); - var type = await FindBodyKeyValueAsync(request, "type"); - var message = await FindBodyKeyValueAsync(request, "message"); - var stack = await FindBodyKeyValueAsync(request, "stack"); + var span = FindSpan(requestJson); + var type = requestJson.GetProperty("type").GetString(); + var message = requestJson.GetProperty("message").GetString(); + var stack = requestJson.GetProperty("stack").GetString(); - if (!string.IsNullOrEmpty(type)) - { - span.SetTag(Tags.ErrorType, type); - } - - if (!string.IsNullOrEmpty(message)) - { - span.SetTag(Tags.ErrorMsg, message); - } - - if (!string.IsNullOrEmpty(stack)) - { - span.SetTag(Tags.ErrorStack, stack); - } + span.Error = true; + span.SetTag(Tags.ErrorType, type); + span.SetTag(Tags.ErrorMsg, message); + span.SetTag(Tags.ErrorStack, stack); } private static async Task ExtractHeaders(HttpRequest request) { - var headerRequestBody = await new StreamReader(request.Body).ReadToEndAsync(); - var parsedDictionary = JsonConvert.DeserializeObject>(headerRequestBody); - var headersList = (Newtonsoft.Json.Linq.JArray)parsedDictionary!["http_headers"]; - var extractedContext = _spanContextExtractor.Extract( - headersList.ToObject()!, - getter: GetHeaderValues - ); - - string? extractedSpanId = null; + var requestJson = await ParseJsonAsync(request.Body); + + var headersList = requestJson.GetProperty("http_headers") + .EnumerateArray() + .GroupBy(pair => pair[0].ToString(), kvp => kvp[1].ToString()) + .Select(g => KeyValuePair.Create(g.Key, g.ToList())); + + // There's a test for case-insensitive header names, so use a case-insensitive comparer. + // (Yeah, the test is only testing this code, not the tracer itself) + // tests/parametric/test_headers_tracecontext.py + // Test_Headers_Tracecontext + // test_traceparent_header_name_valid_casing + var headersDictionary = new Dictionary>(headersList, StringComparer.OrdinalIgnoreCase); + + // TODO: returning null causes an exception when the extractor tried to iterate over the headers + var extractedContext = SpanContextExtractor.Extract( + headersDictionary, + (dict, key) => + dict.GetValueOrDefault(key) ?? []); + if (extractedContext is not null) { - DDContexts[extractedContext.SpanId] = extractedContext; - extractedSpanId = extractedContext.SpanId.ToString(); + SpanContexts[extractedContext.SpanId] = extractedContext; } - return JsonConvert.SerializeObject(new + return Result(new { - span_id = extractedSpanId + span_id = extractedContext?.SpanId }); } private static async Task InjectHeaders(HttpRequest request) { + var requestJson = await ParseJsonAsync(request.Body); + var span = FindSpan(requestJson); var httpHeaders = new List(); - var spanId = await FindBodyKeyValueAsync(request, "span_id"); + SpanContextInjector.Inject( + httpHeaders, + (headers, key, value) => headers.Add([key, value]), + span.Context); - if (!string.IsNullOrEmpty(spanId) && Spans.TryGetValue(Convert.ToUInt64(spanId), out var span)) - { - // Define a function to set headers in HttpRequestHeaders - static void Setter(List headers, string key, string value) => - headers.Add([key, value]); - - Console.WriteLine(JsonConvert.SerializeObject(new - { - HttpHeaders = httpHeaders - })); - - // Invoke SpanContextPropagator.Inject with the HttpRequestHeaders - _spanContextInjector.Inject(httpHeaders, Setter, span.Context); - } - - return JsonConvert.SerializeObject(new + return Result(new { http_headers = httpHeaders }); @@ -260,21 +229,24 @@ static void Setter(List headers, string key, string value) => private static async Task FinishSpan(HttpRequest request) { - var span = Spans[Convert.ToUInt64(await FindBodyKeyValueAsync(request, "span_id"))]; + var requestJson = await ParseJsonAsync(request.Body); + var span = FindSpan(requestJson); span.Finish(); + + _logger?.LogInformation("Finished span {spanId}.", span.SpanId); } private static string Crash(HttpRequest request) { var thread = new Thread(() => throw new BadImageFormatException("Expected")); - thread.Start(); thread.Join(); - return "Failed to crash"; + _logger?.LogInformation("Failed to crash"); + return Result("Failed to crash"); } - private static string GetTracerConfig(HttpRequest request) + private static string GetTracerConfig() { var tracerSettings = Tracer.Instance.Settings; var internalTracer = GetTracerInstance.GetValue(null); @@ -305,13 +277,13 @@ private static string GetTracerConfig(HttpRequest request) // { "dd_trace_sample_ignore_parent", "null" }, // Not supported }; - return JsonConvert.SerializeObject(new + return Result(new { config }); } - internal static async Task FlushSpans() + protected static async Task FlushSpans() { if (Tracer.Instance is null) { @@ -320,23 +292,20 @@ internal static async Task FlushSpans() await Tracer.Instance.ForceFlushAsync(); Spans.Clear(); - ApmTestApiOtel.Activities.Clear(); + SpanContexts.Clear(); + ApmTestApiOtel.ClearActivities(); } - internal static async Task FlushTraceStats() + protected static async Task FlushTraceStats() { - if (GetTracerManager is null) - { - throw new NullReferenceException("GetTracerManager is null"); - } - if (Tracer.Instance is null) { throw new NullReferenceException("Tracer.Instance is null"); } - var tracerManager = GetTracerManager.GetValue(GetTracerInstance.GetValue(null)); - var agentWriter = GetAgentWriter.Invoke(tracerManager, null); + var tracer = GetTracerInstance.GetValue(null); + var tracerManager = GetTracerManager.GetValue(tracer); + var agentWriter = GetAgentWriter.GetValue(tracerManager); var statsAggregator = GetStatsAggregator.GetValue(agentWriter); if (statsAggregator?.GetType() == StatsAggregatorType) @@ -354,12 +323,69 @@ internal static async Task FlushTraceStats() } } - private static async Task FindBodyKeyValueAsync(HttpRequest httpRequest, string keyToFind) + private static ISpan FindSpan(JsonElement json, string key = "span_id") + { + var spanId = json.GetProperty(key).GetUInt64(); + + if (!Spans.TryGetValue(spanId, out var span)) + { + _logger?.LogError("Span not found with span id: {spanId}.", spanId); + throw new InvalidOperationException($"Span not found with span id: {spanId}"); + } + + return span; + } + + private static ISpanContext? FindParentSpanContext(JsonElement json, string key = "parent_id") + { + var jsonProperty = json.GetProperty(key); + + if (jsonProperty.ValueKind == JsonValueKind.Null) + { + return null; + } + + var spanId = jsonProperty.GetUInt64(); + + if (Spans.TryGetValue(spanId, out var span)) + { + return span.Context; + } + + if (SpanContexts.TryGetValue(spanId, out var spanContext)) + { + return spanContext; + } + + _logger?.LogError("Span or SpanContext not found with span id: {spanId}.", spanId); + throw new InvalidOperationException($"Span or SpanContext not found with span id: {spanId}"); } + + protected static async Task ParseJsonAsync(Stream stream, [CallerMemberName] string? caller = null) { - var headerBodyDictionary = await new StreamReader(httpRequest.Body).ReadToEndAsync(); - var parsedDictionary = JsonConvert.DeserializeObject>(headerBodyDictionary); - var keyFound = parsedDictionary!.TryGetValue(keyToFind, out var foundValue); + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-dom#jsondocument-is-idisposable + using var jsonDoc = await JsonDocument.ParseAsync(stream); + var root = jsonDoc.RootElement.Clone(); - return keyFound ? foundValue! : string.Empty; + _logger?.LogInformation("Handler {handler} called with {HttpRequest.Body}.", caller, root); + return root; + } + + protected static string Result(object? value = null, [CallerMemberName] string? caller = null) + { + switch (value) + { + case null: + _logger?.LogInformation("Handler {handler} finished.", caller); + return string.Empty; + case string s: + _logger?.LogInformation("Handler {handler} returning \"{message}\".", caller, s); + return s; + default: + { + var json = JsonSerializer.Serialize(value); + _logger?.LogInformation("Handler {handler} returning {JsonResult}", caller, json); + return json; + } + } } } diff --git a/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs b/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs index 983ee5c809b..7c9da22fb29 100644 --- a/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs +++ b/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Globalization; -using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -8,11 +7,14 @@ namespace ApmTestApi.Endpoints; public abstract class ApmTestApiOtel : ApmTestApi { - internal static readonly ActivitySource ApmTestApiActivitySource = new("ApmTestApi"); - internal static readonly Dictionary Activities = new(); + private static readonly ActivitySource ApmTestApiActivitySource = new("ApmTestApi"); + private static readonly Dictionary Activities = new(); + private static ILogger? _logger; - public static void MapApmOtelEndpoints(WebApplication app) + public static void MapApmOtelEndpoints(WebApplication app, ILogger logger) { + _logger = logger; + app.MapPost("/trace/otel/start_span", OtelStartSpan); app.MapPost("/trace/otel/end_span", OtelEndSpan); app.MapPost("/trace/otel/flush", OtelFlushSpans); @@ -355,7 +357,7 @@ private static async Task OtelFlushTraceStats(HttpRequest request) } // Helper methods: - private static async Task> DeserializeRequestObjectAsync(Stream requestBody) + private static async Task> DeserializeRequestObjectAsync(Stream requestBody) { var headerRequestBody = await new StreamReader(requestBody).ReadToEndAsync(); return JsonConvert.DeserializeObject>(headerRequestBody)!; @@ -465,4 +467,9 @@ private static void SetTag(Activity activity, Dictionary? attribu return tags; } + + public static void ClearActivities() + { + Activities.Clear(); + } } diff --git a/utils/build/docker/dotnet/parametric/Program.cs b/utils/build/docker/dotnet/parametric/Program.cs index fab9edaf2a3..71b0c817519 100644 --- a/utils/build/docker/dotnet/parametric/Program.cs +++ b/utils/build/docker/dotnet/parametric/Program.cs @@ -1,24 +1,19 @@ -using System.Diagnostics; -using ApmTestApi.Endpoints; - // Force the initialization of the tracer _ = Datadog.Trace.Tracer.Instance; var builder = WebApplication.CreateBuilder(args); - var app = builder.Build(); var logger = app.Services.GetRequiredService>(); +var otelLogger = app.Services.GetRequiredService>(); // Map endpoints ApmTestApi.Endpoints.ApmTestApi.MapApmTraceEndpoints(app, logger); -ApmTestApiOtel.MapApmOtelEndpoints(app); +ApmTestApi.Endpoints.ApmTestApiOtel.MapApmOtelEndpoints(app, otelLogger); -if (int.TryParse(Environment.GetEnvironmentVariable("APM_TEST_CLIENT_SERVER_PORT"), out var port)) -{ - app.Run($"http://0.0.0.0:{port}"); -} -else +if (!int.TryParse(Environment.GetEnvironmentVariable("APM_TEST_CLIENT_SERVER_PORT"), out var port)) { throw new InvalidOperationException("Unable to get value for expected `APM_TEST_CLIENT_SERVER_PORT` configuration."); } + +app.Run($"http://0.0.0.0:{port}"); diff --git a/utils/build/docker/dotnet/parametric/appsettings.Development.json b/utils/build/docker/dotnet/parametric/appsettings.Development.json index 45fe774a9f8..63ed6e14613 100644 --- a/utils/build/docker/dotnet/parametric/appsettings.Development.json +++ b/utils/build/docker/dotnet/parametric/appsettings.Development.json @@ -2,8 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", + "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } } -} \ No newline at end of file +} diff --git a/utils/build/docker/dotnet/parametric/appsettings.json b/utils/build/docker/dotnet/parametric/appsettings.json index 222224e368e..214d63f9b19 100644 --- a/utils/build/docker/dotnet/parametric/appsettings.json +++ b/utils/build/docker/dotnet/parametric/appsettings.json @@ -2,9 +2,9 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", + "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" -} \ No newline at end of file +} From 56e10a4ba0c67ce2ffa0a97a680459d9834493ef Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Fri, 17 Jan 2025 14:13:15 -0500 Subject: [PATCH 02/17] Khanayan123/add consistent config system tests (#3745) Co-authored-by: Charles de Beauchesne Co-authored-by: Mikayla Toffler <46911781+mtoffl01@users.noreply.github.com> --- .github/workflows/run-end-to-end.yml | 3 + docs/weblog/README.md | 8 + manifests/cpp.yml | 6 + manifests/dotnet.yml | 6 + manifests/golang.yml | 6 + manifests/java.yml | 6 + manifests/nodejs.yml | 25 ++- manifests/php.yml | 6 + manifests/python.yml | 6 + manifests/ruby.yml | 6 + tests/parametric/test_128_bit_traceids.py | 25 ++- tests/parametric/test_config_consistency.py | 54 ++++- tests/parametric/test_trace_sampling.py | 64 +++--- tests/test_config_consistency.py | 126 ++++++++++++ utils/_context/_scenarios/__init__.py | 9 + utils/_context/_scenarios/endtoend.py | 2 + utils/_context/containers.py | 4 + utils/build/docker/nodejs/express/app.js | 18 ++ .../docker/nodejs/express/package-lock.json | 185 ++++++++++++++++++ .../build/docker/nodejs/express/package.json | 1 + .../build/docker/nodejs/parametric/server.js | 3 +- utils/proxy/_deserializer.py | 4 + 22 files changed, 539 insertions(+), 34 deletions(-) diff --git a/.github/workflows/run-end-to-end.yml b/.github/workflows/run-end-to-end.yml index 301e1221aab..0d745b52abd 100644 --- a/.github/workflows/run-end-to-end.yml +++ b/.github/workflows/run-end-to-end.yml @@ -165,6 +165,9 @@ jobs: - name: Run LIBRARY_CONF_CUSTOM_HEADER_TAGS_INVALID scenario if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"LIBRARY_CONF_CUSTOM_HEADER_TAGS_INVALID"') run: ./run.sh LIBRARY_CONF_CUSTOM_HEADER_TAGS_INVALID + - name: Run RUNTIME_METRICS_ENABLED scenario + if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"RUNTIME_METRICS_ENABLED"') + run: ./run.sh RUNTIME_METRICS_ENABLED - name: Run TRACING_CONFIG_NONDEFAULT scenario if: always() && steps.build.outcome == 'success' && contains(inputs.scenarios, '"TRACING_CONFIG_NONDEFAULT"') run: ./run.sh TRACING_CONFIG_NONDEFAULT diff --git a/docs/weblog/README.md b/docs/weblog/README.md index 231e2f12bb7..9b81faab338 100644 --- a/docs/weblog/README.md +++ b/docs/weblog/README.md @@ -618,6 +618,14 @@ Expected query parameters: This endpoint loads a module/package in applicable languages. It's mainly used for telemetry tests to verify that the `dependencies-loaded` event is appropriately triggered. +### GET /log/library + +This endpoint facilitates logging a message using a logging library. It is primarily designed for testing log injection functionality. Weblog apps must log using JSON format. + +The following query parameters are optional: +- `msg`: Specifies the message to be logged. If not provided, the default message "msg" will be logged. +- `level`: Specifies the log level to be used. If not provided, the default log level is "info". + ### GET /e2e_single_span This endpoint will create two spans, a parent span (which is a root-span), and a child span. diff --git a/manifests/cpp.yml b/manifests/cpp.yml index 2b4b946c81d..c89f9876419 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -244,9 +244,15 @@ tests/: Test_Config_HttpServerErrorStatuses_FeatureFlagCustom: missing_feature Test_Config_IntegrationEnabled_False: missing_feature Test_Config_IntegrationEnabled_True: missing_feature + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature + Test_Config_LogInjection_128Bit_TradeId_Disabled: missing_feature + Test_Config_LogInjection_Default: missing_feature + Test_Config_LogInjection_Enabled: missing_feature Test_Config_ObfuscationQueryStringRegexp_Configured: missing_feature Test_Config_ObfuscationQueryStringRegexp_Default: missing_feature Test_Config_ObfuscationQueryStringRegexp_Empty: missing_feature + Test_Config_RuntimeMetrics_Default: missing_feature + Test_Config_RuntimeMetrics_Enabled: missing_feature Test_Config_UnifiedServiceTagging_CustomService: missing_feature Test_Config_UnifiedServiceTagging_Default: missing_feature test_distributed.py: diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 44a8085ee7e..a6e22bec4cc 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -488,9 +488,15 @@ tests/: Test_Config_HttpServerErrorStatuses_FeatureFlagCustom: v3.5.0 Test_Config_IntegrationEnabled_False: v3.5.0 Test_Config_IntegrationEnabled_True: v3.5.0 + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (disabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: incomplete_test_app (weblog endpoint not implemented) + Test_Config_LogInjection_Default: incomplete_test_app (weblog endpoint not implemented) + Test_Config_LogInjection_Enabled: incomplete_test_app (weblog endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: v3.4.1 Test_Config_ObfuscationQueryStringRegexp_Default: v3.4.1 Test_Config_ObfuscationQueryStringRegexp_Empty: v3.4.1 + Test_Config_RuntimeMetrics_Default: incomplete_test_app (test needs to account for dotnet runtime metrics) + Test_Config_RuntimeMetrics_Enabled: incomplete_test_app (test needs to account for dotnet runtime metrics) Test_Config_UnifiedServiceTagging_CustomService: v3.3.0 Test_Config_UnifiedServiceTagging_Default: v3.3.0 test_data_integrity.py: diff --git a/manifests/golang.yml b/manifests/golang.yml index 2e229ec737f..7deb02009a5 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -592,9 +592,15 @@ tests/: uds-echo: missing_feature Test_Config_IntegrationEnabled_False: irrelevant (not applicable to Go because of how they do auto instrumentation) Test_Config_IntegrationEnabled_True: irrelevant (not applicable to Go because of how they do auto instrumentation) + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (disabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: incomplete_test_app (weblog endpoint not implemented) + Test_Config_LogInjection_Default: incomplete_test_app (weblog endpoint not implemented) + Test_Config_LogInjection_Enabled: incomplete_test_app (weblog endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: v1.67.0 Test_Config_ObfuscationQueryStringRegexp_Default: v1.67.0 Test_Config_ObfuscationQueryStringRegexp_Empty: v1.67.0 + Test_Config_RuntimeMetrics_Default: incomplete_test_app (test needs to account for golang runtime metrics) + Test_Config_RuntimeMetrics_Enabled: incomplete_test_app (test needs to account for golang runtime metrics) Test_Config_UnifiedServiceTagging_CustomService: v1.67.0 Test_Config_UnifiedServiceTagging_Default: v1.67.0 test_data_integrity.py: diff --git a/manifests/java.yml b/manifests/java.yml index ee589c7d965..2666fe7a792 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -1683,9 +1683,15 @@ tests/: Test_Config_IntegrationEnabled_True: '*': irrelevant (kafka endpoints are not implemented) spring-boot: v1.42.0 + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (disabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: incomplete_test_app (weblog endpoint not implemented) + Test_Config_LogInjection_Default: incomplete_test_app (weblog endpoint not implemented) + Test_Config_LogInjection_Enabled: incomplete_test_app (weblog endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: v1.39.0 Test_Config_ObfuscationQueryStringRegexp_Default: v1.39.0 Test_Config_ObfuscationQueryStringRegexp_Empty: v1.39.0 + Test_Config_RuntimeMetrics_Default: incomplete_test_app (test needs to account for java runtime metrics) + Test_Config_RuntimeMetrics_Enabled: incomplete_test_app (test needs to account for java runtime metrics) Test_Config_UnifiedServiceTagging_CustomService: v1.39.0 Test_Config_UnifiedServiceTagging_Default: v1.39.0 test_data_integrity.py: diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index ec775fe8d8c..dd6ade61ef4 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -746,6 +746,8 @@ tests/: test_otel_drop_in.py: Test_Otel_Drop_In: missing_feature parametric/: + test_128_bit_traceids.py: + Test_128_Bit_Traceids: *ref_3_0_0 test_config_consistency.py: Test_Config_Dogstatsd: *ref_5_29_0 Test_Config_RateLimit: *ref_5_25_0 @@ -835,14 +837,29 @@ tests/: Test_Config_HttpServerErrorStatuses_FeatureFlagCustom: missing_feature Test_Config_IntegrationEnabled_False: '*': *ref_5_25_0 - express4-typescript: irrelevant - nextjs: irrelevant # nextjs is not related with kafka + express4-typescript: incomplete_test_app + nextjs: incomplete_test_app Test_Config_IntegrationEnabled_True: '*': *ref_5_25_0 - express4-typescript: irrelevant - nextjs: irrelevant # nextjs is not related with kafka + express4-typescript: incomplete_test_app + nextjs: incomplete_test_app + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (disabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: + '*': *ref_3_15_0 + express4-typescript: incomplete_test_app (endpoint not implemented) + nextjs: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Default: + '*': *ref_3_0_0 + express4-typescript: incomplete_test_app (endpoint not implemented) + nextjs: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Enabled: + '*': *ref_3_0_0 + express4-typescript: incomplete_test_app (endpoint not implemented) + nextjs: incomplete_test_app (endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: *ref_3_0_0 Test_Config_ObfuscationQueryStringRegexp_Empty: *ref_3_0_0 + Test_Config_RuntimeMetrics_Default: *ref_3_0_0 + Test_Config_RuntimeMetrics_Enabled: *ref_3_0_0 Test_Config_UnifiedServiceTagging_CustomService: *ref_5_25_0 Test_Config_UnifiedServiceTagging_Default: *ref_5_25_0 test_distributed.py: diff --git a/manifests/php.yml b/manifests/php.yml index 9560d2fae2c..48cc7e607b5 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -413,8 +413,14 @@ tests/: Test_Config_HttpServerErrorStatuses_FeatureFlagCustom: missing_feature Test_Config_IntegrationEnabled_False: v1.4.0 Test_Config_IntegrationEnabled_True: v1.4.0 + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (not enabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Default: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Enabled: incomplete_test_app (endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: v1.5.0 Test_Config_ObfuscationQueryStringRegexp_Empty: v1.5.0 + Test_Config_RuntimeMetrics_Default: missing_feature + Test_Config_RuntimeMetrics_Enabled: missing_feature Test_Config_UnifiedServiceTagging_CustomService: v1.4.0 Test_Config_UnifiedServiceTagging_Default: v1.4.0 test_distributed.py: diff --git a/manifests/python.yml b/manifests/python.yml index 088a49c67d5..281cd64ade5 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -885,8 +885,14 @@ tests/: Test_Config_IntegrationEnabled_True: '*': irrelevant (kafka endpoint is not implemented) flask-poc: v2.0.0 + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (not enabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Default: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Enabled: incomplete_test_app (endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: v2.0.0 Test_Config_ObfuscationQueryStringRegexp_Empty: v2.15.0 + Test_Config_RuntimeMetrics_Default: incomplete_test_app (test needs to account for python runtime metrics) + Test_Config_RuntimeMetrics_Enabled: incomplete_test_app (test needs to account for python runtime metrics) Test_Config_UnifiedServiceTagging_CustomService: v2.0.0 Test_Config_UnifiedServiceTagging_Default: v2.0.0 test_data_integrity.py: diff --git a/manifests/ruby.yml b/manifests/ruby.yml index a74054c4036..b2d7ded6a1e 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -490,9 +490,15 @@ tests/: Test_Config_IntegrationEnabled_True: '*': irrelevant (endpoint not implemented) rails70: v2.0.0 + Test_Config_LogInjection_128Bit_TradeId_Default: missing_feature (not enabled by default) + Test_Config_LogInjection_128Bit_TradeId_Disabled: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Default: incomplete_test_app (endpoint not implemented) + Test_Config_LogInjection_Enabled: incomplete_test_app (endpoint not implemented) Test_Config_ObfuscationQueryStringRegexp_Configured: missing_feature Test_Config_ObfuscationQueryStringRegexp_Default: bug (APMAPI-1013) Test_Config_ObfuscationQueryStringRegexp_Empty: missing_feature (environment variable is not supported) + Test_Config_RuntimeMetrics_Default: incomplete_test_app (test needs to account for ruby runtime metrics) + Test_Config_RuntimeMetrics_Enabled: incomplete_test_app (test needs to account for ruby runtime metrics) Test_Config_UnifiedServiceTagging_CustomService: v2.0.0 Test_Config_UnifiedServiceTagging_Default: v2.0.0 test_distributed.py: diff --git a/tests/parametric/test_128_bit_traceids.py b/tests/parametric/test_128_bit_traceids.py index be10b9ea417..4a8234cb059 100644 --- a/tests/parametric/test_128_bit_traceids.py +++ b/tests/parametric/test_128_bit_traceids.py @@ -193,7 +193,7 @@ def test_datadog_128_bit_generation_enabled(self, test_agent, test_library): @missing_feature(context.library == "golang", reason="not implemented") @missing_feature(context.library < "java@1.24.0", reason="Implemented in 1.24.0") - @missing_feature(context.library == "nodejs", reason="not implemented") + @missing_feature(context.library < "nodejs@4.19.0", reason="Implemented in 4.19.0 & 3.40.0") @missing_feature(context.library == "ruby", reason="not implemented") @pytest.mark.parametrize("library_env", [{"DD_TRACE_PROPAGATION_STYLE": "Datadog"}]) def test_datadog_128_bit_generation_enabled_by_default(self, test_agent, test_library): @@ -383,6 +383,7 @@ def test_w3c_128_bit_propagation(self, test_agent, test_library): assert dd_p_tid == "640cfd8d00000000" check_128_bit_trace_id(fields[1], trace_id, dd_p_tid) + @missing_feature(context.library < "nodejs@5.7.0", reason="implemented in 5.7.0 & 4.31.0") @missing_feature(context.library == "ruby", reason="not implemented") @pytest.mark.parametrize( "library_env", @@ -409,8 +410,6 @@ def test_w3c_128_bit_propagation_tid_consistent(self, test_agent, test_library): assert propagation_error is None @missing_feature(context.library == "ruby", reason="not implemented") - @missing_feature(context.library == "nodejs", reason="not implemented") - @missing_feature(context.library == "java", reason="not implemented") @pytest.mark.parametrize( "library_env", [{"DD_TRACE_PROPAGATION_STYLE": "tracecontext", "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": "true"}], @@ -422,6 +421,26 @@ def test_w3c_128_bit_propagation_tid_in_chunk_root(self, test_agent, test_librar with test_library.dd_start_span(name="child", service="service", parent_id=parent.span_id) as child: pass + traces = test_agent.wait_for_num_traces(1, clear=True, sort_by_start=False) + trace = find_trace(traces, parent.trace_id) + assert len(trace) == 2 + first_span = find_first_span_in_trace_payload(trace) + tid_chunk_root = first_span["meta"].get("_dd.p.tid") + assert tid_chunk_root is not None + + @missing_feature(context.library == "ruby", reason="not implemented") + @missing_feature(context.library == "java", reason="not implemented") + @pytest.mark.parametrize( + "library_env", + [{"DD_TRACE_PROPAGATION_STYLE": "tracecontext", "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": "true"}], + ) + def test_w3c_128_bit_propagation_tid_only_in_chunk_root(self, test_agent, test_library): + """Ensure that only root span contains the tid.""" + with test_library: + with test_library.dd_start_span(name="parent", service="service", resource="resource") as parent: + with test_library.dd_start_span(name="child", service="service", parent_id=parent.span_id) as child: + pass + traces = test_agent.wait_for_num_traces(1, clear=True, sort_by_start=False) trace = find_trace(traces, parent.trace_id) assert len(trace) == 2 diff --git a/tests/parametric/test_config_consistency.py b/tests/parametric/test_config_consistency.py index e78da652419..eb66e3593ed 100644 --- a/tests/parametric/test_config_consistency.py +++ b/tests/parametric/test_config_consistency.py @@ -138,7 +138,7 @@ class Test_Config_TraceAgentURL: { "DD_TRACE_AGENT_URL": "unix:///var/run/datadog/apm.socket", "DD_AGENT_HOST": "localhost", - "DD_AGENT_PORT": "8126", + "DD_TRACE_AGENT_PORT": "8126", } ], ) @@ -153,7 +153,13 @@ def test_dd_trace_agent_unix_url_nonexistent(self, library_env, test_agent, test # The DD_TRACE_AGENT_URL is validated using the tracer configuration. This approach avoids the need to modify the setup file to create additional containers at the specified URL, which would be unnecessarily complex. @parametrize( "library_env", - [{"DD_TRACE_AGENT_URL": "http://random-host:9999/", "DD_AGENT_HOST": "localhost", "DD_AGENT_PORT": "8126"}], + [ + { + "DD_TRACE_AGENT_URL": "http://random-host:9999/", + "DD_AGENT_HOST": "localhost", + "DD_TRACE_AGENT_PORT": "8126", + } + ], ) def test_dd_trace_agent_http_url_nonexistent(self, library_env, test_agent, test_library): with test_library as t: @@ -164,6 +170,50 @@ def test_dd_trace_agent_http_url_nonexistent(self, library_env, test_agent, test assert url.hostname == "random-host" assert url.port == 9999 + @parametrize( + "library_env", + [ + { + "DD_TRACE_AGENT_URL": "http://[::1]:5000", + "DD_AGENT_HOST": "localhost", + "DD_TRACE_AGENT_PORT": "8126", + } + ], + ) + @missing_feature(context.library == "ruby", reason="does not support ipv6") + def test_dd_trace_agent_http_url_ipv6(self, library_env, test_agent, test_library): + with test_library as t: + resp = t.config() + + url = urlparse(resp["dd_trace_agent_url"]) + assert url.scheme == "http" + assert url.hostname == "::1" + assert url.port == 5000 + + @parametrize( + "library_env", + [ + { + "DD_TRACE_AGENT_URL": "", # Empty string passed to make sure conftest.py does not set trace agent url + "DD_AGENT_HOST": "[::1]", + "DD_TRACE_AGENT_PORT": "5000", + } + ], + ) + @missing_feature(context.library == "ruby", reason="does not support ipv6 hostname") + @missing_feature(context.library == "dotnet", reason="does not support ipv6 hostname") + @missing_feature(context.library == "php", reason="does not support ipv6 hostname") + @missing_feature(context.library == "golang", reason="does not support ipv6 hostname") + @missing_feature(context.library == "python", reason="does not support ipv6 hostname") + def test_dd_agent_host_ipv6(self, library_env, test_agent, test_library): + with test_library as t: + resp = t.config() + + url = urlparse(resp["dd_trace_agent_url"]) + assert url.scheme == "http" + assert url.hostname == "::1" + assert url.port == 5000 + @scenarios.parametric @features.tracing_configuration_consistency diff --git a/tests/parametric/test_trace_sampling.py b/tests/parametric/test_trace_sampling.py index 7f632ce6d94..1697327a6af 100644 --- a/tests/parametric/test_trace_sampling.py +++ b/tests/parametric/test_trace_sampling.py @@ -18,7 +18,6 @@ class Test_Trace_Sampling_Basic: [ { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "webserver.non-matching", "sample_rate": 0}, @@ -28,14 +27,12 @@ class Test_Trace_Sampling_Basic: }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"name": "web.request.non-matching", "sample_rate": 0}, {"name": "web.request", "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "webserver.non-matching", "name": "web.request", "sample_rate": 0}, @@ -61,7 +58,6 @@ def test_trace_sampled_by_trace_sampling_rule_exact_match(self, test_agent, test [ { "DD_TRACE_SAMPLE_RATE": 1, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"service": "webserver", "name": "web.request", "sample_rate": 0}] ), @@ -83,7 +79,6 @@ def test_trace_dropped_by_trace_sampling_rule(self, test_agent, test_library): [ { "DD_TRACE_SAMPLE_RATE": 1, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"service": "webserver", "resource": "drop-me", "sample_rate": 0}] ), @@ -112,21 +107,18 @@ class Test_Trace_Sampling_Globs: [ { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"service": "web.non-matching*", "sample_rate": 0}, {"service": "web*", "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"name": "web.non-matching*", "sample_rate": 0}, {"name": "web.*", "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "webserv?r.non-matching", "name": "web.req*", "sample_rate": 0}, @@ -147,12 +139,51 @@ def test_trace_sampled_by_trace_sampling_rule_glob_match(self, test_agent, test_ assert span["metrics"].get(SAMPLING_PRIORITY_KEY) == 2 assert span["metrics"].get(SAMPLING_RULE_PRIORITY_RATE) == 1.0 + @pytest.mark.parametrize( + "library_env", + [ + { + "DD_TRACE_SAMPLE_RATE": 0, + "DD_TRACE_SAMPLING_RULES": json.dumps( + [ + {"name": "wEb.rEquEst", "sample_rate": 1}, + ] + ), + }, + { + "DD_TRACE_SAMPLE_RATE": 0, + "DD_TRACE_SAMPLING_RULES": json.dumps([{"service": "wEbSerVer", "sample_rate": 1}]), + }, + { + "DD_TRACE_SAMPLE_RATE": 0, + "DD_TRACE_SAMPLING_RULES": json.dumps([{"resource": "/rAnDom", "sample_rate": 1}]), + }, + { + "DD_TRACE_SAMPLE_RATE": 0, + "DD_TRACE_SAMPLING_RULES": json.dumps([{"tags": {"key": "vAlUe"}, "sample_rate": 1}]), + }, + ], + ) + @bug(library="cpp", reason="APMAPI-908") + @bug(library="nodejs", reason="APMAPI-1011") + def test_field_case_insensitivity(self, test_agent, test_library): + """Tests that sampling rule field values are case insensitive""" + with test_library: + with test_library.dd_start_span( + name="web.request", service="webserver", resource="/random", tags=[("key", "value")] + ) as span: + pass + + span = find_only_span(test_agent.wait_for_num_traces(1)) + + assert span["metrics"].get(SAMPLING_PRIORITY_KEY) == 2 + assert span["metrics"].get(SAMPLING_RULE_PRIORITY_RATE) == 1.0 + @pytest.mark.parametrize( "library_env", [ { "DD_TRACE_SAMPLE_RATE": 1, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps([{"service": "w?bs?rv?r", "name": "web.*", "sample_rate": 0}]), } ], @@ -178,21 +209,18 @@ class Test_Trace_Sampling_Globs_Feb2024_Revision: [ { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"service": "web.non-matching*", "sample_rate": 0}, {"service": "web*", "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"name": "web.non-matching*", "sample_rate": 0}, {"name": "wEb.*", "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "webserv?r.non-matching", "name": "wEb.req*", "sample_rate": 0}, @@ -224,14 +252,12 @@ class Test_Trace_Sampling_Resource: [ { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"resource": "/bar.non-matching", "sample_rate": 0}, {"resource": "/?ar", "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"name": "web.request.non-matching", "resource": "/bar", "sample_rate": 0}, @@ -242,7 +268,6 @@ class Test_Trace_Sampling_Resource: }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "webserver.non-matching", "resource": "/bar", "sample_rate": 0}, @@ -253,7 +278,6 @@ class Test_Trace_Sampling_Resource: }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ { @@ -295,7 +319,6 @@ def test_trace_sampled_by_trace_sampling_rule_exact_match(self, test_agent, test [ { "DD_TRACE_SAMPLE_RATE": 1, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "non-matching", "sample_rate": 1}, @@ -328,14 +351,12 @@ class Test_Trace_Sampling_Tags: [ { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"tags": {"tag1": "non-matching"}, "sample_rate": 0}, {"tags": {"tag1": "val1"}, "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"tags": {"tag1": "non-matching"}, "sample_rate": 0}, @@ -348,14 +369,12 @@ class Test_Trace_Sampling_Tags: }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [{"tags": {"tag1": "v?r*"}, "sample_rate": 0}, {"tags": {"tag1": "val?"}, "sample_rate": 1}] ), }, { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"service": "webs?rver.non-matching", "sample_rate": 0}, @@ -390,7 +409,6 @@ def test_trace_sampled_by_trace_sampling_rule_tags(self, test_agent, test_librar [ { "DD_TRACE_SAMPLE_RATE": 1, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"tags": {"tag1": "v?l1", "tag2": "non-matching"}, "sample_rate": 1}, @@ -415,7 +433,6 @@ def test_trace_dropped_by_trace_sampling_rule_tags(self, test_agent, test_librar def tag_sampling_env(tag_glob_pattern): return { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps([{"tags": {"tag": tag_glob_pattern}, "sample_rate": 1.0}]), } @@ -553,7 +570,6 @@ class Test_Trace_Sampling_With_W3C: [ { "DD_TRACE_SAMPLE_RATE": 0, - "DD_TRACE_SAMPLING_RULES_FORMAT": "glob", "DD_TRACE_SAMPLING_RULES": json.dumps( [ {"tags": {"tag2": "val2"}, "sample_rate": 0}, diff --git a/tests/test_config_consistency.py b/tests/test_config_consistency.py index 15d3a3bdff5..83aebef435b 100644 --- a/tests/test_config_consistency.py +++ b/tests/test_config_consistency.py @@ -2,10 +2,16 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2022 Datadog, Inc. +import re import json from utils import weblog, interfaces, scenarios, features, rfc, irrelevant, context, bug, missing_feature from utils.tools import logger +# get the default log output +stdout = interfaces.library_stdout if context.library != "dotnet" else interfaces.library_dotnet_managed +runtime_metrics = {"nodejs": "runtime.node.mem.heap_total"} +log_injection_fields = {"nodejs": {"message": "msg"}} + @scenarios.default @features.tracing_configuration_consistency @@ -468,3 +474,123 @@ def test_integration_enabled_true(self): assert list( filter(lambda span: "kafka.produce" in span.get("name"), spans) ), f"No kafka.produce span found in trace: {spans}" + + +@rfc("https://docs.google.com/document/d/1kI-gTAKghfcwI7YzKhqRv2ExUstcHqADIWA4-TZ387o/edit#heading=h.8v16cioi7qxp") +@scenarios.tracing_config_nondefault +@features.tracing_configuration_consistency +class Test_Config_LogInjection_Enabled: + """Verify log injection behavior when enabled""" + + def setup_log_injection_enabled(self): + self.message = "msg" + self.r = weblog.get("/log/library", params={"msg": self.message}) + + def test_log_injection_enabled(self): + assert self.r.status_code == 200 + pattern = r'"dd":\{[^}]*\}' + stdout.assert_presence(pattern) + dd = parse_log_injection_message(self.message) + required_fields = ["trace_id", "span_id", "service", "version", "env"] + for field in required_fields: + assert field in dd, f"Missing field: {field}" + return + + +@rfc("https://docs.google.com/document/d/1kI-gTAKghfcwI7YzKhqRv2ExUstcHqADIWA4-TZ387o/edit#heading=h.8v16cioi7qxp") +@scenarios.default +@features.tracing_configuration_consistency +class Test_Config_LogInjection_Default: + """Verify log injection is disabled by default""" + + def setup_log_injection_default(self): + self.message = "msg" + self.r = weblog.get("/log/library", params={"msg": self.message}) + + def test_log_injection_default(self): + assert self.r.status_code == 200 + pattern = r'"dd":\{[^}]*\}' + stdout.assert_absence(pattern) + + +@rfc("https://docs.google.com/document/d/1kI-gTAKghfcwI7YzKhqRv2ExUstcHqADIWA4-TZ387o/edit#heading=h.8v16cioi7qxp") +@scenarios.tracing_config_nondefault +@features.tracing_configuration_consistency +class Test_Config_LogInjection_128Bit_TradeId_Default: + """Verify trace IDs are logged in 128bit format when log injection is enabled""" + + def setup_log_injection_128bit_traceid_default(self): + self.message = "msg" + self.r = weblog.get("/log/library", params={"msg": self.message}) + + def test_log_injection_128bit_traceid_default(self): + assert self.r.status_code == 200 + pattern = r'"dd":\{[^}]*\}' + stdout.assert_presence(pattern) + dd = parse_log_injection_message(self.message) + trace_id = dd.get("trace_id") + assert re.match(r"^[0-9a-f]{32}$", trace_id), f"Invalid 128-bit trace_id: {trace_id}" + + +@rfc("https://docs.google.com/document/d/1kI-gTAKghfcwI7YzKhqRv2ExUstcHqADIWA4-TZ387o/edit#heading=h.8v16cioi7qxp") +@scenarios.tracing_config_nondefault_3 +@features.tracing_configuration_consistency +class Test_Config_LogInjection_128Bit_TradeId_Disabled: + """Verify 128 bit traceid are disabled in log injection when DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED=false""" + + def setup_log_injection_128bit_traceid_disabled(self): + self.message = "msg" + self.r = weblog.get("/log/library", params={"msg": self.message}) + + def test_log_injection_128bit_traceid_disabled(self): + assert self.r.status_code == 200 + pattern = r'"dd":\{[^}]*\}' + stdout.assert_presence(pattern) + dd = parse_log_injection_message(self.message) + trace_id = dd.get("trace_id") + assert re.match(r"^\d{1,20}$", trace_id), f"Invalid 64-bit trace_id: {trace_id}" + + +@rfc("https://docs.google.com/document/d/1kI-gTAKghfcwI7YzKhqRv2ExUstcHqADIWA4-TZ387o/edit#heading=h.8v16cioi7qxp") +@scenarios.runtime_metrics_enabled +@features.tracing_configuration_consistency +class Test_Config_RuntimeMetrics_Enabled: + """Verify runtime metrics are enabled when DD_RUNTIME_METRICS_ENABLED=true""" + + # This test verifies runtime metrics by asserting the prescene of a metric in the dogstatsd endpoint + def test_config_runtimemetrics_enabled(self): + for data in interfaces.library.get_data("/dogstatsd/v2/proxy"): + lines = data["request"]["content"].split("\n") + metric_found = False + for line in lines: + if runtime_metrics[context.library.library] in line: + metric_found = True + break + assert metric_found, f"The metric {runtime_metrics[context.library.library]} was not found in any line" + break + + +@rfc("https://docs.google.com/document/d/1kI-gTAKghfcwI7YzKhqRv2ExUstcHqADIWA4-TZ387o/edit#heading=h.8v16cioi7qxp") +@scenarios.default +@features.tracing_configuration_consistency +class Test_Config_RuntimeMetrics_Default: + """Verify runtime metrics are disabled by default""" + + # test that by default runtime metrics are disabled + def test_config_runtimemetrics_default(self): + iterations = 0 + for data in interfaces.library.get_data("/dogstatsd/v2/proxy"): + iterations += 1 + assert iterations == 0, "Runtime metrics are enabled by default" + + +# Parse the JSON-formatted log message from stdout and return the 'dd' object +def parse_log_injection_message(log_message): + for data in stdout.get_data(): + try: + message = json.loads(data.get("message")) + except json.JSONDecodeError: + continue + if message.get("dd") and message.get(log_injection_fields[context.library.library]["message"]) == log_message: + dd = message.get("dd") + return dd diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index e24a7bd6a36..bf1da0d6432 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -489,6 +489,7 @@ class _Scenarios: "DD_TRACE_KAFKAJS_ENABLED": "false", # In Node the integration is kafkajs. "DD_TRACE_PDO_ENABLED": "false", # Use PDO for PHP, "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "tracecontext,datadog,b3multi", + "DD_LOGS_INJECTION": "true", }, appsec_enabled=False, # disable ASM to test non asm client ip tagging iast_enabled=False, @@ -519,6 +520,8 @@ class _Scenarios: weblog_env={ "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING": "false", "DD_TRACE_CLIENT_IP_HEADER": "custom-ip-header", + "DD_LOGS_INJECTION": "true", + "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": "false", }, appsec_enabled=False, doc="", @@ -775,6 +778,12 @@ class _Scenarios: ipv6 = IPV6Scenario("IPV6") + runtime_metrics_enabled = EndToEndScenario( + "RUNTIME_METRICS_ENABLED", + runtime_metrics_enabled=True, + doc="Test runtime metrics", + ) + scenarios = _Scenarios() diff --git a/utils/_context/_scenarios/endtoend.py b/utils/_context/_scenarios/endtoend.py index 48762e89405..2c2c9bcdb89 100644 --- a/utils/_context/_scenarios/endtoend.py +++ b/utils/_context/_scenarios/endtoend.py @@ -246,6 +246,7 @@ def __init__( rc_api_enabled=False, meta_structs_disabled=False, span_events=True, + runtime_metrics_enabled=False, backend_interface_timeout=0, include_postgres_db=False, include_cassandra_db=False, @@ -313,6 +314,7 @@ def __init__( tracer_sampling_rate=tracer_sampling_rate, appsec_enabled=appsec_enabled, iast_enabled=iast_enabled, + runtime_metrics_enabled=runtime_metrics_enabled, additional_trace_header_tags=additional_trace_header_tags, use_proxy=use_proxy_for_weblog, volumes=weblog_volumes, diff --git a/utils/_context/containers.py b/utils/_context/containers.py index ea2aeac26af..1500d9d7167 100644 --- a/utils/_context/containers.py +++ b/utils/_context/containers.py @@ -689,6 +689,7 @@ def __init__( tracer_sampling_rate=None, appsec_enabled=True, iast_enabled=True, + runtime_metrics_enabled=False, additional_trace_header_tags=(), use_proxy=True, volumes=None, @@ -723,6 +724,9 @@ def __init__( base_environment["_DD_TELEMETRY_METRICS_ENABLED"] = "true" base_environment["DD_TELEMETRY_METRICS_INTERVAL_SECONDS"] = self.telemetry_heartbeat_interval + if runtime_metrics_enabled: + base_environment["DD_RUNTIME_METRICS_ENABLED"] = "true" + if appsec_enabled: base_environment["DD_APPSEC_ENABLED"] = "true" base_environment["DD_APPSEC_WAF_TIMEOUT"] = "10000000" # 10 seconds diff --git a/utils/build/docker/nodejs/express/app.js b/utils/build/docker/nodejs/express/app.js index 4bcdf6560d7..ab78de85c08 100644 --- a/utils/build/docker/nodejs/express/app.js +++ b/utils/build/docker/nodejs/express/app.js @@ -11,6 +11,7 @@ const axios = require('axios') const fs = require('fs') const passport = require('passport') const crypto = require('crypto') +const pino = require('pino') const iast = require('./iast') const dsm = require('./dsm') @@ -33,6 +34,8 @@ const { sqsProduce, sqsConsume } = require('./integrations/messaging/aws/sqs') const { kafkaProduce, kafkaConsume } = require('./integrations/messaging/kafka/kafka') const { rabbitmqProduce, rabbitmqConsume } = require('./integrations/messaging/rabbitmq/rabbitmq') +const logger = pino() + iast.initData().catch(() => {}) app.use(require('body-parser').json()) @@ -239,6 +242,21 @@ app.get('/kafka/consume', (req, res) => { }) }) +app.get('/log/library', (req, res) => { + const msg = req.query.msg || 'msg' + switch (req.query.level) { + case 'warn': + logger.warn(msg) + break + case 'error': + logger.error(msg) + break + default: + logger.info(msg) + } + res.send('OK') +}) + app.get('/sqs/produce', (req, res) => { const queue = req.query.queue const message = req.query.message diff --git a/utils/build/docker/nodejs/express/package-lock.json b/utils/build/docker/nodejs/express/package-lock.json index 8567380318d..32cf5b06e4b 100644 --- a/utils/build/docker/nodejs/express/package-lock.json +++ b/utils/build/docker/nodejs/express/package-lock.json @@ -28,6 +28,7 @@ "passport-http": "^0.3.0", "passport-local": "^1.0.0", "pg": "^8.8.0", + "pino": "^9.5.0", "pug": "^3.0.3", "semver": "^7.6.3", "sinon": "^17.0.1" @@ -3804,6 +3805,106 @@ "split2": "^4.1.0" } }, + "node_modules/pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino/node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/pino/node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pino/node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pino/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" + }, + "node_modules/pino/node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/pino/node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/pino/node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/pino/node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -8076,6 +8177,90 @@ "split2": "^4.1.0" } }, + "pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "dependencies": { + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, + "fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" + }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + }, + "pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "requires": { + "split2": "^4.0.0" + } + }, + "pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, + "process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" + }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" + }, + "sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, + "thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "requires": { + "real-require": "^0.2.0" + } + } + } + }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", diff --git a/utils/build/docker/nodejs/express/package.json b/utils/build/docker/nodejs/express/package.json index ee8a15eb346..df490866aee 100644 --- a/utils/build/docker/nodejs/express/package.json +++ b/utils/build/docker/nodejs/express/package.json @@ -29,6 +29,7 @@ "passport-http": "^0.3.0", "passport-local": "^1.0.0", "pg": "^8.8.0", + "pino": "^9.5.0", "pug": "^3.0.3", "semver": "^7.6.3", "sinon": "^17.0.1" diff --git a/utils/build/docker/nodejs/parametric/server.js b/utils/build/docker/nodejs/parametric/server.js index e55b18fe7a9..42ada2a89bc 100644 --- a/utils/build/docker/nodejs/parametric/server.js +++ b/utils/build/docker/nodejs/parametric/server.js @@ -321,6 +321,7 @@ app.post('/trace/otel/set_attributes', (req, res) => { app.get('/trace/config', (req, res) => { const dummyTracer = require('dd-trace').init() const config = dummyTracer._tracer._config + const agentUrl = dummyTracer._tracer?._url || config?.url res.json( { config: { 'dd_service': config?.service !== undefined ? `${config.service}`.toLowerCase() : 'null', @@ -335,7 +336,7 @@ app.get('/trace/config', (req, res) => { 'dd_trace_otel_enabled': 'null', // not exposed in config object in node 'dd_env': config?.tags?.env !== undefined ? `${config.tags.env}` : 'null', 'dd_version': config?.tags?.version !== undefined ? `${config.tags.version}` : 'null', - 'dd_trace_agent_url': config?.url !== undefined ? `${config.url.href}` : 'null', + 'dd_trace_agent_url': agentUrl !== undefined ? agentUrl.toString() : 'null', 'dd_trace_rate_limit': config?.sampler?.rateLimit !== undefined ? `${config?.sampler?.rateLimit}` : 'null', 'dd_dogstatsd_host': config?.dogstatsd?.hostname !== undefined ? `${config.dogstatsd.hostname}` : 'null', 'dd_dogstatsd_port': config?.dogstatsd?.port !== undefined ? `${config.dogstatsd.port}` : 'null', diff --git a/utils/proxy/_deserializer.py b/utils/proxy/_deserializer.py index 8afdbc44d26..eb0f494c1fd 100644 --- a/utils/proxy/_deserializer.py +++ b/utils/proxy/_deserializer.py @@ -120,6 +120,10 @@ def json_load(): return json_load() + if path == "/dogstatsd/v2/proxy" and interface == "library": + # TODO : how to deserialize this ? + return content.decode(encoding="utf-8") + if interface == "library" and path == "/info": if key == "response": return json_load() From 25f6ed0e3648f14c947e0f7bab3cb7c3ea0249ff Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sat, 18 Jan 2025 07:02:03 +0100 Subject: [PATCH 03/17] Add missing DI PII redaction token: appkey (#3824) --- tests/debugger/test_debugger_pii.py | 9 +++++---- utils/build/docker/dotnet/weblog/Models/Debugger/Pii.cs | 9 +++++---- .../datadoghq/system_tests/springboot/debugger/Pii.java | 9 +++++---- .../datadoghq/system_tests/springboot/debugger/Pii.java | 9 +++++---- utils/build/docker/python/flask/debugger/pii.py | 9 +++++---- utils/build/docker/ruby/rails70/app/models/pii.rb | 9 +++++---- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/tests/debugger/test_debugger_pii.py b/tests/debugger/test_debugger_pii.py index 04fc2263622..d5e599e78f6 100644 --- a/tests/debugger/test_debugger_pii.py +++ b/tests/debugger/test_debugger_pii.py @@ -15,16 +15,17 @@ REDACTED_KEYS = [ "_2fa", - "accesstoken", - "access_token", + "ACCESSTOKEN", "Access_Token", - "accessToken", "AccessToken", - "ACCESSTOKEN", + "accessToken", + "access_token", + "accesstoken", "aiohttpsession", "apikey", "apisecret", "apisignature", + "appkey", "applicationkey", "auth", "authorization", diff --git a/utils/build/docker/dotnet/weblog/Models/Debugger/Pii.cs b/utils/build/docker/dotnet/weblog/Models/Debugger/Pii.cs index b2ec1a4178c..3b38521c9ff 100644 --- a/utils/build/docker/dotnet/weblog/Models/Debugger/Pii.cs +++ b/utils/build/docker/dotnet/weblog/Models/Debugger/Pii.cs @@ -11,16 +11,17 @@ public abstract class PiiBase public class Pii : PiiBase { public string? _2fa { get; set; } = PiiBase.Value; - public string? accesstoken { get; set; } = PiiBase.Value; - public string? access_token { get; set; } = PiiBase.Value; + public string? ACCESSTOKEN { get; set; } = PiiBase.Value; public string? Access_Token { get; set; } = PiiBase.Value; - public string? accessToken { get; set; } = PiiBase.Value; public string? AccessToken { get; set; } = PiiBase.Value; - public string? ACCESSTOKEN { get; set; } = PiiBase.Value; + public string? accessToken { get; set; } = PiiBase.Value; + public string? access_token { get; set; } = PiiBase.Value; + public string? accesstoken { get; set; } = PiiBase.Value; public string? aiohttpsession { get; set; } = PiiBase.Value; public string? apikey { get; set; } = PiiBase.Value; public string? apisecret { get; set; } = PiiBase.Value; public string? apisignature { get; set; } = PiiBase.Value; + public string? appkey { get; set; } = PiiBase.Value; public string? applicationkey { get; set; } = PiiBase.Value; public string? auth { get; set; } = PiiBase.Value; public string? authorization { get; set; } = PiiBase.Value; diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java index 98cbc91bbde..dcdda1c7b5e 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java @@ -2,16 +2,17 @@ public class Pii extends PiiBase { public String _2fa = VALUE; - public String accesstoken = VALUE; - public String access_token = VALUE; + public String ACCESSTOKEN = VALUE; public String Access_Token = VALUE; - public String accessToken = VALUE; public String AccessToken = VALUE; - public String ACCESSTOKEN = VALUE; + public String access_token = VALUE; + public String accesstoken = VALUE; + public String accessToken = VALUE; public String aiohttpsession = VALUE; public String apikey = VALUE; public String apisecret = VALUE; public String apisignature = VALUE; + public String appkey = VALUE; public String applicationkey = VALUE; public String auth = VALUE; public String authorization = VALUE; diff --git a/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java b/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java index 98cbc91bbde..2c4150141ba 100644 --- a/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java +++ b/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/Pii.java @@ -2,16 +2,17 @@ public class Pii extends PiiBase { public String _2fa = VALUE; - public String accesstoken = VALUE; - public String access_token = VALUE; + public String ACCESSTOKEN = VALUE; + public String AccessToken = VALUE; public String Access_Token = VALUE; public String accessToken = VALUE; - public String AccessToken = VALUE; - public String ACCESSTOKEN = VALUE; + public String access_token = VALUE; + public String accesstoken = VALUE; public String aiohttpsession = VALUE; public String apikey = VALUE; public String apisecret = VALUE; public String apisignature = VALUE; + public String appkey = VALUE; public String applicationkey = VALUE; public String auth = VALUE; public String authorization = VALUE; diff --git a/utils/build/docker/python/flask/debugger/pii.py b/utils/build/docker/python/flask/debugger/pii.py index 39c6d982c38..152b87f6efc 100644 --- a/utils/build/docker/python/flask/debugger/pii.py +++ b/utils/build/docker/python/flask/debugger/pii.py @@ -9,16 +9,17 @@ class Pii(PiiBase): def __init__(self): super().__init__() self._2fa = self.VALUE - self.accesstoken = self.VALUE - self.access_token = self.VALUE + self.ACCESSTOKEN = self.VALUE + self.AccessToken = self.VALUE self.Access_Token = self.VALUE self.accessToken = self.VALUE - self.AccessToken = self.VALUE - self.ACCESSTOKEN = self.VALUE + self.access_token = self.VALUE + self.accesstoken = self.VALUE self.aiohttpsession = self.VALUE self.apikey = self.VALUE self.apisecret = self.VALUE self.apisignature = self.VALUE + self.appkey = self.VALUE self.applicationkey = self.VALUE self.auth = self.VALUE self.authorization = self.VALUE diff --git a/utils/build/docker/ruby/rails70/app/models/pii.rb b/utils/build/docker/ruby/rails70/app/models/pii.rb index fc303a2c0b9..b2048c94355 100644 --- a/utils/build/docker/ruby/rails70/app/models/pii.rb +++ b/utils/build/docker/ruby/rails70/app/models/pii.rb @@ -3,16 +3,17 @@ REDACTED_KEYS = [ "_2fa", - "accesstoken", - "access_token", + "ACCESSTOKEN", + "AccessToken", "Access_Token", "accessToken", - "AccessToken", - "ACCESSTOKEN", + "access_token", + "accesstoken", "aiohttpsession", "apikey", "apisecret", "apisignature", + "appkey", "applicationkey", "auth", "authorization", From 71931d19144850312d3733a6f9d0da8bdc4ec6bf Mon Sep 17 00:00:00 2001 From: Eliott Bouhana <47679741+eliottness@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:19:01 +0000 Subject: [PATCH 04/17] [golang] appsec: meta_struct Security Events (#3845) Signed-off-by: Eliott Bouhana --- manifests/golang.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manifests/golang.yml b/manifests/golang.yml index 7deb02009a5..492e23ee6f4 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -375,10 +375,10 @@ tests/: Test_Standardization: missing_feature Test_StandardizationBlockMode: missing_feature test_metastruct.py: - Test_SecurityEvents_Appsec_Metastruct_Disabled: irrelevant (no fallback will be implemented) - Test_SecurityEvents_Appsec_Metastruct_Enabled: missing_feature - Test_SecurityEvents_Iast_Metastruct_Disabled: irrelevant (no fallback will be implemented) - Test_SecurityEvents_Iast_Metastruct_Enabled: missing_feature + Test_SecurityEvents_Appsec_Metastruct_Disabled: v1.72.0-dev + Test_SecurityEvents_Appsec_Metastruct_Enabled: v1.72.0-dev + Test_SecurityEvents_Iast_Metastruct_Disabled: irrelevant (No IAST) + Test_SecurityEvents_Iast_Metastruct_Enabled: irrelevant (No IAST) test_remote_config_rule_changes.py: Test_BlockingActionChangesWithRemoteConfig: v1.69.0 Test_UpdateRuleFileWithRemoteConfig: bug (APPSEC-55377) From 43a8a428fc3043b03af108f53e881e0b5105b066 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 20 Jan 2025 12:16:19 +0100 Subject: [PATCH 05/17] Ensure DI PII line probe tests actually run for all tracers (#3823) --- manifests/nodejs.yml | 4 +- tests/debugger/probes/pii_line.json | 6 +- tests/debugger/test_debugger_pii.py | 10 +- tests/debugger/utils.py | 2 +- .../weblog/Controllers/DebuggerController.cs | 2 +- .../debugger/DebuggerController.java | 4 +- .../debugger/DebuggerController.java | 4 +- utils/build/docker/nodejs/express/app.js | 2 +- .../docker/nodejs/express/debugger/index.js | 64 ++++++++++- .../nodejs/express/debugger/log_handler.js | 21 ---- .../docker/nodejs/express/debugger/pii.js | 108 ++++++++++++++++++ .../docker/nodejs/express4-typescript/app.ts | 3 - .../express4-typescript/debugger/index.ts | 6 - .../debugger/log_handler.ts | 21 ---- .../python/flask/debugger_controller.py | 5 +- .../app/controllers/debugger_controller.rb | 30 +++-- 16 files changed, 211 insertions(+), 81 deletions(-) delete mode 100644 utils/build/docker/nodejs/express/debugger/log_handler.js create mode 100644 utils/build/docker/nodejs/express/debugger/pii.js delete mode 100644 utils/build/docker/nodejs/express4-typescript/debugger/index.ts delete mode 100644 utils/build/docker/nodejs/express4-typescript/debugger/log_handler.ts diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index dd6ade61ef4..9e6f775f3b4 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -628,15 +628,13 @@ tests/: test_debugger_pii.py: Test_Debugger_PII_Redaction: "*": irrelevant - express4: v5.31.0 - express4-typescript: v5.31.0 + express4: v5.32.0 test_debugger_probe_snapshot.py: Test_Debugger_Probe_Snaphots: missing_feature (feature not implented) test_debugger_probe_status.py: Test_Debugger_Probe_Statuses: "*": irrelevant express4: v5.32.0 - express4-typescript: v5.32.0 integrations/: crossed_integrations/: test_kafka.py: diff --git a/tests/debugger/probes/pii_line.json b/tests/debugger/probes/pii_line.json index a1280788b67..cfd7a0cd9b9 100644 --- a/tests/debugger/probes/pii_line.json +++ b/tests/debugger/probes/pii_line.json @@ -5,10 +5,10 @@ "id": "log170aa-acda-4453-9111-1478a600line", "version": 0, "where": { - "typeName": null, - "sourceFile": "ACTUAL_SOURCE_FILE", + "typeName": null, + "sourceFile": "ACTUAL_SOURCE_FILE", "lines": [ - "33" + "64" ] }, "captureSnapshot": true, diff --git a/tests/debugger/test_debugger_pii.py b/tests/debugger/test_debugger_pii.py index d5e599e78f6..794db7ca540 100644 --- a/tests/debugger/test_debugger_pii.py +++ b/tests/debugger/test_debugger_pii.py @@ -138,7 +138,8 @@ def _assert(self, redacted_keys, redacted_types, line_probe=False): self.assert_all_weblog_responses_ok() self._validate_pii_keyword_redaction(redacted_keys, line_probe) - self._validate_pii_type_redaction(redacted_types, line_probe) + if context.library != "nodejs": # Node.js does not support type redacting + self._validate_pii_type_redaction(redacted_types, line_probe) def _validate_pii_keyword_redaction(self, should_redact_field_names, line_probe): not_redacted = [] @@ -150,7 +151,7 @@ def _validate_pii_keyword_redaction(self, should_redact_field_names, line_probe) for field_name in should_redact_field_names: if line_probe: - fields = snapshot["captures"]["lines"]["33"]["locals"]["pii"]["fields"] + fields = snapshot["captures"]["lines"]["64"]["locals"]["pii"]["fields"] else: fields = snapshot["captures"]["return"]["locals"]["pii"]["fields"] @@ -186,7 +187,7 @@ def _validate_pii_type_redaction(self, should_redact_types, line_probe): for type_name in should_redact_types: if line_probe: - type_info = snapshot["captures"]["lines"]["33"]["locals"][type_name] + type_info = snapshot["captures"]["lines"]["64"]["locals"][type_name] else: type_info = snapshot["captures"]["return"]["locals"][type_name] @@ -219,9 +220,6 @@ def test_pii_redaction_method_full(self): def setup_pii_redaction_line_full(self): self._setup(line_probe=True) - @missing_feature( - context.library != "ruby", reason="Ruby DI does not provide the functionality required for the test." - ) def test_pii_redaction_line_full(self): self._assert(REDACTED_KEYS, REDACTED_TYPES, line_probe=True) diff --git a/tests/debugger/utils.py b/tests/debugger/utils.py index f3eeba8a3bb..82cf802ce79 100644 --- a/tests/debugger/utils.py +++ b/tests/debugger/utils.py @@ -134,7 +134,7 @@ def __get_probe_type(probe_id): elif language == "ruby": probe["where"]["sourceFile"] = "debugger_controller.rb" elif language == "nodejs": - probe["where"]["sourceFile"] = "debugger/log_handler.js" + probe["where"]["sourceFile"] = "debugger/index.js" probe["type"] = __get_probe_type(probe["id"]) return probes diff --git a/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs b/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs index b5b69a212b5..4f733e34393 100644 --- a/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs +++ b/utils/build/docker/dotnet/weblog/Controllers/DebuggerController.cs @@ -61,7 +61,7 @@ public async Task Pii() PiiBase? customPii = await Task.FromResult(new CustomPii()); var value = pii?.TestValue; var customValue = customPii?.TestValue; - return Content($"PII {value}. CustomPII {customValue}"); + return Content($"PII {value}. CustomPII {customValue}"); // must be line 64 } [HttpGet("expression")] diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java index af0196a8a3f..af61f16c5f2 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java @@ -53,13 +53,15 @@ public String mixProbe(@PathVariable String arg, @PathVariable int intArg) { return "Mixed result " + intMixLocal; } +// Dummy line +// Dummy line @GetMapping("/pii") public String pii() { PiiBase pii = new Pii(); PiiBase customPii = new CustomPii(); String value = pii.TestValue; String customValue = customPii.TestValue; - return "PII " + value + ". CustomPII" + customValue; + return "PII " + value + ". CustomPII" + customValue; // must be line 64 } @GetMapping("/expression") diff --git a/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java b/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java index 70bb616645c..ec71b075a19 100644 --- a/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java +++ b/utils/build/docker/java_otel/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/debugger/DebuggerController.java @@ -53,13 +53,15 @@ public String mixProbe(@PathVariable String arg, @PathVariable int intArg) { return "Mixed result " + intMixLocal; } +// Dummy line +// Dummy line @GetMapping("/pii") public String pii() { PiiBase pii = new Pii(); PiiBase customPii = new CustomPii(); String value = pii.TestValue; String customValue = customPii.TestValue; - return "PII " + value + ". CustomPII" + customValue; + return "PII " + value + ". CustomPII" + customValue; // must be line 64 } @GetMapping("/expression") diff --git a/utils/build/docker/nodejs/express/app.js b/utils/build/docker/nodejs/express/app.js index ab78de85c08..2ac3da27283 100644 --- a/utils/build/docker/nodejs/express/app.js +++ b/utils/build/docker/nodejs/express/app.js @@ -456,7 +456,7 @@ app.get('/createextraservice', (req, res) => { iast.initRoutes(app, tracer) -di.initRoutes(app, tracer) +di.initRoutes(app) require('./auth')(app, passport, tracer) diff --git a/utils/build/docker/nodejs/express/debugger/index.js b/utils/build/docker/nodejs/express/debugger/index.js index 1a292fce771..8c6c425b076 100644 --- a/utils/build/docker/nodejs/express/debugger/index.js +++ b/utils/build/docker/nodejs/express/debugger/index.js @@ -1,9 +1,67 @@ 'use strict' -const logHandler = require('./log_handler') +const Pii = require('./pii') module.exports = { - initRoutes (app, tracer) { - app.get('/log', logHandler) + initRoutes (app) { + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + + app.get('/debugger/log', (req, res) => { + res.send('Log probe') // This needs to be line 20 + }) + + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + // Padding + + app.get('/debugger/pii', (req, res) => { + const pii = new Pii() // eslint-disable-line no-unused-vars + res.send('Hello World') // This needs to be line 64 + }) } } diff --git a/utils/build/docker/nodejs/express/debugger/log_handler.js b/utils/build/docker/nodejs/express/debugger/log_handler.js deleted file mode 100644 index 5bf9dfbaad5..00000000000 --- a/utils/build/docker/nodejs/express/debugger/log_handler.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding - -module.exports = function logHandler (req, res) { - res.send('Log probe') // This needs to be line 20 -} diff --git a/utils/build/docker/nodejs/express/debugger/pii.js b/utils/build/docker/nodejs/express/debugger/pii.js new file mode 100644 index 00000000000..fcd56c55106 --- /dev/null +++ b/utils/build/docker/nodejs/express/debugger/pii.js @@ -0,0 +1,108 @@ +'use strict' + +const REDACTED_KEYS = [ + '_2fa', + 'ACCESSTOKEN', + 'AccessToken', + 'Access_Token', + 'accessToken', + 'access_token', + 'accesstoken', + 'aiohttpsession', + 'apikey', + 'apisecret', + 'apisignature', + 'appkey', + 'applicationkey', + 'auth', + 'authorization', + 'authtoken', + 'ccnumber', + 'certificatepin', + 'cipher', + 'clientid', + 'clientsecret', + 'connectionstring', + 'connectsid', + 'cookie', + 'credentials', + 'creditcard', + 'csrf', + 'csrftoken', + 'cvv', + 'databaseurl', + 'dburl', + 'encryptionkey', + 'encryptionkeyid', + 'geolocation', + 'gpgkey', + 'ipaddress', + 'jti', + 'jwt', + 'licensekey', + 'masterkey', + 'mysqlpwd', + 'nonce', + 'oauth', + 'oauthtoken', + 'otp', + 'passhash', + 'passwd', + 'password', + 'passwordb', + 'pemfile', + 'pgpkey', + 'phpsessid', + 'pin', + 'pincode', + 'pkcs8', + 'privatekey', + 'publickey', + 'pwd', + 'recaptchakey', + 'refreshtoken', + 'routingnumber', + 'salt', + 'secret', + 'secretkey', + 'secrettoken', + 'securityanswer', + 'securitycode', + 'securityquestion', + 'serviceaccountcredentials', + 'session', + 'sessionid', + 'sessionkey', + 'setcookie', + 'signature', + 'signaturekey', + 'sshkey', + 'ssn', + 'symfony', + 'token', + 'transactionid', + 'twiliotoken', + 'usersession', + 'voterid', + 'xapikey', + 'xauthtoken', + 'xcsrftoken', + 'xforwardedfor', + 'xrealip', + 'xsrf', + 'xsrftoken', + 'customidentifier1', + 'customidentifier2' +] + +const VALUE = 'SHOULD_BE_REDACTED' + +class Pii { + constructor () { + REDACTED_KEYS.forEach((key) => { + this[key] = VALUE + }) + } +} + +module.exports = Pii diff --git a/utils/build/docker/nodejs/express4-typescript/app.ts b/utils/build/docker/nodejs/express4-typescript/app.ts index e688f05f4f5..9dc6356aaa8 100644 --- a/utils/build/docker/nodejs/express4-typescript/app.ts +++ b/utils/build/docker/nodejs/express4-typescript/app.ts @@ -16,7 +16,6 @@ const multer = require('multer') const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } }) const iast = require('./iast') -const di = require('./debugger') iast.initData().catch(() => {}) @@ -30,8 +29,6 @@ iast.initMiddlewares(app) require('./auth')(app, passport, tracer) iast.initRoutes(app) -di.initRoutes(app) - app.get('/', (req: Request, res: Response) => { console.log('Received a request'); res.send('Hello\n'); diff --git a/utils/build/docker/nodejs/express4-typescript/debugger/index.ts b/utils/build/docker/nodejs/express4-typescript/debugger/index.ts deleted file mode 100644 index a85e40b511d..00000000000 --- a/utils/build/docker/nodejs/express4-typescript/debugger/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Express } from 'express' -import { logHandler } from './log_handler' - -export function initRoutes (app: Express) { - app.get('/log', logHandler) -} diff --git a/utils/build/docker/nodejs/express4-typescript/debugger/log_handler.ts b/utils/build/docker/nodejs/express4-typescript/debugger/log_handler.ts deleted file mode 100644 index 4add4049f19..00000000000 --- a/utils/build/docker/nodejs/express4-typescript/debugger/log_handler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Request, Response } from 'express' - -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding -// Padding - -export function logHandler (req: Request, res: Response) { - res.send('Log probe') // This needs to be line 20 -} diff --git a/utils/build/docker/python/flask/debugger_controller.py b/utils/build/docker/python/flask/debugger_controller.py index 6c89e592cd5..b6160daf7ff 100644 --- a/utils/build/docker/python/flask/debugger_controller.py +++ b/utils/build/docker/python/flask/debugger_controller.py @@ -52,13 +52,16 @@ def mix_probe(arg, intArg): return f"Mixed result {intMixLocal}" +# dummy line +# dummy line +# dummy line @debugger_blueprint.route("/pii", methods=["GET"]) def pii(): pii = Pii() customPii = CustomPii() value = pii.test_value custom_value = customPii.test_value - return f"PII {value}. CustomPII {custom_value}" + return f"PII {value}. CustomPII {custom_value}" # must be line 64 @debugger_blueprint.route("/expression", methods=["GET"]) diff --git a/utils/build/docker/ruby/rails70/app/controllers/debugger_controller.rb b/utils/build/docker/ruby/rails70/app/controllers/debugger_controller.rb index 9e5120d79c0..677bd1295a8 100644 --- a/utils/build/docker/ruby/rails70/app/controllers/debugger_controller.rb +++ b/utils/build/docker/ruby/rails70/app/controllers/debugger_controller.rb @@ -24,15 +24,15 @@ def log_probe # Padding # Padding # Padding - - def pii - pii = Pii.new - customPii = CustomPii.new - value = pii.test_value - custom_value = customPii.test_value - render inline: "PII #{value}. CustomPII #{custom_value}" # must be line 33 - end - + # Padding + # Padding + # Padding + # Padding + # Padding + # Padding + # Padding + # Padding + # Padding # Padding # Padding # Padding @@ -51,4 +51,16 @@ def mix_probe value = params[:string_arg].length * Integer(params[:int_arg]) render inline: "Mixed result #{value}" # must be line 52 end + + # Padding + # Padding + # Padding + + def pii + pii = Pii.new + customPii = CustomPii.new + value = pii.test_value + custom_value = customPii.test_value + render inline: "PII #{value}. CustomPII #{custom_value}" # must be line 64 + end end From 04276e61b375a71962e2e4ac58ec7ffd894a95f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez=20Garc=C3=ADa?= Date: Mon, 20 Jan 2025 13:23:16 +0100 Subject: [PATCH 06/17] [JAVA]Mark as flaky IAST cookie test_telemetry_metric_executed_sink for vertx4 (#3853) Motivation Although they are marked as easy wins, it has been confirmed that the tests are flaky Changes Mark as flaky: tests/appsec/iast/sink/test_insecure_cookie.py::TestInsecureCookie::test_telemetry_metric_executed_sink tests/appsec/iast/sink/test_no_httponly_cookie.py::TestNoHttponlyCookie::test_telemetry_metric_executed_sink tests/appsec/iast/sink/test_no_samesite_cookie.py::TestNoSamesiteCookie::test_telemetry_metric_executed_sink --- tests/appsec/iast/sink/test_insecure_cookie.py | 4 ++-- tests/appsec/iast/sink/test_no_httponly_cookie.py | 4 ++-- tests/appsec/iast/sink/test_no_samesite_cookie.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/appsec/iast/sink/test_insecure_cookie.py b/tests/appsec/iast/sink/test_insecure_cookie.py index ba5aff1d1d2..09f750aff7b 100644 --- a/tests/appsec/iast/sink/test_insecure_cookie.py +++ b/tests/appsec/iast/sink/test_insecure_cookie.py @@ -2,7 +2,7 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2021 Datadog, Inc. -from utils import context, missing_feature, bug, weblog, features, rfc, scenarios +from utils import context, missing_feature, bug, weblog, features, rfc, scenarios, flaky from ..utils import BaseSinkTest, BaseTestCookieNameFilter, validate_stack_traces @@ -36,7 +36,7 @@ def test_telemetry_metric_instrumented_sink(self): super().test_telemetry_metric_instrumented_sink() @missing_feature(context.library < "java@1.22.0", reason="Metrics not implemented") - @missing_feature(weblog_variant="vertx4", reason="Metrics not implemented") + @flaky(weblog_variant="vertx4", reason="APPSEC-56453") def test_telemetry_metric_executed_sink(self): super().test_telemetry_metric_executed_sink() diff --git a/tests/appsec/iast/sink/test_no_httponly_cookie.py b/tests/appsec/iast/sink/test_no_httponly_cookie.py index ceb566b7542..819c921e370 100644 --- a/tests/appsec/iast/sink/test_no_httponly_cookie.py +++ b/tests/appsec/iast/sink/test_no_httponly_cookie.py @@ -2,7 +2,7 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2021 Datadog, Inc. -from utils import context, missing_feature, bug, weblog, features, rfc, scenarios +from utils import context, missing_feature, bug, weblog, features, rfc, scenarios, flaky from ..utils import BaseSinkTest, BaseTestCookieNameFilter, validate_stack_traces @@ -36,7 +36,7 @@ def test_telemetry_metric_instrumented_sink(self): super().test_telemetry_metric_instrumented_sink() @missing_feature(context.library < "java@1.22.0", reason="Metric not implemented") - @missing_feature(weblog_variant="vertx4", reason="Metric not implemented") + @flaky(weblog_variant="vertx4", reason="APPSEC-56453") def test_telemetry_metric_executed_sink(self): super().test_telemetry_metric_executed_sink() diff --git a/tests/appsec/iast/sink/test_no_samesite_cookie.py b/tests/appsec/iast/sink/test_no_samesite_cookie.py index 3a737f08708..28ccd6387cf 100644 --- a/tests/appsec/iast/sink/test_no_samesite_cookie.py +++ b/tests/appsec/iast/sink/test_no_samesite_cookie.py @@ -2,7 +2,7 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2021 Datadog, Inc. -from utils import context, missing_feature, bug, weblog, features, rfc, scenarios +from utils import context, missing_feature, bug, weblog, features, rfc, scenarios, flaky from ..utils import BaseSinkTest, BaseTestCookieNameFilter, validate_stack_traces @@ -36,7 +36,7 @@ def test_telemetry_metric_instrumented_sink(self): super().test_telemetry_metric_instrumented_sink() @missing_feature(context.library < "java@1.22.0", reason="Metrics not implemented") - @missing_feature(weblog_variant="vertx4", reason="Metrics not implemented") + @flaky(weblog_variant="vertx4", reason="APPSEC-56453") def test_telemetry_metric_executed_sink(self): super().test_telemetry_metric_executed_sink() From 2067ebf773be4a6973a64a60d7dadee1995015a2 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 20 Jan 2025 13:36:55 +0100 Subject: [PATCH 07/17] Deserialize JSON in multipart (#3854) --- utils/proxy/_deserializer.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/utils/proxy/_deserializer.py b/utils/proxy/_deserializer.py index eb0f494c1fd..3b0a682a59f 100644 --- a/utils/proxy/_deserializer.py +++ b/utils/proxy/_deserializer.py @@ -247,15 +247,27 @@ def _deserialize_file_in_multipart_form_data( item["system-tests-error"] = "Filename not found in content-disposition, please contact #apm-shared-testing" else: filename = meta_data["filename"].strip('"') + item["system-tests-filename"] = filename + if filename.lower().endswith(".gz"): filename = filename[:-3] - file_path = f"{export_content_files_to}/{md5(content).hexdigest()}_{filename}" - with open(file_path, "wb") as f: - f.write(content) + content_is_deserialized = False + if filename.lower().endswith(".json"): + try: + item["content"] = json.loads(content) + content_is_deserialized = True + except json.JSONDecodeError: + item["system-tests-error"] = "Can't decode json file" + + if not content_is_deserialized: + file_path = f"{export_content_files_to}/{md5(content).hexdigest()}_{filename}" + + item["system-tests-information"] = "File exported to a separated file" + item["system-tests-file-path"] = file_path - item["system-tests-information"] = "File exported to a separated file" - item["system-tests-file-path"] = file_path + with open(file_path, "wb") as f: + f.write(content) def _deserialized_nested_json_from_trace_payloads(content, interface): From b5e686a768885ad76307aaf5fef3b2a4d3c9d31e Mon Sep 17 00:00:00 2001 From: Robert Pickering Date: Mon, 20 Jan 2025 13:31:04 +0000 Subject: [PATCH 08/17] PHP quick wins (#3848) --- manifests/php.yml | 92 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/manifests/php.yml b/manifests/php.yml index 48cc7e607b5..f37678ba4dd 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -7,8 +7,8 @@ tests/: appsec/: api_security/: test_api_security_rc.py: - Test_API_Security_RC_ASM_DD_processors: missing_feature - Test_API_Security_RC_ASM_DD_scanners: missing_feature + Test_API_Security_RC_ASM_DD_processors: v1.6.2 + Test_API_Security_RC_ASM_DD_scanners: v1.6.2 Test_API_Security_RC_ASM_processor_overrides_and_custom_scanner: irrelevant (waf does not support it yet) test_apisec_sampling.py: missing_feature test_schemas.py: @@ -20,7 +20,7 @@ tests/: Test_Schema_Request_Path_Parameters: missing_feature Test_Schema_Request_Query_Parameters: v0.94.0 Test_Schema_Response_Body: v0.99.1 - Test_Schema_Response_Body_env_var: missing_feature + Test_Schema_Response_Body_env_var: v1.6.2 Test_Schema_Response_Headers: v0.94.0 iast/: sink/: @@ -150,11 +150,69 @@ tests/: test_security_controls.py: TestSecurityControls: missing_feature rasp/: - test_cmdi.py: missing_feature - test_lfi.py: missing_feature - test_shi.py: missing_feature - test_sqli.py: missing_feature - test_ssrf.py: missing_feature + test_cmdi.py: + Test_Cmdi_BodyJson: missing_feature + Test_Cmdi_BodyUrlEncoded: missing_feature + Test_Cmdi_BodyXml: missing_feature + Test_Cmdi_Capability: missing_feature + Test_Cmdi_Mandatory_SpanTags: missing_feature + Test_Cmdi_Optional_SpanTags: missing_feature + Test_Cmdi_Rules_Version: v1.6.2 + Test_Cmdi_StackTrace: missing_feature + Test_Cmdi_Telemetry: missing_feature + Test_Cmdi_Telemetry_Variant_Tag: missing_feature + Test_Cmdi_UrlQuery: missing_feature + Test_Cmdi_Waf_Version: v1.6.2 + test_lfi.py: + Test_Lfi_BodyJson: missing_feature + Test_Lfi_BodyUrlEncoded: missing_feature + Test_Lfi_BodyXml: missing_feature + Test_Lfi_Capability: missing_feature + Test_Lfi_Mandatory_SpanTags: missing_feature + Test_Lfi_Optional_SpanTags: missing_feature + Test_Lfi_RC_CustomAction: missing_feature + Test_Lfi_Rules_Version: v1.6.2 + Test_Lfi_StackTrace: missing_feature + Test_Lfi_Telemetry: missing_feature + Test_Lfi_UrlQuery: missing_feature + Test_Lfi_Waf_Version: v1.6.2 + test_shi.py: + Test_Shi_BodyJson: missing_feature + Test_Shi_BodyUrlEncoded: missing_feature + Test_Shi_BodyXml: missing_feature + Test_Shi_Capability: missing_feature + Test_Shi_Mandatory_SpanTags: missing_feature + Test_Shi_Optional_SpanTags: missing_feature + Test_Shi_Rules_Version: v1.6.2 + Test_Shi_StackTrace: missing_feature + Test_Shi_Telemetry: missing_feature + Test_Shi_Telemetry_Variant_Tag: missing_feature + Test_Shi_UrlQuery: missing_feature + Test_Shi_Waf_Version: v1.6.2 + test_sqli.py: + Test_Sqli_BodyJson: missing_feature + Test_Sqli_BodyUrlEncoded: missing_feature + Test_Sqli_BodyXml: missing_feature + Test_Sqli_Capability: missing_feature + Test_Sqli_Mandatory_SpanTags: missing_feature + Test_Sqli_Optional_SpanTags: missing_feature + Test_Sqli_Rules_Version: v1.6.2 + Test_Sqli_StackTrace: missing_feature + Test_Sqli_Telemetry: missing_feature + Test_Sqli_UrlQuery: missing_feature + Test_Sqli_Waf_Version: v1.6.2 + test_ssrf.py: + Test_Ssrf_BodyJson: missing_feature + Test_Ssrf_BodyUrlEncoded: missing_feature + Test_Ssrf_BodyXml: missing_feature + Test_Ssrf_Capability: missing_feature + Test_Ssrf_Mandatory_SpanTags: missing_feature + Test_Ssrf_Optional_SpanTags: missing_feature + Test_Ssrf_Rules_Version: v1.6.2 + Test_Ssrf_StackTrace: missing_feature + Test_Ssrf_Telemetry: missing_feature + Test_Ssrf_UrlQuery: missing_feature + Test_Ssrf_Waf_Version: v1.6.2 waf/: test_addresses.py: Test_BodyJson: v0.98.1 # TODO what is the earliest version? @@ -223,16 +281,16 @@ tests/: test_blocking_addresses.py: Test_BlockingGraphqlResolvers: missing_feature Test_Blocking_request_body: irrelevant (Php does not accept url encoded entries without key) - Test_Blocking_request_body_multipart: missing_feature + Test_Blocking_request_body_multipart: v1.6.2 Test_Blocking_response_headers: irrelevant (On php it is not possible change the status code once its header is sent) Test_Blocking_response_status: irrelevant (On php it is not possible change the status code once its header is sent) - Test_Suspicious_Request_Blocking: missing_feature (v0.86.0 but test is not implemented) + Test_Suspicious_Request_Blocking: v1.6.2 test_client_ip.py: Test_StandardTagsClientIp: v0.81.0 test_fingerprinting.py: - Test_Fingerprinting_Endpoint: missing_feature + Test_Fingerprinting_Endpoint: v1.6.2 Test_Fingerprinting_Endpoint_Capability: missing_feature - Test_Fingerprinting_Header_And_Network: missing_feature + Test_Fingerprinting_Header_And_Network: v1.6.2 Test_Fingerprinting_Header_Capability: missing_feature Test_Fingerprinting_Network_Capability: missing_feature Test_Fingerprinting_Session: missing_feature @@ -247,16 +305,16 @@ tests/: Test_SecurityEvents_Iast_Metastruct_Disabled: irrelevant (no fallback will be implemented) Test_SecurityEvents_Iast_Metastruct_Enabled: missing_feature test_remote_config_rule_changes.py: - Test_BlockingActionChangesWithRemoteConfig: missing_feature - Test_UpdateRuleFileWithRemoteConfig: missing_feature (v0.8.0 but lacks telemetry support) + Test_BlockingActionChangesWithRemoteConfig: v1.6.2 + Test_UpdateRuleFileWithRemoteConfig: v1.6.2 test_reports.py: Test_ExtraTagsFromRule: v0.88.0 Test_Info: v0.68.3 # probably 0.68.2, but was flaky test_request_blocking.py: - Test_AppSecRequestBlocking: missing_feature # missing version + Test_AppSecRequestBlocking: v1.6.2 test_runtime_activation.py: - Test_RuntimeActivation: missing_feature # missing version - Test_RuntimeDeactivation: missing_feature # missing version + Test_RuntimeActivation: v1.6.2 + Test_RuntimeDeactivation: v1.6.2 test_shell_execution.py: Test_ShellExecution: v0.95.0 test_suspicious_attacker_blocking.py: From 94b4a79b2272b9c138a57bec8ba5a9742fee030e Mon Sep 17 00:00:00 2001 From: ishabi Date: Mon, 20 Jan 2025 14:42:16 +0100 Subject: [PATCH 09/17] add missing iast stacktrace config (#3857) --- tests/telemetry_intake/static/config_norm_rules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/telemetry_intake/static/config_norm_rules.json b/tests/telemetry_intake/static/config_norm_rules.json index 2a4afb9a4e4..ea117da534e 100644 --- a/tests/telemetry_intake/static/config_norm_rules.json +++ b/tests/telemetry_intake/static/config_norm_rules.json @@ -123,7 +123,7 @@ "DD_IAST_REDACTION_VALUE_PATTERN": "iast_redaction_value_pattern", "DD_IAST_REGEXP_TIMEOUT": "iast_regexp_timeout", "DD_IAST_REQUEST_SAMPLING": "iast_request_sampling_percentage", - "DD_IAST_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", + "DD_IAST_STACK_TRACE_ENABLED": "iast_stack_trace_enabled", "DD_IAST_TELEMETRY_VERBOSITY": "iast_telemetry_verbosity", "DD_IAST_TRUNCATION_MAX_VALUE_LENGTH": "iast_truncation_max_value_length", "DD_IAST_VULNERABILITIES_PER_REQUEST": "iast_vulnerability_per_request", @@ -510,6 +510,7 @@ "iast.requestSampling": "iast_request_sampling", "iast.telemetryVerbosity": "iast_telemetry_verbosity", "iast.vulnerabilities-per-request": "iast_vulnerability_per_request", + "iast.stackTrace.enabled": "iast_stack_trace_enabled", "ignite.cache.include_keys": "ignite_cache_include_keys_enabled", "inferredProxyServicesEnabled": "inferred_proxy_services_enabled", "inject_force": "ssi_forced_injection_enabled", From c8163b74d3ce10acb037f356e3ae871eac86c31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez=20Garc=C3=ADa?= Date: Mon, 20 Jan 2025 14:44:15 +0100 Subject: [PATCH 10/17] [JAVA] Mark as irrelevant test_login_event_blocking_auto_id (#3855) Motivation Although they are marked as easy wins, is irrelevant as blocking by user ID not available in java Changes Mark as irrelevant tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Blocking::test_login_event_blocking_auto_id --- tests/appsec/test_automated_login_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/appsec/test_automated_login_events.py b/tests/appsec/test_automated_login_events.py index d996beda3e7..714d2c20507 100644 --- a/tests/appsec/test_automated_login_events.py +++ b/tests/appsec/test_automated_login_events.py @@ -1928,6 +1928,7 @@ def setup_login_event_blocking_auto_id(self): self.config_state_3 = rc.rc_state.set_config(*BLOCK_USER_ID).apply() self.r_login_blocked = weblog.post("/login?auth=local", data=login_data(context, USER, PASSWORD)) + @irrelevant(context.library == "java", reason="Blocking by user ID not available in java") def test_login_event_blocking_auto_id(self): assert self.config_state_1[rc.RC_STATE] == rc.ApplyState.ACKNOWLEDGED assert self.r_login.status_code == 200 From 963c97f95a9b47a12da1a42b6298aae6b7c4df86 Mon Sep 17 00:00:00 2001 From: Roberto Montero <108007532+robertomonteromiguel@users.noreply.github.com> Date: Mon, 20 Jan 2025 14:53:51 +0100 Subject: [PATCH 11/17] K8s Lib Injection: Add operator scenario (using real agent) (#3850) --- .gitlab/k8s_gitlab-ci.yml | 15 ++- .../test_k8s_lib_injection.py | 21 ++++ tests/k8s_lib_injection/utils.py | 7 +- utils/_context/_scenarios/__init__.py | 6 ++ .../_context/_scenarios/k8s_lib_injection.py | 45 ++++++--- utils/k8s_lib_injection/k8s_command_utils.py | 40 +++----- .../k8s_datadog_kubernetes.py | 98 +++++++++++++------ utils/k8s_lib_injection/k8s_weblog.py | 5 +- .../datadog-helm-chart-values-uds.yaml} | 0 .../datadog-helm-chart-values.yaml} | 0 .../resources/operator/datadog-operator.yaml | 26 +++++ .../operator/scripts/path_clusterrole.sh | 3 - 12 files changed, 185 insertions(+), 81 deletions(-) rename utils/k8s_lib_injection/resources/{operator/operator-helm-values-uds.yaml => helm/datadog-helm-chart-values-uds.yaml} (100%) rename utils/k8s_lib_injection/resources/{operator/operator-helm-values.yaml => helm/datadog-helm-chart-values.yaml} (100%) create mode 100644 utils/k8s_lib_injection/resources/operator/datadog-operator.yaml delete mode 100755 utils/k8s_lib_injection/resources/operator/scripts/path_clusterrole.sh diff --git a/.gitlab/k8s_gitlab-ci.yml b/.gitlab/k8s_gitlab-ci.yml index 50c56e2f08c..aa04f6c6ec3 100644 --- a/.gitlab/k8s_gitlab-ci.yml +++ b/.gitlab/k8s_gitlab-ci.yml @@ -69,6 +69,11 @@ configure_env: --with-decryption --query "Parameter.Value" --out text) - echo "FP_IMPORT_URL=${FP_IMPORT_URL}" >> fpd.env - echo "FP_API_KEY=${FP_API_KEY}" >> fpd.env + - export DD_API_KEY_ONBOARDING=$(aws ssm get-parameter --region us-east-1 --name ci.${CI_PROJECT_NAME}.dd-api-key-onboarding --with-decryption --query "Parameter.Value" --out text) + - export DD_APP_KEY_ONBOARDING=$(aws ssm get-parameter --region us-east-1 --name ci.${CI_PROJECT_NAME}.dd-app-key-onboarding --with-decryption --query "Parameter.Value" --out text) + - echo "DD_API_KEY_ONBOARDING=${DD_API_KEY_ONBOARDING}" >> fpd.env + - echo "DD_APP_KEY_ONBOARDING=${DD_APP_KEY_ONBOARDING}" >> fpd.env + artifacts: reports: dotenv: fpd.env @@ -86,7 +91,7 @@ k8s_java: K8S_CLUSTER_VERSION: ['7.57.0', '7.59.0'] - K8S_WEBLOG: [dd-lib-java-init-test-app] K8S_WEBLOG_IMG: [ghcr.io/datadog/system-tests/dd-lib-java-init-test-app:latest] - K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE] + K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE, K8S_LIB_INJECTION_OPERATOR] K8S_LIB_INIT_IMG: ["gcr.io/datadoghq/dd-lib-java-init:latest", "ghcr.io/datadog/dd-trace-java/dd-lib-java-init:latest_snapshot"] K8S_CLUSTER_VERSION: ['7.56.2', '7.57.0', '7.59.0'] @@ -98,7 +103,7 @@ k8s_dotnet: matrix: - K8S_WEBLOG: [dd-lib-dotnet-init-test-app] K8S_WEBLOG_IMG: [ghcr.io/datadog/system-tests/dd-lib-dotnet-init-test-app:latest] - K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE] + K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE, K8S_LIB_INJECTION_OPERATOR] K8S_LIB_INIT_IMG: ["gcr.io/datadoghq/dd-lib-dotnet-init:latest", "ghcr.io/datadog/dd-trace-dotnet/dd-lib-dotnet-init:latest_snapshot"] K8S_CLUSTER_VERSION: ['7.56.2', '7.57.0', '7.59.0'] @@ -110,7 +115,7 @@ k8s_nodejs: matrix: - K8S_WEBLOG: [sample-app] K8S_WEBLOG_IMG: [ghcr.io/datadog/system-tests/sample-app:latest] - K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE] + K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE, K8S_LIB_INJECTION_OPERATOR] K8S_LIB_INIT_IMG: ["gcr.io/datadoghq/dd-lib-js-init:latest", "ghcr.io/datadog/dd-trace-js/dd-lib-js-init:latest_snapshot"] K8S_CLUSTER_VERSION: ['7.56.2', '7.57.0', '7.59.0'] @@ -122,7 +127,7 @@ k8s_python: matrix: - K8S_WEBLOG: [dd-lib-python-init-test-django] K8S_WEBLOG_IMG: [ghcr.io/datadog/system-tests/dd-lib-python-init-test-django:latest] - K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE] + K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE, K8S_LIB_INJECTION_OPERATOR] K8S_LIB_INIT_IMG: ["gcr.io/datadoghq/dd-lib-python-init:latest", "ghcr.io/datadog/dd-trace-py/dd-lib-python-init:latest_snapshot"] K8S_CLUSTER_VERSION: ['7.56.2', '7.57.0', '7.59.0'] - K8S_WEBLOG: [dd-lib-python-init-test-django-gunicorn] @@ -164,7 +169,7 @@ k8s_ruby: matrix: - K8S_WEBLOG: [dd-lib-ruby-init-test-rails] K8S_WEBLOG_IMG: [ghcr.io/datadog/system-tests/dd-lib-ruby-init-test-rails:latest] - K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE] + K8S_SCENARIO: [K8S_LIB_INJECTION, K8S_LIB_INJECTION_UDS, K8S_LIB_INJECTION_NO_AC, K8S_LIB_INJECTION_NO_AC_UDS, K8S_LIB_INJECTION_PROFILING_DISABLED, K8S_LIB_INJECTION_PROFILING_ENABLED, K8S_LIB_INJECTION_PROFILING_OVERRIDE, K8S_LIB_INJECTION_OPERATOR] K8S_LIB_INIT_IMG: ["gcr.io/datadoghq/dd-lib-ruby-init:latest", "ghcr.io/datadog/dd-trace-rb/dd-lib-ruby-init:latest_snapshot"] K8S_CLUSTER_VERSION: ['7.56.2', '7.57.0', '7.59.0'] - K8S_WEBLOG: [dd-lib-ruby-init-test-rails-explicit] diff --git a/tests/k8s_lib_injection/test_k8s_lib_injection.py b/tests/k8s_lib_injection/test_k8s_lib_injection.py index c7111596e33..fb84112b6b7 100644 --- a/tests/k8s_lib_injection/test_k8s_lib_injection.py +++ b/tests/k8s_lib_injection/test_k8s_lib_injection.py @@ -1,5 +1,9 @@ from utils import scenarios, features, context from tests.k8s_lib_injection.utils import get_dev_agent_traces +from utils.tools import logger +from utils.onboarding.weblog_interface import make_get_request, warmup_weblog +from utils.onboarding.backend_interface import wait_backend_trace_id +from utils.onboarding.wait_for_tcp_port import wait_for_port class _TestK8sLibInjection: @@ -40,3 +44,20 @@ class TestK8sLibInjectionNoAC_UDS(_TestK8sLibInjection): """Test K8s lib injection without admission controller and UDS""" pass + + +@features.k8s_admission_controller +@scenarios.k8s_lib_injection_operator +class TestK8sLibInjection_operator(_TestK8sLibInjection): + """Test K8s lib injection using the operator""" + + def test_k8s_lib_injection(self): + cluster_info = context.scenario.k8s_cluster_provider.get_cluster_info() + context_url = f"http://{cluster_info.cluster_host_name}:{cluster_info.get_weblog_port()}/" + logger.info(f"Waiting for weblog available [{cluster_info.cluster_host_name}:{cluster_info.get_weblog_port()}]") + wait_for_port(cluster_info.get_weblog_port(), cluster_info.cluster_host_name, 80.0) + logger.info(f"[{cluster_info.cluster_host_name}]: Weblog app is ready!") + warmup_weblog(context_url) + request_uuid = make_get_request(context_url) + logger.info(f"Http request done with uuid: [{request_uuid}] for ip [{cluster_info.cluster_host_name}]") + wait_backend_trace_id(request_uuid, 120.0) diff --git a/tests/k8s_lib_injection/utils.py b/tests/k8s_lib_injection/utils.py index 72d26f9de58..9c97b2f0ef1 100644 --- a/tests/k8s_lib_injection/utils.py +++ b/tests/k8s_lib_injection/utils.py @@ -6,11 +6,10 @@ def get_dev_agent_traces(k8s_cluster_info, retry=10): """get_dev_agent_traces fetches traces from the dev agent running in the k8s cluster.""" + dev_agent_url = f"http://{k8s_cluster_info.cluster_host_name}:{k8s_cluster_info.get_agent_port()}/test/traces" for _ in range(retry): - logger.info(f"[Check traces] Checking traces:") - response = requests.get( - f"http://{k8s_cluster_info.cluster_host_name}:{k8s_cluster_info.get_agent_port()}/test/traces" - ) + logger.info(f"[Check traces] Checking traces : {dev_agent_url}") + response = requests.get(dev_agent_url) traces_json = response.json() if len(traces_json) > 0: logger.debug(f"Test traces response: {traces_json}") diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index bf1da0d6432..d4df361b854 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -703,6 +703,12 @@ class _Scenarios: ) k8s_lib_injection = K8sScenario("K8S_LIB_INJECTION", doc="Kubernetes lib injection with admission controller") + k8s_lib_injection_operator = K8sScenario( + "K8S_LIB_INJECTION_OPERATOR", + doc="Use CRD Datadog Operator (uses real agent). Not configure the admission controller, the operator does it", + with_datadog_operator=True, + with_admission_controller=False, + ) k8s_lib_injection_uds = K8sScenario( "K8S_LIB_INJECTION_UDS", doc="Kubernetes lib injection with admission controller and uds", diff --git a/utils/_context/_scenarios/k8s_lib_injection.py b/utils/_context/_scenarios/k8s_lib_injection.py index 98f3a8387a1..52cbf581027 100644 --- a/utils/_context/_scenarios/k8s_lib_injection.py +++ b/utils/_context/_scenarios/k8s_lib_injection.py @@ -33,6 +33,7 @@ def __init__( with_admission_controller=True, weblog_env={}, dd_cluster_feature={}, + with_datadog_operator=False, ) -> None: super().__init__( name, @@ -42,10 +43,19 @@ def __init__( ) self.use_uds = use_uds self.with_admission_controller = with_admission_controller + self.with_datadog_operator = with_datadog_operator self.weblog_env = weblog_env self.dd_cluster_feature = dd_cluster_feature def configure(self, config): + # If we are using the datadog operator, we don't need to deploy the test agent + # But we'll use the real agent deployed automatically by the operator + # We'll use the real backend, we need the real api key and app key + if self.with_datadog_operator: + assert os.getenv("DD_API_KEY_ONBOARDING") is not None, "DD_API_KEY_ONBOARDING is not set" + assert os.getenv("DD_APP_KEY_ONBOARDING") is not None, "DD_APP_KEY_ONBOARDING is not set" + self._api_key = os.getenv("DD_API_KEY_ONBOARDING") + self._app_key = os.getenv("DD_APP_KEY_ONBOARDING") # These are the tested components: dd_cluser_agent_version, weblog image, library_init_version self.k8s_weblog = config.option.k8s_weblog self.k8s_weblog_img = config.option.k8s_weblog_img @@ -66,15 +76,17 @@ def configure(self, config): # is it on sleep mode? self._sleep_mode = config.option.sleep - # Prepare kubernetes datadog (manages the dd_cluster_agent and test_agent) and the weblog handler - self.test_agent = K8sDatadog(self.host_log_folder) - self.test_agent.configure( + # Prepare kubernetes datadog (manages the dd_cluster_agent and test_agent or the operator) + self.k8s_datadog = K8sDatadog(self.host_log_folder) + self.k8s_datadog.configure( self.k8s_cluster_provider.get_cluster_info(), dd_cluster_feature=self.dd_cluster_feature, dd_cluster_uds=self.use_uds, dd_cluster_version=self.k8s_cluster_version, + api_key=self._api_key if self.with_datadog_operator else None, + app_key=self._app_key if self.with_datadog_operator else None, ) - + # Weblog handler self.test_weblog = K8sWeblog( self.k8s_weblog_img, self.library.library, self.k8s_lib_init_img, self.host_log_folder ) @@ -109,12 +121,17 @@ def get_warmups(self): warmups = super().get_warmups() warmups.append(lambda: logger.terminal.write_sep("=", "Starting Kubernetes Kind Cluster", bold=True)) warmups.append(self.k8s_cluster_provider.ensure_cluster) - warmups.append(self.test_agent.deploy_test_agent) + if self.with_admission_controller: - warmups.append(self.test_agent.deploy_datadog_cluster_agent) - warmups.append(self.test_weblog.install_weblog_pod_with_admission_controller) + warmups.append(self.k8s_datadog.deploy_test_agent) + warmups.append(self.k8s_datadog.deploy_datadog_cluster_agent) + warmups.append(self.test_weblog.install_weblog_pod) + elif self.with_datadog_operator: + warmups.append(self.k8s_datadog.deploy_datadog_operator) + warmups.append(self.test_weblog.install_weblog_pod) else: - warmups.append(self.test_weblog.install_weblog_pod_without_admission_controller) + warmups.append(self.k8s_datadog.deploy_test_agent) + warmups.append(self.test_weblog.install_weblog_pod_with_manual_inject) return warmups def pytest_sessionfinish(self, session, exitstatus): # noqa: ARG002 @@ -126,7 +143,7 @@ def close_targets(self): self.k8s_cluster_provider.destroy_cluster() return logger.info("K8sInstance Exporting debug info") - self.test_agent.export_debug_info(namespace="default") + self.k8s_datadog.export_debug_info(namespace="default") self.test_weblog.export_debug_info(namespace="default") logger.info("Destroying cluster") self.k8s_cluster_provider.destroy_cluster() @@ -169,8 +186,8 @@ def configure(self, config): super().configure(config) self.weblog_env["LIB_INIT_IMAGE"] = self.k8s_lib_init_img - self.test_agent = K8sDatadog(self.host_log_folder) - self.test_agent.configure( + self.k8s_datadog = K8sDatadog(self.host_log_folder) + self.k8s_datadog.configure( self.k8s_cluster_provider.get_cluster_info(), dd_cluster_feature=self.dd_cluster_feature, dd_cluster_uds=self.use_uds, @@ -192,9 +209,9 @@ def get_warmups(self): warmups.append(lambda: logger.terminal.write_sep("=", "Starting Kubernetes Kind Cluster", bold=True)) warmups.append(self.k8s_cluster_provider.ensure_cluster) warmups.append(self.k8s_cluster_provider.create_spak_service_account) - warmups.append(self.test_agent.deploy_test_agent) - warmups.append(self.test_agent.deploy_datadog_cluster_agent) - warmups.append(self.test_weblog.install_weblog_pod_with_admission_controller) + warmups.append(self.k8s_datadog.deploy_test_agent) + warmups.append(self.k8s_datadog.deploy_datadog_cluster_agent) + warmups.append(self.test_weblog.install_weblog_pod) return warmups diff --git a/utils/k8s_lib_injection/k8s_command_utils.py b/utils/k8s_lib_injection/k8s_command_utils.py index 29723190165..efa0df94580 100644 --- a/utils/k8s_lib_injection/k8s_command_utils.py +++ b/utils/k8s_lib_injection/k8s_command_utils.py @@ -12,7 +12,7 @@ def execute_command(command, timeout=None, logfile=None, subprocess_env=None, qu if timeout is not None: applied_timeout = timeout - logger.debug(f"Launching Command: {_clean_secrets(command)} ") + logger.debug(f"Launching Command: {command} ") command_out_redirect = subprocess.PIPE if logfile: command_out_redirect = open(logfile, "w") @@ -38,40 +38,26 @@ def execute_command(command, timeout=None, logfile=None, subprocess_env=None, qu return None else: # if we specify a timeout, we raise an exception - raise Exception(f"Command: {_clean_secrets(command)} timed out after {applied_timeout} seconds") + raise Exception(f"Command: {command} timed out after {applied_timeout} seconds") if not logfile: output = process.stdout.read() output = str(output, "utf-8") if not quiet: - logger.debug(f"Command: {_clean_secrets(command)} \n {_clean_secrets(output)}") + logger.debug(f"Command: {command} \n {output}") else: - logger.info(f"Command: {_clean_secrets(command)}") + logger.info(f"Command: {command}") if process.returncode != 0: output_error = process.stderr.read() - logger.debug(f"Command: {_clean_secrets(command)} \n {_clean_secrets(output_error)}") - raise Exception(f"Error executing command: {_clean_secrets(command)} \n {_clean_secrets(output)}") + logger.debug(f"Command: {command} \n {output_error}") + raise Exception(f"Error executing command: {command} \n {output}") except Exception as ex: - logger.error(f"Error executing command: {_clean_secrets(command)} \n {ex}") + logger.error(f"Error executing command: {command} \n {ex}") raise ex return output -def _clean_secrets(data_to_clean): - """Clean secrets from the output.""" - if ( - hasattr(context.scenario, "api_key") - and context.scenario.api_key - and hasattr(context.scenario, "app_key") - and context.scenario.app_key - ): - data_to_clean = data_to_clean.replace(context.scenario.api_key, "DD_API_KEY").replace( - context.scenario.app_key, "DD_APP_KEY" - ) - return data_to_clean - - @retry(delay=1, tries=5) def helm_add_repo(name, url, k8s_cluster_info, update=False): logger.info(f"Adding helm repo {name} with url {url} for cluster {k8s_cluster_info.cluster_name}") @@ -81,7 +67,7 @@ def helm_add_repo(name, url, k8s_cluster_info, update=False): @retry(delay=1, tries=5) -def helm_install_chart(k8s_cluster_info, name, chart, set_dict={}, value_file=None, upgrade=False): +def helm_install_chart(k8s_cluster_info, name, chart, set_dict={}, value_file=None, upgrade=False, timeout=90): # Copy and replace cluster name in the value file custom_value_file = None if value_file: @@ -101,12 +87,16 @@ def helm_install_chart(k8s_cluster_info, name, chart, set_dict={}, value_file=No for key, value in set_dict.items(): set_str += f" --set {key}={value}" - command = f"helm install {name} --debug --wait {set_str} {chart}" + wait = "--wait" + if timeout == 0 or timeout is None: + wait = "" + + command = f"helm install {name} --debug {wait} {set_str} {chart}" if upgrade: - command = f"helm upgrade {name} --debug --install --wait {set_str} {chart}" + command = f"helm upgrade {name} --debug --install {wait} {set_str} {chart}" if custom_value_file: command = f"helm install {name} {set_str} --debug -f {custom_value_file} {chart}" if upgrade: command = f"helm upgrade {name} {set_str} --debug --install -f {custom_value_file} {chart}" execute_command("kubectl config current-context") - execute_command(command, timeout=90, quiet=True) # Too many traces to show in the logs + execute_command(command, timeout=timeout, quiet=False) # Too many traces to show in the logs diff --git a/utils/k8s_lib_injection/k8s_datadog_kubernetes.py b/utils/k8s_lib_injection/k8s_datadog_kubernetes.py index 09350c757eb..94e7cbb583d 100644 --- a/utils/k8s_lib_injection/k8s_datadog_kubernetes.py +++ b/utils/k8s_lib_injection/k8s_datadog_kubernetes.py @@ -15,11 +15,21 @@ class K8sDatadog: def __init__(self, output_folder): self.output_folder = output_folder - def configure(self, k8s_cluster_info, dd_cluster_feature={}, dd_cluster_uds=None, dd_cluster_version=None): + def configure( + self, + k8s_cluster_info, + dd_cluster_feature={}, + dd_cluster_uds=None, + dd_cluster_version=None, + api_key=None, + app_key=None, + ): self.k8s_cluster_info = k8s_cluster_info self.dd_cluster_feature = dd_cluster_feature self.dd_cluster_uds = dd_cluster_uds self.dd_cluster_version = dd_cluster_version + self.api_key = api_key + self.app_key = app_key logger.info(f"K8sDatadog configured with cluster: {self.k8s_cluster_info.cluster_name}") def deploy_test_agent(self, namespace="default"): @@ -93,14 +103,17 @@ def deploy_test_agent(self, namespace="default"): def deploy_datadog_cluster_agent(self, namespace="default"): """Installs the Datadog Cluster Agent via helm for manual library injection testing. - It returns when the Cluster Agent pod is ready. + We enable the admission controller and wait for the datdog cluster to be ready. + The Datadog Admission Controller is an important piece of the Datadog Cluster Agent. + The main benefit of the Datadog Admission Controller is to simplify your life when it comes to configure your application Pods. + Datadog Admission Controller is a Mutating Admission Controller type because it mutates, or changes, the pods configurations. """ logger.info("[Deploy datadog cluster] Deploying Datadog Cluster Agent with Admission Controler") - operator_file = "utils/k8s_lib_injection/resources/operator/operator-helm-values.yaml" + operator_file = "utils/k8s_lib_injection/resources/helm/datadog-helm-chart-values.yaml" if self.dd_cluster_uds: logger.info("[Deploy datadog cluster] Using UDS") - operator_file = "utils/k8s_lib_injection/resources/operator/operator-helm-values-uds.yaml" + operator_file = "utils/k8s_lib_injection/resources/helm/datadog-helm-chart-values-uds.yaml" logger.info("[Deploy datadog cluster] Configuring helm repository") helm_add_repo("datadog", "https://helm.datadoghq.com", self.k8s_cluster_info, update=True) @@ -118,7 +131,32 @@ def deploy_datadog_cluster_agent(self, namespace="default"): ) logger.info("[Deploy datadog cluster] Waiting for the cluster to be ready") - self._wait_for_operator_ready(namespace) + self._wait_for_cluster_agent_ready(namespace) + + def deploy_datadog_operator(self, namespace="default"): + """Datadog Operator is a Kubernetes Operator that enables you to deploy and configure the Datadog Agent in a Kubernetes environment. + By using the Datadog Operator, you can use a single Custom Resource Definition (CRD) to deploy the node-based Agent, + the Datadog Cluster Agent, and Cluster check runners. + """ + logger.info("[Deploy datadog operator] Configuring helm repository") + helm_add_repo("datadog", "https://helm.datadoghq.com", self.k8s_cluster_info, update=True) + helm_install_chart( + self.k8s_cluster_info, + "my-datadog-operator", + "datadog/datadog-operator", + value_file=None, + set_dict={}, + timeout=None, + ) + logger.info("[Deploy datadog operator] the operator is ready") + logger.info("[Deploy datadog operator] Create the operator secrets") + execute_command( + f"kubectl create secret generic datadog-secret --from-literal api-key={self.api_key} --from-literal app-key={self.app_key}" + ) + logger.info("[Deploy datadog operator] Create the operator custom resource") + execute_command(f"kubectl apply -f utils/k8s_lib_injection/resources/operator/datadog-operator.yaml") + logger.info("[Deploy datadog operator] Waiting for the cluster to be ready") + self._wait_for_cluster_agent_ready(namespace, label_selector="agent.datadoghq.com/component=cluster-agent") def wait_for_test_agent(self, namespace): """Waits for the test agent to be ready.""" @@ -158,19 +196,20 @@ def list_namespaced_pod(self, namespace, **kwargs): """Necessary to retry the list_namespaced_pod call in case of error (used by watch stream)""" return self.k8s_cluster_info.core_v1_api().list_namespaced_pod(namespace, **kwargs) - def _wait_for_operator_ready(self, namespace): - operator_ready = False - operator_status = None + def _wait_for_cluster_agent_ready(self, namespace, label_selector="app=datadog-cluster-agent"): + cluster_agent_ready = False + cluster_agent_status = None datadog_cluster_name = None for i in range(20): try: if datadog_cluster_name is None: pods = self.k8s_cluster_info.core_v1_api().list_namespaced_pod( - namespace, label_selector="app=datadog-cluster-agent" + namespace, label_selector=label_selector ) datadog_cluster_name = pods.items[0].metadata.name if pods and len(pods.items) > 0 else None - operator_status = ( + logger.info(f"[status cluster agent] Cluster agent name: {datadog_cluster_name}") + cluster_agent_status = ( self.k8s_cluster_info.core_v1_api().read_namespaced_pod_status( name=datadog_cluster_name, namespace=namespace ) @@ -178,31 +217,32 @@ def _wait_for_operator_ready(self, namespace): else None ) if ( - operator_status - and operator_status.status.phase == "Running" - and operator_status.status.container_statuses[0].ready == True + cluster_agent_status + and cluster_agent_status.status.phase == "Running" + and cluster_agent_status.status.container_statuses[0].ready == True ): - logger.info("[Deploy operator] Operator datadog running!") - operator_ready = True + logger.info("[sattus cluster agent] Cluster agent datadog running!") + cluster_agent_ready = True break except Exception as e: - logger.info(f"Error waiting for operator: {e}") + logger.info(f"Error waiting for cluster agent: {e}") + datadog_cluster_name = None time.sleep(5) - if not operator_ready: - logger.error("Operator not created. Last status: %s" % operator_status) + if not cluster_agent_ready: + logger.error("Cluster agent not created. Last status: %s" % cluster_agent_status) if datadog_cluster_name: - operator_logs = self.k8s_cluster_info.core_v1_api().read_namespaced_pod_log( + cluster_agent_logs = self.k8s_cluster_info.core_v1_api().read_namespaced_pod_log( name=datadog_cluster_name, namespace=namespace ) - logger.error(f"Operator logs: {operator_logs}") - raise Exception("Operator not created") - # At this point the operator should be ready, we are going to wait a little bit more - # to make sure the operator is ready (some times the operator is ready but the cluster agent is not ready yet) + logger.error(f"Cluster agent logs: {cluster_agent_logs}") + raise Exception("Cluster agent not created") + # At this point the cluster_agent should be ready, we are going to wait a little bit more + # to make sure the cluster_agent is ready (some times the cluster_agent is ready but the cluster agent is not ready yet) time.sleep(5) def export_debug_info(self, namespace): - """Exports debug information for the test agent and the operator. + """Exports debug information for the test agent and the cluster_agent. We shouldn't raise any exception here, we just log the errors. """ @@ -224,12 +264,14 @@ def export_debug_info(self, namespace): for deployment in deployments.items: k8s_logger(self.output_folder, "get.deployments").info(deployment) - # Daemonset describe - api_response = self.k8s_cluster_info.apps_api().read_namespaced_daemon_set(name="datadog", namespace=namespace) - k8s_logger(self.output_folder, "daemon.set.describe").info(api_response) - # Cluster logs, admission controller try: + # Daemonset describe + api_response = self.k8s_cluster_info.apps_api().read_namespaced_daemon_set( + name="datadog", namespace=namespace + ) + k8s_logger(self.output_folder, "daemon.set.describe").info(api_response) + pods = self.k8s_cluster_info.core_v1_api().list_namespaced_pod( namespace, label_selector="app=datadog-cluster-agent" ) diff --git a/utils/k8s_lib_injection/k8s_weblog.py b/utils/k8s_lib_injection/k8s_weblog.py index 88a0276cba1..526328afadc 100644 --- a/utils/k8s_lib_injection/k8s_weblog.py +++ b/utils/k8s_lib_injection/k8s_weblog.py @@ -111,14 +111,15 @@ def _get_base_weblog_pod(self): logger.info("[Deploy weblog] Weblog pod configuration done.") return pod_body - def install_weblog_pod_with_admission_controller(self, namespace="default"): + def install_weblog_pod(self, namespace="default"): logger.info("[Deploy weblog] Installing weblog pod using admission controller") pod_body = self._get_base_weblog_pod() self.k8s_cluster_info.core_v1_api().create_namespaced_pod(namespace=namespace, body=pod_body) logger.info("[Deploy weblog] Weblog pod using admission controller created. Waiting for it to be ready!") self.wait_for_weblog_ready_by_label_app("my-app", namespace, timeout=200) - def install_weblog_pod_without_admission_controller(self, namespace="default"): + def install_weblog_pod_with_manual_inject(self, namespace="default"): + """We do our own pod mutation to inject the library manually instead of using the admission controller""" pod_body = self._get_base_weblog_pod() pod_body.spec.init_containers = [] init_container1 = client.V1Container( diff --git a/utils/k8s_lib_injection/resources/operator/operator-helm-values-uds.yaml b/utils/k8s_lib_injection/resources/helm/datadog-helm-chart-values-uds.yaml similarity index 100% rename from utils/k8s_lib_injection/resources/operator/operator-helm-values-uds.yaml rename to utils/k8s_lib_injection/resources/helm/datadog-helm-chart-values-uds.yaml diff --git a/utils/k8s_lib_injection/resources/operator/operator-helm-values.yaml b/utils/k8s_lib_injection/resources/helm/datadog-helm-chart-values.yaml similarity index 100% rename from utils/k8s_lib_injection/resources/operator/operator-helm-values.yaml rename to utils/k8s_lib_injection/resources/helm/datadog-helm-chart-values.yaml diff --git a/utils/k8s_lib_injection/resources/operator/datadog-operator.yaml b/utils/k8s_lib_injection/resources/operator/datadog-operator.yaml new file mode 100644 index 00000000000..47dd40392cf --- /dev/null +++ b/utils/k8s_lib_injection/resources/operator/datadog-operator.yaml @@ -0,0 +1,26 @@ +apiVersion: datadoghq.com/v2alpha1 +kind: DatadogAgent +metadata: + name: datadog +spec: +#https://www.datadoghq.com/architecture/instrument-your-app-using-the-datadog-operator-and-admission-controller/ +#https://github.com/DataDog/datadog-operator/blob/main/docs/configuration.v2alpha1.md + global: + clusterName: docker-desktop + kubelet: + tlsVerify: false + tags: + - env:dev + credentials: + apiSecret: + secretName: datadog-secret + keyName: api-key + appSecret: + secretName: datadog-secret + keyName: app-key + features: + admissionController: + enabled: true + apm: + instrumentation: + enabled: true \ No newline at end of file diff --git a/utils/k8s_lib_injection/resources/operator/scripts/path_clusterrole.sh b/utils/k8s_lib_injection/resources/operator/scripts/path_clusterrole.sh deleted file mode 100755 index 1067788ea3a..00000000000 --- a/utils/k8s_lib_injection/resources/operator/scripts/path_clusterrole.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -kubectl patch clusterrole datadog-cluster-agent --type='json' -p '[{"op": "add", "path": "/rules/0", "value":{ "apiGroups": ["apps"], "resources": ["deployments"], "verbs": ["patch"]}}]' \ No newline at end of file From f21bd3819ac97d27e0ff397f90b35840b9c88db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Mon, 20 Jan 2025 15:43:09 +0100 Subject: [PATCH 12/17] [Java] Enable session blocking tests in Java (#3822) --- manifests/java.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/manifests/java.yml b/manifests/java.yml index 2666fe7a792..a0e5b2891c0 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -1071,7 +1071,20 @@ tests/: vertx3: missing_feature (login endpoints not implemented) vertx4: missing_feature (login endpoints not implemented) test_automated_user_and_session_tracking.py: - Test_Automated_Session_Blocking: missing_feature + Test_Automated_Session_Blocking: + '*': v1.46.0 + akka-http: missing_feature (session tracking not implemented) + jersey-grizzly2: missing_feature (session tracking not implemented) + play: missing_feature (session tracking not implemented) + ratpack: missing_feature (session tracking not implemented) + resteasy-netty3: missing_feature (session tracking not implemented) + spring-boot-3-native: flaky (APMAPI-979) + spring-boot-jetty: missing_feature (session tracking not implemented) + spring-boot-payara: bug (APPSEC-54985) + spring-boot-undertow: missing_feature (session tracking not implemented) + spring-boot-wildfly: missing_feature (session tracking not implemented) + vertx3: missing_feature (home endpoint does not use cookies) + vertx4: missing_feature (home endpoint does not use cookies) Test_Automated_User_Blocking: '*': v1.45.0 akka-http: missing_feature (login endpoints not implemented) From 226b51c8c86017f2215c9c138f54c63dbef416ca Mon Sep 17 00:00:00 2001 From: Ugaitz Urien Date: Mon, 20 Jan 2025 16:19:17 +0100 Subject: [PATCH 13/17] [nodejs] Use --import instead of --require in nextjs app (#3698) --- utils/build/docker/nodejs/nextjs.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/docker/nodejs/nextjs.Dockerfile b/utils/build/docker/nodejs/nextjs.Dockerfile index e9074e9af50..0a529f39ccf 100644 --- a/utils/build/docker/nodejs/nextjs.Dockerfile +++ b/utils/build/docker/nodejs/nextjs.Dockerfile @@ -26,5 +26,5 @@ ENV PORT=7777 ENV HOSTNAME=0.0.0.0 COPY utils/build/docker/nodejs/app.sh app.sh RUN printf './node_modules/.bin/next start' >> app.sh -ENV NODE_OPTIONS="--require dd-trace/init.js" +ENV NODE_OPTIONS="--import dd-trace/initialize.mjs" CMD ./app.sh From 0c630a233de294be88b4a1244d47a90146e4e5aa Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 20 Jan 2025 16:45:02 +0100 Subject: [PATCH 14/17] [nodejs] Enable DI line probe snapshot tests (#3825) --- manifests/nodejs.yml | 4 +++- tests/debugger/test_debugger_probe_snapshot.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 9e6f775f3b4..367a7af5d55 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -630,7 +630,9 @@ tests/: "*": irrelevant express4: v5.32.0 test_debugger_probe_snapshot.py: - Test_Debugger_Probe_Snaphots: missing_feature (feature not implented) + Test_Debugger_Probe_Snaphots: + "*": irrelevant + express4: v5.32.0 test_debugger_probe_status.py: Test_Debugger_Probe_Statuses: "*": irrelevant diff --git a/tests/debugger/test_debugger_probe_snapshot.py b/tests/debugger/test_debugger_probe_snapshot.py index 14c93d92788..45529d69670 100644 --- a/tests/debugger/test_debugger_probe_snapshot.py +++ b/tests/debugger/test_debugger_probe_snapshot.py @@ -49,6 +49,7 @@ def _validate_spans(self): def setup_log_method_probe_snaphots(self): self._setup("probe_snapshot_log_method", "/debugger/log") + @missing_feature(context.library == "nodejs", reason="Not yet implemented") def test_log_method_probe_snaphots(self): self._assert() self._validate_snapshots() @@ -58,6 +59,7 @@ def setup_span_method_probe_snaphots(self): self._setup("probe_snapshot_span_method", "/debugger/span") @missing_feature(context.library == "ruby", reason="Not yet implemented") + @missing_feature(context.library == "nodejs", reason="Not yet implemented") def test_span_method_probe_snaphots(self): self._assert() self._validate_spans() @@ -67,6 +69,7 @@ def setup_span_decoration_method_probe_snaphots(self): self._setup("probe_snapshot_span_decoration_method", "/debugger/span-decoration/asd/1") @missing_feature(context.library == "ruby", reason="Not yet implemented") + @missing_feature(context.library == "nodejs", reason="Not yet implemented") def test_span_decoration_method_probe_snaphots(self): self._assert() self._validate_spans() @@ -85,6 +88,7 @@ def setup_span_decoration_line_probe_snaphots(self): self._setup("probe_snapshot_span_decoration_line", "/debugger/span-decoration/asd/1") @missing_feature(context.library == "ruby", reason="Not yet implemented") + @missing_feature(context.library == "nodejs", reason="Not yet implemented") def test_span_decoration_line_probe_snaphots(self): self._assert() self._validate_spans() @@ -94,6 +98,7 @@ def test_span_decoration_line_probe_snaphots(self): def setup_mix_probe(self): self._setup("probe_snapshot_log_mixed", "/debugger/mix/asd/1") + @missing_feature(context.library == "nodejs", reason="Not yet implemented") def test_mix_probe(self): self._assert() self._validate_snapshots() From fe1a6fabf191024f02e8dc9656fba8e24db6b673 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Tue, 21 Jan 2025 08:57:21 +0100 Subject: [PATCH 15/17] [php] Disable Login Events v2 (#3860) --- manifests/php.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manifests/php.yml b/manifests/php.yml index f37678ba4dd..e29087da723 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -266,9 +266,9 @@ tests/: test_automated_login_events.py: Test_Login_Events: irrelevant (was v0.89.0 but will be replaced by V2) Test_Login_Events_Extended: irrelevant (was v0.89.0 but will be replaced by V2) - Test_V2_Login_Events: v1.3.0-dev - Test_V2_Login_Events_Anon: v1.3.0-dev - Test_V2_Login_Events_RC: missing_feature + Test_V2_Login_Events: irrelevant + Test_V2_Login_Events_Anon: irrelevant + Test_V2_Login_Events_RC: irrelevant Test_V3_Auto_User_Instrum_Mode_Capability: missing_feature Test_V3_Login_Events: missing_feature Test_V3_Login_Events_Anon: missing_feature From f837dd08cb702597ff69eef74165ddd5abc39f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Tue, 21 Jan 2025 09:17:52 +0100 Subject: [PATCH 16/17] Add a parameter to define when to trigger SDK events (#3844) --- docs/weblog/README.md | 1 + ...est_automated_user_and_session_tracking.py | 40 ++++++++---- .../security/AppSecAuthenticationFilter.java | 62 +++++++++++++++---- .../AppSecAuthenticationProvider.java | 26 +------- .../springboot/security/AppSecToken.java | 28 --------- 5 files changed, 80 insertions(+), 77 deletions(-) diff --git a/docs/weblog/README.md b/docs/weblog/README.md index 9b81faab338..97a80fe6a20 100644 --- a/docs/weblog/README.md +++ b/docs/weblog/README.md @@ -665,6 +665,7 @@ Body fields accepted in POST method: It also supports HTTP authentication by using GET method and the authorization header. Additionally, both methods support the following query parameters to use the sdk functions along with the authentication framework: +- `sdk_trigger`: when to call the sdk function, `after` or `before` the automated login event (by default `after`) - `sdk_event`: login event type: `success` or `failure`. - `sdk_user`: user id to be used in the sdk call. - `sdk_mail`: user's mail to be used in the sdk call. diff --git a/tests/appsec/test_automated_user_and_session_tracking.py b/tests/appsec/test_automated_user_and_session_tracking.py index 1f0de5d2fcc..c5d7b6da7bc 100644 --- a/tests/appsec/test_automated_user_and_session_tracking.py +++ b/tests/appsec/test_automated_user_and_session_tracking.py @@ -67,21 +67,35 @@ def test_user_tracking_auto(self): assert meta["_dd.appsec.user.collection_mode"] == "identification" def setup_user_tracking_sdk_overwrite(self): - self.r_login = weblog.post( - "/login?auth=local&sdk_event=success&sdk_user=sdkUser", data=login_data(context, USER, PASSWORD) - ) + self.requests = { + "before": weblog.post( + "/login?auth=local&sdk_trigger=before&sdk_event=success&sdk_user=sdkUser", + data=login_data(context, USER, PASSWORD), + ), + "after": weblog.post( + "/login?auth=local&sdk_trigger=after&sdk_event=success&sdk_user=sdkUser", + data=login_data(context, USER, PASSWORD), + ), + } def test_user_tracking_sdk_overwrite(self): - assert self.r_login.status_code == 200 - for _, _, span in interfaces.library.get_spans(request=self.r_login): - meta = span.get("meta", {}) - assert meta["usr.id"] == "sdkUser" - if context.library in libs_without_user_id: - assert meta["_dd.appsec.usr.id"] == USER - else: - assert meta["_dd.appsec.usr.id"] == "social-security-id" - - assert meta["_dd.appsec.user.collection_mode"] == "sdk" + for trigger, request in self.requests.items(): + assert request.status_code == 200 + for _, _, span in interfaces.library.get_spans(request=request): + meta = span.get("meta", {}) + assert meta["usr.id"] == "sdkUser", f"{trigger}: 'usr.id' must be set by the SDK" + if context.library in libs_without_user_id: + assert ( + meta["_dd.appsec.usr.id"] == USER + ), f"{trigger}: '_dd.appsec.usr.id' must be set by the automated instrumentation" + else: + assert ( + meta["_dd.appsec.usr.id"] == "social-security-id" + ), f"{trigger}: '_dd.appsec.usr.id' must be set by the automated instrumentation" + + assert ( + meta["_dd.appsec.user.collection_mode"] == "sdk" + ), f"{trigger}: The collection mode should be 'sdk'" CONFIG_ENABLED = ( diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationFilter.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationFilter.java index c0e9b653c21..7eef9338f6f 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationFilter.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationFilter.java @@ -1,11 +1,14 @@ package com.datadoghq.system_tests.springboot.security; -import static java.util.Collections.emptyList; - +import datadog.trace.api.EventTracker; +import datadog.trace.api.GlobalTracer; +import org.checkerframework.checker.units.qual.A; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Base64Utils; @@ -14,9 +17,28 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; public class AppSecAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + private enum SdkTrigger { + BEFORE, + AFTER, + NONE; + + public static SdkTrigger get(final HttpServletRequest request) { + if (request.getParameter("sdk_event") == null) { + return NONE; + } + final String trigger = request.getParameter("sdk_trigger"); + if (trigger == null || "after".equalsIgnoreCase(trigger)) { + return AFTER; + } + return BEFORE; + } + } + public AppSecAuthenticationFilter(String url, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(url), authenticationManager); this.setAuthenticationSuccessHandler((request, response, authentication) -> { @@ -48,15 +70,33 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ default: return null; } - Authentication authentication; - String sdkEvent = request.getParameter("sdk_event"); - if (sdkEvent != null) { - String sdkUser = request.getParameter("sdk_user"); - boolean sdkUserExists = Boolean.parseBoolean(request.getParameter("sdk_user_exists")); - authentication = new AppSecToken(username, password, sdkEvent, sdkUser, sdkUserExists); - } else { - authentication = new AppSecToken(username, password); + final SdkTrigger trigger = SdkTrigger.get(request); + AuthenticationException sdkException = trigger == SdkTrigger.BEFORE ? triggerSdk(request) : null; + try { + return this.getAuthenticationManager().authenticate(new AppSecToken(username, password)); + } finally { + sdkException = trigger == SdkTrigger.AFTER ? triggerSdk(request) : sdkException; + if (sdkException != null) { + throw sdkException; + } + } + } + + private AuthenticationException triggerSdk(final HttpServletRequest request) { + final String sdkEvent = request.getParameter("sdk_event"); + final String sdkUser = request.getParameter("sdk_user"); + final boolean sdkUserExists = Boolean.parseBoolean(request.getParameter("sdk_user_exists")); + final EventTracker tracker = GlobalTracer.getEventTracker(); + final Map metadata = new HashMap<>(); + switch (sdkEvent) { + case "success": + tracker.trackLoginSuccessEvent(sdkUser, metadata); + return null; + case "failure": + tracker.trackLoginFailureEvent(sdkUser, sdkUserExists, metadata); + return new BadCredentialsException(sdkUser); + default: + throw new IllegalArgumentException("Invalid SDK event: " + sdkEvent); } - return this.getAuthenticationManager().authenticate(authentication); } } diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationProvider.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationProvider.java index 0d686988e7f..13ee7a0ce83 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationProvider.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecAuthenticationProvider.java @@ -25,11 +25,7 @@ public AppSecAuthenticationProvider(final UserDetailsManager userDetailsManager) @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { AppSecToken token = (AppSecToken) authentication; - if (token.getSdkEvent() == null) { - return loginUserPassword(token); - } else { - return loginSdk(token); - } + return loginUserPassword(token); } private Authentication loginUserPassword(final AppSecToken auth) { @@ -44,26 +40,6 @@ private Authentication loginUserPassword(final AppSecToken auth) { return new AppSecToken(new AppSecUser(user), auth.getCredentials(), Collections.emptyList()); } - private Authentication loginSdk(final AppSecToken auth) { - Map metadata = new HashMap<>(); - EventTracker tracker = GlobalTracer.getEventTracker(); - switch (auth.getSdkEvent()) { - case "success": - tracker.trackLoginSuccessEvent(auth.getSdkUser(), metadata); - return new AppSecToken(auth.getName(), auth.getCredentials(), Collections.emptyList()); - case "failure": - tracker.trackLoginFailureEvent(auth.getSdkUser(), auth.isSdkUserExists(), metadata); - if (auth.isSdkUserExists()) { - throw new BadCredentialsException(auth.getSdkUser()); - } else { - throw new UsernameNotFoundException(auth.getSdkUser()); - } - default: - throw new IllegalArgumentException("Invalid SDK event: " + auth.getSdkEvent()); - } - - } - @Override public boolean supports(Class authentication) { return AppSecToken.class.isAssignableFrom(authentication); diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecToken.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecToken.java index f937d40eeb5..e13f7f64f9f 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecToken.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/security/AppSecToken.java @@ -5,42 +5,14 @@ import java.util.Collection; -/** - * Token used to bypass appsec auto user instrumentation when using the SDK - */ public class AppSecToken extends UsernamePasswordAuthenticationToken { - private String sdkEvent; - - private String sdkUser; - - private boolean sdkUserExists; - public AppSecToken(Object principal, Object credentials) { - this(principal, credentials, null, null, false); - } - - public AppSecToken(Object principal, Object credentials, String sdkEvent, String sdkUser, boolean sdkUserExists) { super(principal, credentials); - this.sdkEvent = sdkEvent; - this.sdkUser = sdkUser; - this.sdkUserExists = sdkUserExists; } public AppSecToken(Object principal, Object credentials, Collection authorities) { super(principal, credentials, authorities); } - - public String getSdkEvent() { - return sdkEvent; - } - - public String getSdkUser() { - return sdkUser; - } - - public boolean isSdkUserExists() { - return sdkUserExists; - } } From 618cf56f752d226d190b7f61fe7e84379bc89ca4 Mon Sep 17 00:00:00 2001 From: Yury Lebedev Date: Tue, 21 Jan 2025 10:00:07 +0100 Subject: [PATCH 17/17] [ruby] Remove special handling of GraphQL response status code (#3843) --- tests/appsec/test_blocking_addresses.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/appsec/test_blocking_addresses.py b/tests/appsec/test_blocking_addresses.py index 8b629778cab..53a011e4f7c 100644 --- a/tests/appsec/test_blocking_addresses.py +++ b/tests/appsec/test_blocking_addresses.py @@ -688,11 +688,10 @@ def setup_request_block_attack(self): ), ) + @bug(context.library < "ruby@2.10.0-dev", reason="APPSEC-56464") def test_request_block_attack(self): - assert self.r_attack.status_code == ( - # We don't change the status code in Ruby - 200 if context.library == "ruby" else 403 - ) + assert self.r_attack.status_code == 403 + for _, span in interfaces.library.get_root_spans(request=self.r_attack): meta = span.get("meta", {}) meta_struct = span.get("meta_struct", {}) @@ -728,12 +727,10 @@ def setup_request_block_attack_directive(self): ), ) + @bug(context.library < "ruby@2.10.0-dev", reason="APPSEC-56464") def test_request_block_attack_directive(self): - # We don't change the status code - assert self.r_attack.status_code == ( - # We don't change the status code in Ruby - 200 if context.library == "ruby" else 403 - ) + assert self.r_attack.status_code == 403 + for _, span in interfaces.library.get_root_spans(request=self.r_attack): meta = span.get("meta", {}) meta_struct = span.get("meta_struct", {})