diff --git a/tracer/src/Datadog.Trace/AppSec/Security.cs b/tracer/src/Datadog.Trace/AppSec/Security.cs index a620a2b11862..64c937a57507 100644 --- a/tracer/src/Datadog.Trace/AppSec/Security.cs +++ b/tracer/src/Datadog.Trace/AppSec/Security.cs @@ -210,6 +210,7 @@ private ApplyDetails[] UpdateFromRcm(Dictionary +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -22,7 +22,7 @@ internal partial interface IMetricsTelemetryCollector /// /// Sets the version of the WAF used for future metrics /// - public void SetWafVersion(string wafVersion); + public void SetWafAndRulesVersion(string wafVersion, string? eventRulesVersion); public Task DisposeAsync(); } diff --git a/tracer/src/Datadog.Trace/Telemetry/Collectors/MetricsTelemetryCollectorBase.cs b/tracer/src/Datadog.Trace/Telemetry/Collectors/MetricsTelemetryCollectorBase.cs index c8557c0f99eb..2f24a6cdac37 100644 --- a/tracer/src/Datadog.Trace/Telemetry/Collectors/MetricsTelemetryCollectorBase.cs +++ b/tracer/src/Datadog.Trace/Telemetry/Collectors/MetricsTelemetryCollectorBase.cs @@ -1,4 +1,4 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -14,12 +14,12 @@ namespace Datadog.Trace.Telemetry; internal abstract partial class MetricsTelemetryCollectorBase { + private static readonly string[] _unknownWafAndRulesVersionTags = { "waf_version:unknown", "event_rules_version:unknown" }; private readonly TimeSpan _aggregationInterval; private readonly Action? _aggregationNotification; - private readonly string[] _unknownWafVersionTags = { "waf_version:unknown" }; private readonly Task _aggregateTask; private readonly TaskCompletionSource _processExit = new(); - private string[]? _wafVersionTags; + private string[]? _wafAndRulesVersionTags; protected MetricsTelemetryCollectorBase() : this(TimeSpan.FromSeconds(10)) @@ -53,10 +53,10 @@ public Task DisposeAsync() return _aggregateTask; } - public void SetWafVersion(string wafVersion) + public void SetWafAndRulesVersion(string wafVersion, string? eventRulesVersion) { // Setting this an array so we can reuse it for multiple metrics - _wafVersionTags = new[] { $"waf_version:{wafVersion}" }; + _wafAndRulesVersionTags = new[] { $"waf_version:{wafVersion}", $"event_rules_version:{eventRulesVersion ?? "unknown"}" }; } protected static AggregatedMetric[] GetPublicApiCountBuffer() @@ -220,12 +220,19 @@ private async Task AggregateMetricsLoopAsync() if (metricKeyTags is null) { - return _wafVersionTags ?? _unknownWafVersionTags; + return _wafAndRulesVersionTags ?? _unknownWafAndRulesVersionTags; } - var wafVersionTag = (_wafVersionTags ?? _unknownWafVersionTags)[0]; + if (string.Equals(metricKeyTags[0], "waf_version")) + { + metricKeyTags[0] = (_wafAndRulesVersionTags ?? _unknownWafAndRulesVersionTags)[0]; + } + + if (metricKeyTags.Length > 1 && string.Equals(metricKeyTags[1], "event_rules_version")) + { + metricKeyTags[1] = (_wafAndRulesVersionTags ?? _unknownWafAndRulesVersionTags)[1]; + } - metricKeyTags[0] = wafVersionTag; return metricKeyTags; } diff --git a/tracer/src/Datadog.Trace/Telemetry/Collectors/NullMetricsTelemetryCollector.cs b/tracer/src/Datadog.Trace/Telemetry/Collectors/NullMetricsTelemetryCollector.cs index 04f5f4a03caa..1530fc579dad 100644 --- a/tracer/src/Datadog.Trace/Telemetry/Collectors/NullMetricsTelemetryCollector.cs +++ b/tracer/src/Datadog.Trace/Telemetry/Collectors/NullMetricsTelemetryCollector.cs @@ -1,4 +1,4 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -21,7 +21,7 @@ public void Record(PublicApiUsage api) public MetricResults GetMetrics() => new(null, null); - public void SetWafVersion(string wafVersion) + public void SetWafAndRulesVersion(string wafVersion, string? eventRulesVersion) { } } diff --git a/tracer/src/Datadog.Trace/Telemetry/Metrics/MetricTags.cs b/tracer/src/Datadog.Trace/Telemetry/Metrics/MetricTags.cs index 0089de331d13..0e69b23500d2 100644 --- a/tracer/src/Datadog.Trace/Telemetry/Metrics/MetricTags.cs +++ b/tracer/src/Datadog.Trace/Telemetry/Metrics/MetricTags.cs @@ -272,11 +272,11 @@ public enum WafAnalysis // Note the initial 'waf_version'. This is an optimisation to avoid multiple array allocations // It is replaced with the "real" waf_version at runtime // CAUTION: waf_version should aways be placed in first position - [Description("waf_version;rule_triggered:false;request_blocked:false;waf_timeout:false;request_excluded:false")]Normal, - [Description("waf_version;rule_triggered:true;request_blocked:false;waf_timeout:false;request_excluded:false")]RuleTriggered, - [Description("waf_version;rule_triggered:true;request_blocked:true;waf_timeout:false;request_excluded:false")]RuleTriggeredAndBlocked, - [Description("waf_version;rule_triggered:false;request_blocked:false;waf_timeout:true;request_excluded:false")]WafTimeout, - [Description("waf_version;rule_triggered:false;request_blocked:false;waf_timeout:false;request_excluded:true")]RequestExcludedViaFilter, + [Description("waf_version;event_rules_version;rule_triggered:false;request_blocked:false;waf_timeout:false;request_excluded:false")]Normal, + [Description("waf_version;event_rules_version;rule_triggered:true;request_blocked:false;waf_timeout:false;request_excluded:false")]RuleTriggered, + [Description("waf_version;event_rules_version;rule_triggered:true;request_blocked:true;waf_timeout:false;request_excluded:false")]RuleTriggeredAndBlocked, + [Description("waf_version;event_rules_version;rule_triggered:false;request_blocked:false;waf_timeout:true;request_excluded:false")]WafTimeout, + [Description("waf_version;event_rules_version;rule_triggered:false;request_blocked:false;waf_timeout:false;request_excluded:true")]RequestExcludedViaFilter, } [EnumExtensions] diff --git a/tracer/src/Datadog.Trace/TracerManagerFactory.cs b/tracer/src/Datadog.Trace/TracerManagerFactory.cs index 2e8b4346eded..202f2cbf3c2d 100644 --- a/tracer/src/Datadog.Trace/TracerManagerFactory.cs +++ b/tracer/src/Datadog.Trace/TracerManagerFactory.cs @@ -140,7 +140,7 @@ internal TracerManager CreateTracerManager( gitMetadataTagsProvider); telemetry.RecordTracerSettings(settings, defaultServiceName); - TelemetryFactory.Metrics.SetWafVersion(Security.Instance.DdlibWafVersion); + TelemetryFactory.Metrics.SetWafAndRulesVersion(Security.Instance.DdlibWafVersion, Security.Instance.WafRuleFileVersion); ErrorData? initError = !string.IsNullOrEmpty(Security.Instance.InitializationError) ? new ErrorData(TelemetryErrorCode.AppsecConfigurationError, Security.Instance.InitializationError) : null; diff --git a/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetBase.cs b/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetBase.cs index 224fd1b3042f..0022af4b6ddc 100644 --- a/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetBase.cs +++ b/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetBase.cs @@ -128,7 +128,7 @@ public void ScrubFingerprintHeaders(VerifySettings settings) settings.AddRegexScrubber(AppSecFingerPrintNetwork, "_dd.appsec.fp.http.network: "); } - public async Task VerifySpans(IImmutableList spans, VerifySettings settings, bool testInit = false, string methodNameOverride = null, string testName = null, bool forceMetaStruct = false, string fileNameOverride = null) + public async Task VerifySpans(IImmutableList spans, VerifySettings settings, bool testInit = false, string methodNameOverride = null, string testName = null, bool forceMetaStruct = false, string fileNameOverride = null, bool showRulesVersion = false) { settings.ModifySerialization( serializationSettings => @@ -209,11 +209,15 @@ public async Task VerifySpans(IImmutableList spans, VerifySettings set if (!testInit) { settings.AddRegexScrubber(AppSecWafVersion, string.Empty); - settings.AddRegexScrubber(AppSecWafRulesVersion, string.Empty); settings.AddRegexScrubber(AppSecErrorCount, string.Empty); settings.AddRegexScrubber(AppSecEventRulesLoaded, string.Empty); } + if (!showRulesVersion && !testInit) + { + settings.AddRegexScrubber(AppSecWafRulesVersion, string.Empty); + } + var appsecSpans = spans.Where(s => s.Tags.ContainsKey("_dd.appsec.json") || (s.MetaStruct != null && s.MetaStruct.ContainsKey("appsec"))); if (appsecSpans.Any()) { diff --git a/tracer/test/Datadog.Trace.Security.IntegrationTests/Rcm/AspNetCore5AsmRemoteRules.cs b/tracer/test/Datadog.Trace.Security.IntegrationTests/Rcm/AspNetCore5AsmRemoteRules.cs index 0953213d8a9a..cb052229e773 100644 --- a/tracer/test/Datadog.Trace.Security.IntegrationTests/Rcm/AspNetCore5AsmRemoteRules.cs +++ b/tracer/test/Datadog.Trace.Security.IntegrationTests/Rcm/AspNetCore5AsmRemoteRules.cs @@ -68,7 +68,8 @@ public async Task TestRemoteRules() spans.AddRange(spans5); spans.AddRange(spans6); - await VerifySpans(spans.ToImmutableList(), settings); + // We want to test that the remote rules version tag gets updated through RC + await VerifySpans(spans.ToImmutableList(), settings, showRulesVersion: true); } protected override string GetTestName() => Prefix + nameof(AspNetCore5AsmRemoteRules); diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/Collectors/MetricsTelemetryCollectorTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/Collectors/MetricsTelemetryCollectorTests.cs index 770453f5c824..66d742a50553 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/Collectors/MetricsTelemetryCollectorTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/Collectors/MetricsTelemetryCollectorTests.cs @@ -83,9 +83,10 @@ public async Task AggregatesOnShutdown() } [Theory] - [InlineData(null)] - [InlineData("1.2.3")] - public async Task AllMetricsAreReturned_ForMetricsTelemetryCollector(string wafVersion) + [InlineData(null, null)] + [InlineData("1.2.4", null)] + [InlineData("1.2.3", "10.2")] + public async Task AllMetricsAreReturned_ForMetricsTelemetryCollector(string wafVersion, string rulesVersion) { static void IncrementOpenTelemetryConfigMetrics(MetricsTelemetryCollector collector, string openTelemetryKey) { @@ -146,14 +147,14 @@ static void IncrementOpenTelemetryConfigMetrics(MetricsTelemetryCollector collec collector.AggregateMetrics(); - var expectedWafTag = "waf_version:unknown"; - if (wafVersion is not null) { - collector.SetWafVersion(wafVersion); - expectedWafTag = $"waf_version:{wafVersion}"; + collector.SetWafAndRulesVersion(wafVersion, rulesVersion); } + var expectedWafTag = $"waf_version:{wafVersion ?? "unknown"}"; + var expectedRulesetTag = $"event_rules_version:{rulesVersion ?? "unknown"}"; + using var scope = new AssertionScope(); scope.FormattingOptions.MaxLines = 1000; @@ -242,7 +243,7 @@ static void IncrementOpenTelemetryConfigMetrics(MetricsTelemetryCollector collec Metric = Count.WafInit.GetName(), Points = new[] { new { Value = 4 } }, Type = TelemetryMetricType.Count, - Tags = new[] { expectedWafTag }, + Tags = new[] { expectedWafTag, expectedRulesetTag }, Common = true, Namespace = NS.ASM, }, @@ -251,7 +252,7 @@ static void IncrementOpenTelemetryConfigMetrics(MetricsTelemetryCollector collec Metric = Count.WafRequests.GetName(), Points = new[] { new { Value = 5 } }, Type = TelemetryMetricType.Count, - Tags = new[] { expectedWafTag, "rule_triggered:false", "request_blocked:false", "waf_timeout:false", "request_excluded:false" }, + Tags = new[] { expectedWafTag, expectedRulesetTag, "rule_triggered:false", "request_blocked:false", "waf_timeout:false", "request_excluded:false" }, Common = true, Namespace = NS.ASM, }, @@ -504,9 +505,10 @@ static void IncrementOpenTelemetryConfigMetrics(MetricsTelemetryCollector collec } [Theory] - [InlineData(null)] - [InlineData("1.2.3")] - public async Task AllMetricsAreReturned_ForCiVisibilityCollector(string wafVersion) + [InlineData(null, null)] + [InlineData("1.2.3", null)] + [InlineData("1.2.3", "10.2")] + public async Task AllMetricsAreReturned_ForCiVisibilityCollector(string wafVersion, string rulesVersion) { var collector = new CiVisibilityMetricsTelemetryCollector(Timeout.InfiniteTimeSpan); collector.Record(PublicApiUsage.Tracer_Configure); @@ -551,12 +553,9 @@ public async Task AllMetricsAreReturned_ForCiVisibilityCollector(string wafVersi collector.AggregateMetrics(); - var expectedWafTag = "waf_version:unknown"; - if (wafVersion is not null) { - collector.SetWafVersion(wafVersion); - expectedWafTag = $"waf_version:{wafVersion}"; + collector.SetWafAndRulesVersion(wafVersion, rulesVersion); } using var scope = new AssertionScope(); diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/Metrics/MetricTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/Metrics/MetricTests.cs index d511c5504067..342e2a9d1102 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/Metrics/MetricTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/Metrics/MetricTests.cs @@ -1,4 +1,4 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -23,9 +23,6 @@ public class MetricTests { private static readonly Dictionary IgnoredTagsByMetricName = new() { - { "waf.init", new[] { "event_rules_version" } }, // we don't send this tag as cardinality is infinite - { "waf.updates", new[] { "event_rules_version" } }, // we don't send this tag as cardinality is infinite - { "waf.requests", new[] { "event_rules_version" } }, // we don't send this tag as cardinality is infinite { "spans_finished", new[] { "integration_name" } }, // this is technically difficult for us, so we don't tag it { "trace_chunks_dropped", ["src_library"] }, // this is optional, only added by the rust library { "trace_chunks_sent", ["src_library"] }, // this is optional, only added by the rust library @@ -66,8 +63,8 @@ public class MetricTests public void OnlyAllowedMetricsAreSubmitted() { // Only metrics defined in the following json documents should be submitted - // https://github.com/DataDog/dd-go/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/common_metrics.json - // https://github.com/DataDog/dd-go/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/dotnet_metrics.json + // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/common_metrics.json + // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-metrics/static/dotnet_metrics.json // // These are duplicated in this repo. When adding new metrics, add them into the embedded json files here, then // after merging, update the source JSON file in dd-go @@ -144,6 +141,38 @@ public void OnlyAllowedMetricsAreSubmitted() } } + /* + if a metric uses waf_version then it's always the first element + if a metric uses event_rules_version then it's always the second element + if a metric uses event_rules_version then we always have a waf_version + */ + + [Fact] + public void CheckASMTags() + { + var actual = GetImplementedMetricsAndTags(); + + foreach (var metric in actual) + { + foreach (var permutation in metric.TagPermutations) + { + for (int i = 0; i < permutation.Length; i++) + { + if (permutation[i].StartsWith("waf_version")) + { + i.Should().Be(0, $"waf_version should always be the first tag for {metric.Metric}"); + } + + if (permutation[i].StartsWith("event_rules_version")) + { + i.Should().Be(1, $"event_rules_version should always be the second tag for {metric.Metric}"); + permutation[0].Should().StartWith("waf_version", $"event_rules_version should always be accompanied by waf_version for {metric.Metric}"); + } + } + } + } + } + private static List GetImplementedMetricsAndTags() { var results = new List(); @@ -194,11 +223,16 @@ static List GetTagPermutations(FieldInfo member, string ns) var attributeType = attr.AttributeType; if (attributeType == typeof(TelemetryMetricAttribute)) { - // no tags unless this is an ASM metric, in which case we include waf_version + // no tags unless this is an ASM metric, in which case we include waf_version and event_rules_version // see src\Datadog.Trace\Telemetry\Collectors\MetricsTelemetryCollector.GetTags(string? ns, string[]? metricKeyTags) if (ns == MetricNamespaceConstants.ASM) { - return new() { new[] { "waf_version:unknown" } }; + bool isRasp = member.Name.StartsWith("rasp", StringComparison.Ordinal); + return new() + { + isRasp ? new[] { "waf_version:unknown" } : + new[] { "waf_version:unknown", "event_rules_version:unknown" } + }; } return new List(); diff --git a/tracer/test/snapshots/Security.AspNetCore5AsmRemoteRules._.verified.txt b/tracer/test/snapshots/Security.AspNetCore5AsmRemoteRules._.verified.txt index 550310299add..56528285a658 100644 --- a/tracer/test/snapshots/Security.AspNetCore5AsmRemoteRules._.verified.txt +++ b/tracer/test/snapshots/Security.AspNetCore5AsmRemoteRules._.verified.txt @@ -29,6 +29,7 @@ network.client.ip: 127.0.0.1, runtime-id: Guid_1, span.kind: server, + _dd.appsec.event_rules.version: 1.13.1, _dd.appsec.fp.http.endpoint: http-get-7f4bf8ee-49fefa92-, _dd.appsec.fp.http.header: hdr-0000000000-3626b5f8-1-4740ae63, _dd.appsec.fp.http.network: net-1-1000000000, @@ -79,6 +80,7 @@ network.client.ip: 127.0.0.1, runtime-id: Guid_1, span.kind: server, + _dd.appsec.event_rules.version: 2.22.222, _dd.appsec.fp.http.endpoint: http-get-7f4bf8ee-49fefa92-, _dd.appsec.fp.http.header: hdr-0000000000-3626b5f8-1-4740ae63, _dd.appsec.fp.http.network: net-1-1000000000, @@ -129,6 +131,7 @@ network.client.ip: 127.0.0.1, runtime-id: Guid_1, span.kind: server, + _dd.appsec.event_rules.version: 3.33.333, _dd.appsec.fp.http.endpoint: http-get-7f4bf8ee-49fefa92-, _dd.appsec.fp.http.header: hdr-0000000000-3626b5f8-1-4740ae63, _dd.appsec.fp.http.network: net-1-1000000000, @@ -178,6 +181,7 @@ network.client.ip: 127.0.0.1, runtime-id: Guid_1, span.kind: server, + _dd.appsec.event_rules.version: 18.18.18, _dd.appsec.fp.http.header: hdr-0000000000-bf177a93-1-4740ae63, _dd.appsec.fp.http.network: net-1-1000000000, _dd.appsec.json: {"triggers":[{"rule":{"id":"new-test-non-blocking","name":"Datadog test scanner - NON blocking version: user-agent","tags":{"category":"attack_attempt","type":"attack_tool"}},"rule_matches":[{"operator":"match_regex","operator_value":"^dd-test-scanner-log-block(?:$|/|\\s)","parameters":[{"address":"server.request.headers.no_cookies","highlight":["dd-test-scanner-log-block"],"key_path":["user-agent"],"value":"dd-test-scanner-log-block"}]}]}]}, @@ -223,6 +227,7 @@ network.client.ip: 127.0.0.1, runtime-id: Guid_1, span.kind: server, + _dd.appsec.event_rules.version: 1.13.1, _dd.appsec.fp.http.header: hdr-0000000000-bf177a93-1-4740ae63, _dd.appsec.fp.http.network: net-1-1000000000, _dd.appsec.json: {"triggers":[{"rule":{"id":"ua0-600-56x","name":"Datadog test scanner - blocking version: user-agent","tags":{"category":"attack_attempt","type":"attack_tool"}},"rule_matches":[{"operator":"match_regex","operator_value":"^dd-test-scanner-log-block(?:$|/|\\s)","parameters":[{"address":"server.request.headers.no_cookies","highlight":["dd-test-scanner-log-block"],"key_path":["user-agent"],"value":"dd-test-scanner-log-block"}]}]}]}, @@ -268,6 +273,7 @@ network.client.ip: 127.0.0.1, runtime-id: Guid_1, span.kind: server, + _dd.appsec.event_rules.version: 1.13.1, _dd.appsec.fp.http.header: hdr-0000000000-bf177a93-1-4740ae63, _dd.appsec.fp.http.network: net-1-1000000000, _dd.appsec.json: {"triggers":[{"rule":{"id":"ua0-600-56x","name":"Datadog test scanner - blocking version: user-agent","tags":{"category":"attack_attempt","type":"attack_tool"}},"rule_matches":[{"operator":"match_regex","operator_value":"^dd-test-scanner-log-block(?:$|/|\\s)","parameters":[{"address":"server.request.headers.no_cookies","highlight":["dd-test-scanner-log-block"],"key_path":["user-agent"],"value":"dd-test-scanner-log-block"}]}]}]},