AzureMonitorDatasource: Add bounds check to fix panics (#110879)

(azure-monitor-datasource): add bounds check to fix panics
This commit is contained in:
Tim Mulqueen
2025-09-11 12:31:11 +02:00
committed by GitHub
parent 5ce13061d5
commit 01b5543121
4 changed files with 202 additions and 0 deletions
@@ -573,6 +573,10 @@ func addTraceDataLinksToFields(query *AzureLogAnalyticsQuery, azurePortalBaseUrl
return err
}
if len(queryJSONModel.AzureTraces.Resources) == 0 {
return fmt.Errorf("no resources specified for Azure traces data link")
}
traceIdVariable := "${__data.fields.traceID}"
resultFormat := dataquery.ResultFormatTrace
queryJSONModel.AzureTraces.ResultFormat = &resultFormat
@@ -668,6 +672,9 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, queryUR
if query.AppInsightsQuery {
// If the query type is traces then we only need the first resource as the rest are specified in the query
if query.QueryType == dataquery.AzureQueryTypeAzureTraces {
if len(query.Resources) == 0 {
return nil, fmt.Errorf("no resources specified for Azure traces Application Insights query")
}
body["applications"] = []string{query.Resources[0]}
} else {
body["applications"] = query.Resources
@@ -15,6 +15,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery"
@@ -710,6 +711,19 @@ func TestLogAnalyticsCreateRequest(t *testing.T) {
t.Errorf("Unexpected Body: %v", cmp.Diff(string(body), expectedBody))
}
})
t.Run("returns error for AppInsights traces query with empty resources", func(t *testing.T) {
ds := AzureLogAnalyticsDatasource{}
_, err := ds.createRequest(ctx, url, &AzureLogAnalyticsQuery{
Resources: []string{}, // Empty resources
Query: "traces",
QueryType: dataquery.AzureQueryTypeAzureTraces,
AppInsightsQuery: true,
DashboardTime: false,
})
require.Error(t, err)
require.Contains(t, err.Error(), "no resources specified for Azure traces Application Insights query")
})
}
func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) {
@@ -826,3 +840,62 @@ func Test_exemplarsFeatureToggle(t *testing.T) {
require.Error(t, err, "query type unsupported as azureMonitorPrometheusExemplars feature toggle is not enabled")
})
}
func TestAddTraceDataLinksToFields_EmptyResources(t *testing.T) {
dsInfo := types.DatasourceInfo{
Services: map[string]types.DatasourceService{
"Azure Monitor": {},
},
JSONData: map[string]any{
"azureLogAnalyticsSameAs": false,
},
}
tests := []struct {
name string
queryJSON string
expectedErrorString string
}{
{
name: "empty resources array should return error",
queryJSON: `{
"queryType": "Azure Traces",
"azureTraces": {
"resources": [],
"resultFormat": "table",
"traceTypes": ["trace"]
}
}`,
expectedErrorString: "no resources specified for Azure traces data link",
},
{
name: "missing resources field should return error",
queryJSON: `{
"queryType": "Azure Traces",
"azureTraces": {
"resultFormat": "table",
"traceTypes": ["trace"]
}
}`,
expectedErrorString: "no resources specified for Azure traces data link",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := &AzureLogAnalyticsQuery{
JSON: []byte(tt.queryJSON),
QueryType: dataquery.AzureQueryTypeAzureTraces,
ResultFormat: dataquery.ResultFormatTable,
}
// Create a mock data frame
frame := data.NewFrame("test")
err := addTraceDataLinksToFields(query, "https://portal.azure.com", frame, dsInfo)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedErrorString)
})
}
}
@@ -207,6 +207,10 @@ func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo
resources = []string{fmt.Sprintf("/subscriptions/%s", subscription)}
}
if len(resources) == 0 {
return nil, fmt.Errorf("no resources specified for Azure traces query")
}
resourceOrWorkspace := resources[0]
appInsightsQuery := appInsightsRegExp.Match([]byte(resourceOrWorkspace))
resourcesMap := make(map[string]bool, 0)
@@ -236,6 +240,9 @@ func buildAppInsightsQuery(ctx context.Context, query backend.DataQuery, dsInfo
if query.QueryType == string(dataquery.AzureQueryTypeTraceExemplar) {
resources = queryResources
if len(resources) == 0 {
return nil, fmt.Errorf("no correlation resources found for trace exemplar query with operation ID: %s", operationId)
}
resourceOrWorkspace = resources[0]
}
@@ -1184,3 +1184,118 @@ func TestBuildAppInsightsQuery(t *testing.T) {
})
}
}
func TestBuildAppInsightsQuery_EmptyResources(t *testing.T) {
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
timeRange := backend.TimeRange{From: fromStart, To: fromStart.Add(34 * time.Minute)}
ctx := context.Background()
// Create a mock HTTP server that returns empty correlation resources
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Return empty correlation response
correlationRes := AzureCorrelationAPIResponse{
ID: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1",
Name: "guid-1",
Type: "microsoft.insights/transactions",
Properties: AzureCorrelationAPIResponseProperties{
Resources: []string{}, // Empty resources array
NextLink: nil,
},
}
err := json.NewEncoder(w).Encode(correlationRes)
if err != nil {
t.Errorf("failed to encode correlation API response")
}
}))
defer svr.Close()
provider := httpclient.NewProvider(httpclient.ProviderOptions{Timeout: &httpclient.DefaultTimeoutOptions})
client, err := provider.New()
require.NoError(t, err)
dsInfo := types.DatasourceInfo{
Services: map[string]types.DatasourceService{
"Azure Monitor": {URL: svr.URL, HTTPClient: client},
},
JSONData: map[string]any{
"azureLogAnalyticsSameAs": false,
},
Settings: types.AzureMonitorSettings{
SubscriptionId: "test-sub-id",
},
}
appInsightsRegExp, err := regexp.Compile("providers/Microsoft.Insights/components")
require.NoError(t, err)
logger := log.NewNullLogger()
tests := []struct {
name string
queryModel backend.DataQuery
expectedErrorString string
}{
{
name: "empty resources array should return error",
queryModel: backend.DataQuery{
JSON: []byte(fmt.Sprintf(`{
"queryType": "Azure Traces",
"azureTraces": {
"resources": [],
"resultFormat": "%s",
"traceTypes": ["trace"]
}
}`, dataquery.ResultFormatTable)),
RefID: "A",
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeAzureTraces),
},
expectedErrorString: "no resources specified for Azure traces query",
},
{
name: "missing resources field should return error",
queryModel: backend.DataQuery{
JSON: []byte(fmt.Sprintf(`{
"queryType": "Azure Traces",
"azureTraces": {
"resultFormat": "%s",
"traceTypes": ["trace"]
}
}`, dataquery.ResultFormatTable)),
RefID: "A",
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeAzureTraces),
},
expectedErrorString: "no resources specified for Azure traces query",
},
{
name: "trace exemplar with empty correlation resources should return error",
queryModel: backend.DataQuery{
JSON: []byte(fmt.Sprintf(`{
"queryType": "Azure Traces",
"azureTraces": {
"resources": ["/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Insights/components/r1"],
"resultFormat": "%s",
"traceTypes": ["trace"],
"operationId": "missing-op-id"
}
}`, dataquery.ResultFormatTable)),
RefID: "A",
TimeRange: timeRange,
QueryType: string(dataquery.AzureQueryTypeTraceExemplar),
},
expectedErrorString: "no correlation resources found for trace exemplar query with operation ID: missing-op-id",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query, err := buildAppInsightsQuery(ctx, tt.queryModel, dsInfo, appInsightsRegExp, logger)
require.Error(t, err)
require.Nil(t, query)
require.Contains(t, err.Error(), tt.expectedErrorString)
})
}
}