Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit abcaad9

Browse files
JasonYeMSFTjongio
andauthored
Add extension azqr tool (#510)
* Implement extension azqr tool * Fix tool description * Add to tool description on when to use the tool, format code * Generate both xlsx and json report Disable scanning costs to reduce chance of hitting throttling error * Add azqr to cspell.json * Add readme and test prompt * Add example commands Fix merged code from main * Use global options for subscription and resource group * Fix global command not registering resource group option * Only register resource group option from azqr command * Add azqr tests Use azqr specific resource group option to make it non required Update docs * Fix format issue * Reorder tool description in readme to follow alphabetical order * Removed unneeded double quote in argument placeholder * Incorporate PR feedback * Fix tests by mocking azqr exe existence * Reuse global option Add changelog entry * Use azqr's own resource group option * Add forward link to the official azqr GitHub repo to the command not found error message --------- Co-authored-by: Jon Gallant <2163001+jongio@users.noreply.github.com>
1 parent df3a60b commit abcaad9

12 files changed

Lines changed: 320 additions & 1 deletion

File tree

.vscode/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
"Apim",
167167
"appconfig",
168168
"azmcp",
169+
"azqr",
169170
"azuremcp",
170171
"azsdk",
171172
"azureblob",

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
### Features Added
66
- Support for Azure Load testing operations - Modify load testing resource, test and test runs. [#315](https://github.com/Azure/azure-mcp/pull/315)
7+
- Added support for scanning Azure resources for compliance recommendations using the Azure Quick Review CLI via the command: `azmcp extension azqr`. [#510](https://github.com/Azure/azure-mcp/pull/510)
78
- Support for Azure Data Lake Storage Gen2 operations - List paths in Data Lake file systems via the command: `azmcp storage datalake file-system list-paths`. [#608](https://github.com/Azure/azure-mcp/pull/608)
89
- Added new commands for Azure Function code generation and deployment best practices (https://github.com/Azure/azure-mcp/pull/630)
910
- Add `azmcp sql firewall-rule list` command to list SQL server firewall rules. [[#610](https://github.com/Azure/azure-mcp/pull/610)]
10-
- Added support for listing SQL elastic pools via the command: `azmcp sql elastic-pool list`. [[#581](https://github.com/Azure/azure-mcp/pull/581)]
11+
12+
### Breaking Changes
1113

1214
### Bugs Fixed
1315
- Fixed Azure CLI executable path resolution on Windows to prioritize .cmd over bash script. [[#611](https://github.com/Azure/azure-mcp/issues/611)]

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
165165
### ⚙️ Azure Native ISV Services
166166
- List Monitored Resources in a Datadog Monitor
167167

168+
### Azure Quick Review CLI Extension
169+
- Scan Azure resources for compliance related recommendations
170+
168171
### � Azure Resource Groups
169172
- List resource groups
170173

docs/azmcp-commands.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,17 @@ azmcp subscription list [--tenant-id <tenant-id>]
681681
azmcp azureterraformbestpractices get
682682
```
683683

684+
### Azure Quick Review CLI Extension Operations
685+
```base
686+
# Scan a subscription for recommendations
687+
azmcp extension azqr --subscription <subscription>
688+
689+
# Scan a subscription and scope to a specific resource group
690+
azmcp extension azqr --subscription <subscription> --resource-group <resource-group-name>
691+
```
692+
693+
### Azure AI Search
694+
684695
### Bicep
685696

686697
```bash

e2eTests/e2eTestPrompts.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,11 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
294294
| Tool Name | Test Prompt |
295295
|:----------|:----------|
296296
| azmcp-bicepschema-get | How can I use Bicep to create an Azure OpenAI service? |
297+
298+
## Azure Quick Review CLI
299+
300+
| Tool Name | Test Prompt |
301+
| --------- | ----------- |
302+
| azmcp-extension-azqr | Scan my Azure subscription for compliance recommendations |
303+
| azmcp-extension-azqr | Check my Azure subscription for any compliance issues or recommendations |
304+
| azmcp-extension-azqr | Provide compliance recommendations for my current Azure subscription |
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Runtime.InteropServices;
5+
using AzureMcp.Areas.Extension.Options;
6+
using AzureMcp.Commands.Subscription;
7+
using AzureMcp.Services.Azure.Subscription;
8+
using AzureMcp.Services.ProcessExecution;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace AzureMcp.Areas.Extension.Commands;
12+
13+
public sealed class AzqrCommand(ILogger<AzqrCommand> logger, int processTimeoutSeconds = 300) : SubscriptionCommand<AzqrOptions>()
14+
{
15+
private const string CommandTitle = "Azure Quick Review CLI Command";
16+
private readonly ILogger<AzqrCommand> _logger = logger;
17+
private readonly int _processTimeoutSeconds = processTimeoutSeconds;
18+
private static string? _cachedAzqrPath;
19+
20+
public override string Name => "azqr";
21+
22+
public override string Description =>
23+
"""
24+
Runs Azure Quick Review CLI (azqr) commands to generate compliance/security reports for Azure resources.
25+
This tool should be used when the user wants to identify any non-compliant configurations or areas for improvement in their Azure resources.
26+
Requires a subscription id and optionally a resource group name. Returns the generated report file's path.
27+
Note that Azure Quick Review CLI (azqr) is different from Azure CLI (az).
28+
""";
29+
30+
public override string Title => CommandTitle;
31+
32+
protected override void RegisterOptions(Command command)
33+
{
34+
base.RegisterOptions(command);
35+
command.AddOption(ExtensionOptionDefinitions.Azqr.OptionalResourceGroup);
36+
}
37+
38+
[McpServerTool(Destructive = false, ReadOnly = true, Title = CommandTitle)]
39+
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
40+
{
41+
var options = BindOptions(parseResult);
42+
var response = context.Response;
43+
try
44+
{
45+
if (!Validate(parseResult.CommandResult, response).IsValid)
46+
{
47+
return response;
48+
}
49+
50+
ArgumentNullException.ThrowIfNull(options.Subscription);
51+
52+
var azqrPath = FindAzqrCliPath() ?? throw new FileNotFoundException("Azure Quick Review CLI (azqr) executable not found in PATH. Please ensure azqr is installed. Go to https://aka.ms/azqr to learn more about how to install Azure Quick Review CLI.");
53+
54+
var subscriptionService = context.GetService<ISubscriptionService>();
55+
var subscription = await subscriptionService.GetSubscription(options.Subscription, options.Tenant);
56+
57+
// Compose azqr command
58+
var command = $"scan --subscription-id {subscription.Id}";
59+
if (!string.IsNullOrWhiteSpace(options.ResourceGroup))
60+
{
61+
command += $" --resource-group {options.ResourceGroup}";
62+
}
63+
64+
var tempDir = Path.GetTempPath();
65+
var dateString = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss");
66+
var reportFileName = Path.Combine(tempDir, $"azqr-report-{options.Subscription}-{dateString}");
67+
68+
// Azure Quick Review always appends the file extension to the report file's name, we need to create a new path with the file extension to check for the existence of the report file.
69+
var xlsxReportFilePath = $"{reportFileName}.xlsx";
70+
var jsonReportFilePath = $"{reportFileName}.json";
71+
command += $" --output-name \"{reportFileName}\"";
72+
73+
// Azure Quick Review CLI can easily get throttle errors when scanning subscriptions with many resources with costs enabled.
74+
// Unfortunately, getting such an error will abort the entire job and waste all the partial results.
75+
// To reduce the chance of throttling, we disable costs reporting by default.
76+
command += " --costs=false";
77+
78+
// Also generate a JSON report for users who don't have access to Excel.
79+
command += " --json";
80+
81+
var processService = context.GetService<IExternalProcessService>();
82+
var result = await processService.ExecuteAsync(azqrPath, command, _processTimeoutSeconds);
83+
84+
if (result.ExitCode != 0)
85+
{
86+
response.Status = 500;
87+
response.Message = result.Error;
88+
return response;
89+
}
90+
91+
if (!File.Exists(xlsxReportFilePath) && !File.Exists(jsonReportFilePath))
92+
{
93+
response.Status = 500;
94+
response.Message = $"Report file '{xlsxReportFilePath}' and '{jsonReportFilePath}' were not found after azqr execution.";
95+
return response;
96+
}
97+
var resultObj = new AzqrReportResult(xlsxReportFilePath, jsonReportFilePath, result.Output);
98+
response.Results = ResponseResult.Create(resultObj, JsonSourceGenerationContext.Default.AzqrReportResult);
99+
response.Status = 200;
100+
response.Message = "azqr report generated successfully.";
101+
return response;
102+
}
103+
catch (Exception ex)
104+
{
105+
_logger.LogError(ex, "An exception occurred executing azqr command.");
106+
HandleException(context, ex);
107+
return response;
108+
}
109+
}
110+
111+
private static string? FindAzqrCliPath()
112+
{
113+
// Return cached path if available and still exists
114+
if (!string.IsNullOrEmpty(_cachedAzqrPath) && File.Exists(_cachedAzqrPath))
115+
{
116+
return _cachedAzqrPath;
117+
}
118+
var exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "azqr.exe" : "azqr";
119+
var pathEnv = Environment.GetEnvironmentVariable("PATH");
120+
if (string.IsNullOrEmpty(pathEnv))
121+
return null;
122+
foreach (var dir in pathEnv.Split(Path.PathSeparator))
123+
{
124+
var fullPath = Path.Combine(dir.Trim(), exeName);
125+
if (File.Exists(fullPath))
126+
{
127+
_cachedAzqrPath = fullPath;
128+
return fullPath;
129+
}
130+
}
131+
return null;
132+
}
133+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace AzureMcp.Areas.Extension.Commands;
7+
8+
// This file should only contain a single definition of AzqrReportResult
9+
public sealed record AzqrReportResult(
10+
[property: JsonPropertyName("xlsxReportPath")] string XlsxReportPath,
11+
[property: JsonPropertyName("jsonReportPath")] string JsonReportPath,
12+
[property: JsonPropertyName("stdout")] string Stdout
13+
);

src/Areas/Extension/ExtensionSetup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor
2222

2323
extension.AddCommand("az", new AzCommand(loggerFactory.CreateLogger<AzCommand>()));
2424
extension.AddCommand("azd", new AzdCommand(loggerFactory.CreateLogger<AzdCommand>()));
25+
extension.AddCommand("azqr", new AzqrCommand(loggerFactory.CreateLogger<AzqrCommand>()));
2526
}
2627
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
using AzureMcp.Options;
6+
7+
namespace AzureMcp.Areas.Extension.Options;
8+
9+
public class AzqrOptions : SubscriptionOptions;

src/Areas/Extension/Options/ExtensionOptionDefinitions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using AzureMcp.Models.Option;
5+
46
namespace AzureMcp.Areas.Extension.Options;
57

68
public static class ExtensionOptionDefinitions
@@ -71,4 +73,15 @@ The name of the azd environment to use. This is typically the name of the Azure
7173
IsRequired = false
7274
};
7375
}
76+
77+
public static class Azqr
78+
{
79+
public static readonly Option<string> OptionalResourceGroup = new(
80+
$"--{OptionDefinitions.Common.ResourceGroupName}",
81+
"The name of the Azure resource group. This is a logical container for Azure resources."
82+
)
83+
{
84+
IsRequired = false
85+
};
86+
}
7487
}

0 commit comments

Comments
 (0)