Allow Azure Resource Graph Kusto endpoints in Kusto cluster URI validation (#4029)

* Allow Azure Resource Graph Kusto endpoints in Kusto cluster URI validation

Add .arg.core.windows.net to KustoClient's domain suffix allow-list so the Kusto
tools can query Azure Resource Graph Kusto endpoints. The Kusto SDKs' well-known
endpoints already trust this suffix, and KustoClient already keeps the endpoint
path and requests the Kusto scope these endpoints use.

Fixes #4028

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: df5f4d96-6fea-431c-bc49-01af1461746b

* Make the KustoClient request test endpoint-agnostic

Rename the test and use neutral database and query arguments so it doesn't suggest
that Azure Resource Graph endpoints get special handling. Any cluster URI that
passes the allow-list works; this one includes a path to show it's preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: df5f4d96-6fea-431c-bc49-01af1461746b

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: df5f4d96-6fea-431c-bc49-01af1461746b
This commit is contained in:
Karan SinghandCopilot authored and GitHub committed 2026-10-26 15:27:15 -07:00
1 parent c3e9ec1093
commit 150473f1fc
3 files changed
+34

No files matched your search

@@ -0,0 +1,3 @@
changes:
- section: "Bugs Fixed"
description: "Fixed Kusto tools rejecting Azure Resource Graph Kusto endpoints (`*.arg.core.windows.net`), which the Kusto SDKs already trust."
@@ -26,6 +26,7 @@ public sealed class KustoClient(
".kustomfa.windows.net",
".kusto.data.microsoft.com",
".kusto.fabric.microsoft.com",
".arg.core.windows.net", // Azure Resource Graph Kusto endpoints: https://github.com/microsoft/mcp/issues/4028
".adx.loganalytics.azure.com",
".adx.applicationinsights.azure.com",
".adx.monitor.azure.com",
@@ -47,6 +47,27 @@ public sealed class KustoClientTests
Assert.NotNull(result);
}
[Fact]
public async Task ExecuteQueryCommandAsync_KeepsPathAndUsesKustoScope()
{
// Arrange - any cluster URI that passes the allow-list works here; this one includes a path to show it's preserved
const string clusterUri = "https://mycluster.arg.core.windows.net/providers/Microsoft.ResourceGraph/connector/kusto/sample";
var handler = new MockHttpMessageHandler();
using var httpClient = new HttpClient(handler);
_azureService.GetClient(Arg.Any<string>()).Returns(httpClient);
var kustoClient = new KustoClient(clusterUri, _tokenCredential, "azmcp", _azureService);
// Act
await kustoClient.ExecuteQueryCommandAsync("testdb", "test query", TestContext.Current.CancellationToken);
// Assert
Assert.Equal(new Uri($"{clusterUri}/v1/rest/query"), handler.LastRequestUri);
await _tokenCredential.Received(1).GetTokenAsync(
Arg.Is<TokenRequestContext>(context =>
context.Scopes.Length == 1 && context.Scopes[0] == "https://kusto.kusto.windows.net/.default"),
Arg.Any<CancellationToken>());
}
#region SSRF Protection Tests
[Theory]
@@ -57,6 +78,8 @@ public sealed class KustoClientTests
[InlineData("https://test.kusto.windows.net.example.com/path")]
[InlineData("https://169.254.169.254/metadata/instance")] // Azure IMDS
[InlineData("https://management.azure.com/subscriptions")]
[InlineData("https://arg.core.windows.net")] // Resource Graph suffix without an endpoint name
[InlineData("https://mycluster.arg.core.windows.net.example.com")]
public void Constructor_RejectsInvalidClusterUris(string invalidClusterUri)
{
// Act & Assert
@@ -109,6 +132,9 @@ public sealed class KustoClientTests
[InlineData("https://mycluster.adx.applicationinsights.azure.com")]
// Germany
[InlineData("https://mycluster.kusto.sovcloud-api.de")]
// Azure Resource Graph
[InlineData("https://mycluster.arg.core.windows.net")]
[InlineData("https://mycluster.arg.core.windows.net/providers/Microsoft.ResourceGraph/connector/kusto/sample")]
public void Constructor_AcceptsValidKustoClusterUris(string validClusterUri)
{
// Act - should not throw
@@ -198,8 +224,12 @@ public sealed class KustoClientTests
private sealed class MockHttpMessageHandler : HttpMessageHandler
{
public Uri? LastRequestUri { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequestUri = request.RequestUri;
// The caller (HttpClient.SendAsync) takes ownership and is responsible for disposal HttpResponseMessage.
var response = new HttpResponseMessage(HttpStatusCode.OK)
{