diff --git a/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs b/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs
index 287ce6e..5a33f90 100644
--- a/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs
+++ b/src/Extensions/Cloudflare.Dns/DnsFirewallExtensions.cs
@@ -155,6 +155,43 @@ namespace AMWD.Net.Api.Cloudflare.Dns
#endregion DNS Firewall
+ #region Analytics
+
+ ///
+ /// Retrieves a list of summarised aggregate metrics over a given time period.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The DNS firewall identifier.
+ /// Filter options (optional).
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ ///
+ public static Task> GetDnsFirewallAnalyticsReport(this ICloudflareClient client, string accountId, string dnsFirewallId, GetDnsAnalyticsReportFilter? options = null, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ dnsFirewallId.ValidateCloudflareId();
+
+ return client.GetAsync($"/accounts/{accountId}/dns_firewall/{dnsFirewallId}/dns_analytics/report", options, cancellationToken);
+ }
+
+ ///
+ /// Retrieves a list of aggregate metrics grouped by time interval.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The DNS firewall identifier.
+ /// Filter options (optional).
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> GetDnsFirewallAnalyticsByTime(this ICloudflareClient client, string accountId, string dnsFirewallId, GetDnsAnalyticsByTimeFilter? options = null, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+ dnsFirewallId.ValidateCloudflareId();
+
+ return client.GetAsync($"/accounts/{accountId}/dns_firewall/{dnsFirewallId}/dns_analytics/report/bytime", options, cancellationToken);
+ }
+
+ #endregion Analytics
+
#region Reverse DNS
///
diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md
index 2292e74..6841c95 100644
--- a/src/Extensions/Cloudflare.Dns/README.md
+++ b/src/Extensions/Cloudflare.Dns/README.md
@@ -122,6 +122,12 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
- [Delete DNS Firewall Cluster](https://developers.cloudflare.com/api/resources/dns_firewall/methods/delete/)
+#### [Analytics (Firewall)]
+
+- [Get Report Table](https://developers.cloudflare.com/api/resources/dns_firewall/subresources/analytics/subresources/reports/methods/get/)
+- [Get Report By Time](https://developers.cloudflare.com/api/resources/dns_firewall/subresources/analytics/subresources/reports/subresources/bytimes/methods/get/)
+
+
#### [Reverse DNS]
- [Show DNS Firewall Cluster Reverse DNS](https://developers.cloudflare.com/api/resources/dns_firewall/subresources/reverse_dns/methods/get/)
@@ -152,5 +158,7 @@ Published under MIT License (see [choose a license])
[Outgoing]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/outgoing/
[Peers]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/peers/
[TSIGs]: https://developers.cloudflare.com/api/resources/dns/subresources/zone_transfers/subresources/tsigs/
+
[DNS Firewall]: https://developers.cloudflare.com/api/resources/dns_firewall/
+ [Analytics (Firewall)]: https://developers.cloudflare.com/api/resources/dns_firewall/subresources/analytics/
[Reverse DNS]: https://developers.cloudflare.com/api/resources/dns_firewall/subresources/reverse_dns/
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsByTimeTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsByTimeTest.cs
new file mode 100644
index 0000000..4b2b6a6
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsByTimeTest.cs
@@ -0,0 +1,276 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsFirewallExtensions.Analytics
+{
+ [TestClass]
+ public class GetDnsFirewallAnalyticsByTimeTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+ private const string ClusterId = "023e105f4ecef8ad9ca31a8372d0c355";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [new ResponseInfo(1000, "Message 1")],
+ Errors = [new ResponseInfo(1001, "Error 1")],
+ ResultInfo = new PaginationInfo
+ {
+ Count = 1,
+ Page = 1,
+ PerPage = 100,
+ TotalCount = 100,
+ TotalPages = 1,
+ },
+ Result = new DnsAnalyticsByTime()
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldGetDnsFirewallAnalyticsByTime()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.GetDnsFirewallAnalyticsByTime(AccountId, ClusterId, cancellationToken: TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+
+ Assert.IsNotNull(response.Result);
+ Assert.IsInstanceOfType(response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, queryFilter) = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", requestPath);
+ Assert.IsNull(queryFilter);
+
+ _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldGetDnsFirewallAnalyticsByTimeWithFilter()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter
+ {
+ Since = DateTime.UtcNow.AddDays(-7),
+ Until = DateTime.UtcNow,
+ TimeDelta = TimeDeltaUnit.Day,
+ Dimensions = ["queryName", "responseCode"],
+ Filters = "queryType eq A",
+ Limit = 500,
+ Metrics = ["requests", "responses"],
+ Sort = ["-requests", "+responses"]
+ };
+
+ var client = GetClient();
+
+ // Act
+ var response = await client.GetDnsFirewallAnalyticsByTime(AccountId, ClusterId, filter, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+
+ Assert.IsNotNull(response.Result);
+ Assert.IsInstanceOfType(response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, queryFilter) = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", requestPath);
+ Assert.IsNotNull(queryFilter);
+
+ Assert.IsInstanceOfType(queryFilter);
+
+ _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report/bytime", filter, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public void ShouldReturnEmptyParameterList()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter();
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFullParameterList()
+ {
+ // Arrange
+ var since = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var until = new DateTime(2024, 1, 8, 0, 0, 0, DateTimeKind.Utc);
+ var filter = new GetDnsAnalyticsByTimeFilter
+ {
+ Dimensions = ["queryName", "responseCode"],
+ Filters = "queryType eq A",
+ Limit = 1000,
+ Metrics = ["requests", "responses"],
+ Since = since,
+ Sort = ["-requests", "+responses"],
+ Until = until,
+ TimeDelta = TimeDeltaUnit.Hour
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.HasCount(8, dict);
+
+ Assert.IsTrue(dict.ContainsKey("dimensions"));
+ Assert.AreEqual("queryName,responseCode", dict["dimensions"]);
+
+ Assert.IsTrue(dict.ContainsKey("filters"));
+ Assert.AreEqual("queryType eq A", dict["filters"]);
+
+ Assert.IsTrue(dict.ContainsKey("limit"));
+ Assert.AreEqual("1000", dict["limit"]);
+
+ Assert.IsTrue(dict.ContainsKey("metrics"));
+ Assert.AreEqual("requests,responses", dict["metrics"]);
+
+ Assert.IsTrue(dict.ContainsKey("since"));
+ Assert.AreEqual(since.ToIso8601Format(), dict["since"]);
+
+ Assert.IsTrue(dict.ContainsKey("sort"));
+ Assert.AreEqual("-requests,+responses", dict["sort"]);
+
+ Assert.IsTrue(dict.ContainsKey("until"));
+ Assert.AreEqual(until.ToIso8601Format(), dict["until"]);
+
+ Assert.IsTrue(dict.ContainsKey("time_delta"));
+ Assert.AreEqual("hour", dict["time_delta"]);
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void ShouldNotAddFilters(string str)
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter { Filters = str };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow(0)]
+ public void ShouldNotAddLimit(int? limit)
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter { Limit = limit };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddDimensionsIfEmpty()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter { Dimensions = [] };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddMetricsIfEmpty()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter { Metrics = [] };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddSortIfEmpty()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter { Sort = [] };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow((TimeDeltaUnit)0)]
+ public void ShouldNotAddTimeDelta(TimeDeltaUnit? timeDelta)
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsByTimeFilter { TimeDelta = timeDelta };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsReportTest.cs b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsReportTest.cs
new file mode 100644
index 0000000..f033593
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/DnsFirewallExtensions/Analytics/GetDnsFirewallAnalyticsReportTest.cs
@@ -0,0 +1,261 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.DnsFirewallExtensions.Analytics
+{
+ [TestClass]
+ public class GetDnsFirewallAnalyticsReportTest
+ {
+ public TestContext TestContext { get; set; }
+
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+ private const string ClusterId = "023e105f4ecef8ad9ca31a8372d0c355";
+
+ private Mock _clientMock;
+ private CloudflareResponse _response;
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages =
+ [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors =
+ [
+ new ResponseInfo(1001, "Error 1")
+ ],
+ ResultInfo = new PaginationInfo
+ {
+ Count = 1,
+ Page = 1,
+ PerPage = 100,
+ TotalCount = 100,
+ TotalPages = 1,
+ },
+ Result = new DnsAnalyticsReport()
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldGetDnsFirewallAnalyticsReport()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.GetDnsFirewallAnalyticsReport(AccountId, ClusterId, cancellationToken: TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+
+ Assert.IsNotNull(response.Result);
+ Assert.IsInstanceOfType(response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, queryFilter) = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", requestPath);
+ Assert.IsNull(queryFilter);
+
+ _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", null, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public async Task ShouldGetDnsFirewallAnalyticsReportWithFilter()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter
+ {
+ Since = DateTime.UtcNow.AddDays(-7),
+ Until = DateTime.UtcNow,
+ Dimensions = ["queryName", "responseCode"],
+ Filters = "queryType eq A",
+ Limit = 500,
+ Metrics = ["requests", "responses"],
+ Sort = ["-requests", "+responses"]
+ };
+
+ var client = GetClient();
+
+ // Act
+ var response = await client.GetDnsFirewallAnalyticsReport(AccountId, ClusterId, filter, TestContext.CancellationToken);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+
+ Assert.IsNotNull(response.Result);
+ Assert.IsInstanceOfType(response.Result);
+
+ Assert.HasCount(1, _callbacks);
+
+ var (requestPath, queryFilter) = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", requestPath);
+ Assert.IsNotNull(queryFilter);
+
+ Assert.IsInstanceOfType(queryFilter);
+
+ _clientMock.Verify(m => m.GetAsync($"/accounts/{AccountId}/dns_firewall/{ClusterId}/dns_analytics/report", filter, TestContext.CancellationToken), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [TestMethod]
+ public void ShouldReturnEmptyParameterList()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter();
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldReturnFullParameterList()
+ {
+ // Arrange
+ var since = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var until = new DateTime(2024, 1, 8, 0, 0, 0, DateTimeKind.Utc);
+ var filter = new GetDnsAnalyticsReportFilter
+ {
+ Dimensions = ["queryName", "responseCode"],
+ Filters = "queryType eq A",
+ Limit = 1000,
+ Metrics = ["requests", "responses"],
+ Since = since,
+ Sort = ["-requests", "+responses"],
+ Until = until
+ };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.HasCount(7, dict);
+
+ Assert.IsTrue(dict.ContainsKey("dimensions"));
+ Assert.AreEqual("queryName,responseCode", dict["dimensions"]);
+
+ Assert.IsTrue(dict.ContainsKey("filters"));
+ Assert.AreEqual("queryType eq A", dict["filters"]);
+
+ Assert.IsTrue(dict.ContainsKey("limit"));
+ Assert.AreEqual("1000", dict["limit"]);
+
+ Assert.IsTrue(dict.ContainsKey("metrics"));
+ Assert.AreEqual("requests,responses", dict["metrics"]);
+
+ Assert.IsTrue(dict.ContainsKey("since"));
+ Assert.AreEqual(since.ToIso8601Format(), dict["since"]);
+
+ Assert.IsTrue(dict.ContainsKey("sort"));
+ Assert.AreEqual("-requests,+responses", dict["sort"]);
+
+ Assert.IsTrue(dict.ContainsKey("until"));
+ Assert.AreEqual(until.ToIso8601Format(), dict["until"]);
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void ShouldNotAddFilters(string str)
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter { Filters = str };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow(0)]
+ public void ShouldNotAddLimit(int? limit)
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter { Limit = limit };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddDimensionsIfEmpty()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter { Dimensions = [] };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddMetricsIfEmpty()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter { Metrics = [] };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ [TestMethod]
+ public void ShouldNotAddSortIfEmpty()
+ {
+ // Arrange
+ var filter = new GetDnsAnalyticsReportFilter { Sort = [] };
+
+ // Act
+ var dict = filter.GetQueryParameters();
+
+ // Assert
+ Assert.IsNotNull(dict);
+ Assert.IsEmpty(dict);
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}