Add DNS firewall analytics

This commit is contained in:
2025-11-06 21:22:11 +01:00
parent dbcda1685a
commit 7dd0510294
4 changed files with 582 additions and 0 deletions

View File

@@ -155,6 +155,43 @@ namespace AMWD.Net.Api.Cloudflare.Dns
#endregion DNS Firewall
#region Analytics
/// <summary>
/// Retrieves a list of summarised aggregate metrics over a given time period.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="accountId">The account identifier.</param>
/// <param name="dnsFirewallId">The DNS firewall identifier.</param>
/// <param name="options">Filter options (optional).</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
/// <returns></returns>
public static Task<CloudflareResponse<DnsAnalyticsReport>> GetDnsFirewallAnalyticsReport(this ICloudflareClient client, string accountId, string dnsFirewallId, GetDnsAnalyticsReportFilter? options = null, CancellationToken cancellationToken = default)
{
accountId.ValidateCloudflareId();
dnsFirewallId.ValidateCloudflareId();
return client.GetAsync<DnsAnalyticsReport>($"/accounts/{accountId}/dns_firewall/{dnsFirewallId}/dns_analytics/report", options, cancellationToken);
}
/// <summary>
/// Retrieves a list of aggregate metrics grouped by time interval.
/// </summary>
/// <param name="client">The <see cref="ICloudflareClient"/> instance.</param>
/// <param name="accountId">The account identifier.</param>
/// <param name="dnsFirewallId">The DNS firewall identifier.</param>
/// <param name="options">Filter options (optional).</param>
/// <param name="cancellationToken">A cancellation token used to propagate notification that this operation should be canceled.</param>
public static Task<CloudflareResponse<DnsAnalyticsByTime>> GetDnsFirewallAnalyticsByTime(this ICloudflareClient client, string accountId, string dnsFirewallId, GetDnsAnalyticsByTimeFilter? options = null, CancellationToken cancellationToken = default)
{
accountId.ValidateCloudflareId();
dnsFirewallId.ValidateCloudflareId();
return client.GetAsync<DnsAnalyticsByTime>($"/accounts/{accountId}/dns_firewall/{dnsFirewallId}/dns_analytics/report/bytime", options, cancellationToken);
}
#endregion Analytics
#region Reverse DNS
/// <summary>

View File

@@ -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/

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsAnalyticsByTime> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsAnalyticsByTime>
{
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<DnsAnalyticsByTime>(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<DnsAnalyticsByTime>($"/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<DnsAnalyticsByTime>(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<GetDnsAnalyticsByTimeFilter>(queryFilter);
_clientMock.Verify(m => m.GetAsync<DnsAnalyticsByTime>($"/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<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<DnsAnalyticsByTime>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}

View File

@@ -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<ICloudflareClient> _clientMock;
private CloudflareResponse<DnsAnalyticsReport> _response;
private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
[TestInitialize]
public void Initialize()
{
_callbacks = [];
_response = new CloudflareResponse<DnsAnalyticsReport>
{
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<DnsAnalyticsReport>(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<DnsAnalyticsReport>($"/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<DnsAnalyticsReport>(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<GetDnsAnalyticsReportFilter>(queryFilter);
_clientMock.Verify(m => m.GetAsync<DnsAnalyticsReport>($"/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<ICloudflareClient>();
_clientMock
.Setup(m => m.GetAsync<DnsAnalyticsReport>(It.IsAny<string>(), It.IsAny<IQueryParameterFilter>(), It.IsAny<CancellationToken>()))
.Callback<string, IQueryParameterFilter, CancellationToken>((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
.ReturnsAsync(() => _response);
return _clientMock.Object;
}
}
}