Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d24955b
Add PermissionsToExclude property and initialization
bartizan Aug 11, 2025
d3c3948
Move ApiSpecsFolderPath property initialization to dedicated method I…
bartizan Aug 11, 2025
3ffb3ba
Small code adjustments
bartizan Aug 11, 2025
1cda8dd
Move GetMethodAndUrl to common convert method ToMethodAndUrl
bartizan Aug 12, 2025
826d644
Add MethodAndUrl record instead of tuple and MethodAndUrlComparer
bartizan Aug 13, 2025
ea0c49b
Move MethodAndUrlUtils to GraphUtils unit
bartizan Aug 13, 2025
1905de1
Move common GetRequestsFromBatch and GetTokenizedUrl methods to Graph…
bartizan Aug 13, 2025
87f2048
Exclude permissions from excessive list
bartizan Aug 14, 2025
b8442f4
Add ExcludedPermissions property to report class
bartizan Aug 14, 2025
f0698ce
Add logging of what permissions are excluded
bartizan Aug 14, 2025
7517090
Add permissionsToExclude to MinimalPermissionsGuidancePlugin config s…
bartizan Aug 14, 2025
02eba7f
Update schema only for v1.1.0
bartizan Aug 15, 2025
65e8cf7
Lower accessibility level to internal for GetMethodAndUrl, GetTokeniz…
bartizan Aug 15, 2025
7555b0c
Move GetMethodAndUrl func out to MethodAndUrlUtils unit
bartizan Aug 15, 2025
63291d9
Evaluate excessivePermissions and properly initiate flag UsesMinimalP…
bartizan Aug 15, 2025
6c89a22
Merge branch 'main' into 1052_add-scopes-to-ignore-in-MinimalPermissi…
waldekmastykarz Aug 26, 2025
04b0ac1
Refactor: Remove unused using directives and improve exception handli…
waldekmastykarz Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions DevProxy.Plugins/Generation/MockGeneratorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Mocking;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Titanium.Web.Proxy.EventArguments;
Expand All @@ -34,7 +33,6 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation

Logger.LogInformation("Creating mocks from recorded requests...");

var methodAndUrlComparer = new MethodAndUrlComparer();
var mocks = new List<MockResponse>();

foreach (var request in e.RequestLogs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
Expand Down Expand Up @@ -77,9 +76,8 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
return;
}

var methodAndUrlComparer = new MethodAndUrlComparer();
var delegatedEndpoints = new List<(string method, string url)>();
var applicationEndpoints = new List<(string method, string url)>();
var delegatedEndpoints = new List<MethodAndUrl>();
var applicationEndpoints = new List<MethodAndUrl>();

// scope for delegated permissions
IEnumerable<string> scopesToEvaluate = [];
Expand All @@ -94,21 +92,21 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
}

var methodAndUrlString = request.Message;
var methodAndUrl = GetMethodAndUrl(methodAndUrlString);
if (methodAndUrl.method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
var methodAndUrl = MethodAndUrlUtils.GetMethodAndUrl(methodAndUrlString);
if (methodAndUrl.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
{
continue;
}

if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.url))
if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.Url))
{
Logger.LogDebug("URL not matched: {Url}", methodAndUrl.url);
Logger.LogDebug("URL not matched: {Url}", methodAndUrl.Url);
continue;
}

var requestsFromBatch = Array.Empty<(string method, string url)>();
var requestsFromBatch = Array.Empty<MethodAndUrl>();

var uri = new Uri(methodAndUrl.url);
var uri = new Uri(methodAndUrl.Url);
if (!ProxyUtils.IsGraphUrl(uri))
{
continue;
Expand All @@ -117,11 +115,11 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
if (ProxyUtils.IsGraphBatchUrl(uri))
{
var graphVersion = ProxyUtils.IsGraphBetaUrl(uri) ? "beta" : "v1.0";
requestsFromBatch = GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host);
requestsFromBatch = GraphUtils.GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host);
}
else
{
methodAndUrl = (methodAndUrl.method, GetTokenizedUrl(methodAndUrl.url));
methodAndUrl = new(methodAndUrl.Method, GraphUtils.GetTokenizedUrl(methodAndUrl.Url));
}

var (type, permissions) = GetPermissionsAndType(request);
Expand Down Expand Up @@ -162,8 +160,8 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
}

// Remove duplicates
delegatedEndpoints = [.. delegatedEndpoints.Distinct(methodAndUrlComparer)];
applicationEndpoints = [.. applicationEndpoints.Distinct(methodAndUrlComparer)];
delegatedEndpoints = [.. delegatedEndpoints.Distinct()];
applicationEndpoints = [.. applicationEndpoints.Distinct()];

if (delegatedEndpoints.Count == 0 && applicationEndpoints.Count == 0)
{
Expand All @@ -177,8 +175,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation

Logger.LogWarning("This plugin is in preview and may not return the correct results.\r\nPlease review the permissions and test your app before using them in production.\r\nIf you have any feedback, please open an issue at https://aka.ms/devproxy/issue.\r\n");

if (Configuration.PermissionsToExclude is not null &&
Configuration.PermissionsToExclude.Any())
if (Configuration.PermissionsToExclude?.Any() == true)
{
Logger.LogInformation("Excluding the following permissions: {Permissions}", string.Join(", ", Configuration.PermissionsToExclude));
}
Expand All @@ -188,7 +185,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
var delegatedPermissionsInfo = new GraphMinimalPermissionsInfo();
report.DelegatedPermissions = delegatedPermissionsInfo;

Logger.LogInformation("Evaluating delegated permissions for: {Endpoints}", string.Join(", ", delegatedEndpoints.Select(e => $"{e.method} {e.url}")));
Logger.LogInformation("Evaluating delegated permissions for: {Endpoints}", string.Join(", ", delegatedEndpoints.Select(e => $"{e.Method} {e.Url}")));

await EvaluateMinimalScopesAsync(delegatedEndpoints, scopesToEvaluate, GraphPermissionsType.Delegated, delegatedPermissionsInfo, cancellationToken);
}
Expand All @@ -198,7 +195,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
var applicationPermissionsInfo = new GraphMinimalPermissionsInfo();
report.ApplicationPermissions = applicationPermissionsInfo;

Logger.LogInformation("Evaluating application permissions for: {Endpoints}", string.Join(", ", applicationEndpoints.Select(e => $"{e.method} {e.url}")));
Logger.LogInformation("Evaluating application permissions for: {Endpoints}", string.Join(", ", applicationEndpoints.Select(e => $"{e.Method} {e.Url}")));

await EvaluateMinimalScopesAsync(applicationEndpoints, rolesToEvaluate, GraphPermissionsType.Application, applicationPermissionsInfo, cancellationToken);
}
Expand All @@ -218,7 +215,7 @@ private void InitializePermissionsToExclude()
}

private async Task EvaluateMinimalScopesAsync(
IEnumerable<(string method, string url)> endpoints,
IEnumerable<MethodAndUrl> endpoints,
IEnumerable<string> permissionsFromAccessToken,
GraphPermissionsType scopeType,
GraphMinimalPermissionsInfo permissionsInfo,
Expand All @@ -229,12 +226,12 @@ private async Task EvaluateMinimalScopesAsync(
throw new InvalidOperationException("GraphUtils is not initialized. Make sure to call InitializeAsync first.");
}

var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.method, Url = e.url });
var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.Method, Url = e.Url });

permissionsInfo.Operations = [.. endpoints.Select(e => new GraphMinimalPermissionsOperationInfo
{
Method = e.method,
Endpoint = e.url
Method = e.Method,
Endpoint = e.Url
})];
permissionsInfo.PermissionsFromTheToken = permissionsFromAccessToken;

Expand Down Expand Up @@ -290,40 +287,6 @@ private async Task EvaluateMinimalScopesAsync(
}
}

private static (string method, string url)[] GetRequestsFromBatch(string batchBody, string graphVersion, string graphHostName)
{
var requests = new List<(string method, string url)>();

if (string.IsNullOrEmpty(batchBody))
{
return [.. requests];
}

try
{
var batch = JsonSerializer.Deserialize<GraphBatchRequestPayload>(batchBody, ProxyUtils.JsonSerializerOptions);
if (batch == null)
{
return [.. requests];
}

foreach (var request in batch.Requests)
{
try
{
var method = request.Method;
var url = request.Url;
var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}";
requests.Add((method, GetTokenizedUrl(absoluteUrl)));
}
catch { }
}
}
catch { }

return [.. requests];
}

/// <summary>
/// Returns permissions and type (delegated or application) from the access token
/// used on the request.
Expand Down Expand Up @@ -377,20 +340,4 @@ private static (GraphPermissionsType type, IEnumerable<string> permissions) GetP
return (GraphPermissionsType.Application, []);
}
}

private static (string method, string url) GetMethodAndUrl(string message)
{
var info = message.Split(" ");
if (info.Length > 2)
{
info = [info[0], string.Join(" ", info.Skip(1))];
}
return (method: info[0], url: info[1]);
}

private static string GetTokenizedUrl(string absoluteUrl)
{
var sanitizedUrl = ProxyUtils.SanitizeUrl(absoluteUrl);
return "/" + string.Join("", new Uri(sanitizedUrl).Segments.Skip(2).Select(Uri.UnescapeDataString));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ void transformPermissionsInfo(GraphMinimalPermissionsInfo permissionsInfo, strin
transformPermissionsInfo(ApplicationPermissions, "application");
}

if (ExcludedPermissions is not null &&
ExcludedPermissions.Any())
if (ExcludedPermissions?.Any() == true)
{
_ = sb.AppendLine("## Excluded permissions")
.AppendLine()
Expand Down Expand Up @@ -112,10 +111,9 @@ void transformPermissionsInfo(GraphMinimalPermissionsInfo permissionsInfo, strin
transformPermissionsInfo(ApplicationPermissions, "Application");
}

if (ExcludedPermissions is not null &&
ExcludedPermissions.Any())
if (ExcludedPermissions?.Any() == true)
{
_ = sb.AppendLine("Excluded: permissions:")
_ = sb.AppendLine("Excluded permissions:")
.AppendLine()
.AppendLine(string.Join(", ", ExcludedPermissions));
}
Expand Down
76 changes: 12 additions & 64 deletions DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
using DevProxy.Abstractions.Models;
using DevProxy.Plugins.Models;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -60,8 +59,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
return;
}

var methodAndUrlComparer = new MethodAndUrlComparer();
var endpoints = new List<(string method, string url)>();
var endpoints = new List<MethodAndUrl>();

foreach (var request in e.RequestLogs)
{
Expand All @@ -71,19 +69,19 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
}

var methodAndUrlString = request.Message;
var methodAndUrl = GetMethodAndUrl(methodAndUrlString);
if (methodAndUrl.method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
var methodAndUrl = MethodAndUrlUtils.GetMethodAndUrl(methodAndUrlString);
if (methodAndUrl.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
{
continue;
}

if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.url))
if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.Url))
{
Logger.LogDebug("URL not matched: {Url}", methodAndUrl.url);
Logger.LogDebug("URL not matched: {Url}", methodAndUrl.Url);
continue;
}

var uri = new Uri(methodAndUrl.url);
var uri = new Uri(methodAndUrl.Url);
if (!ProxyUtils.IsGraphUrl(uri))
{
continue;
Expand All @@ -92,26 +90,26 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
if (ProxyUtils.IsGraphBatchUrl(uri))
{
var graphVersion = ProxyUtils.IsGraphBetaUrl(uri) ? "beta" : "v1.0";
var requestsFromBatch = GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host);
var requestsFromBatch = GraphUtils.GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host);
endpoints.AddRange(requestsFromBatch);
}
else
{
methodAndUrl = (methodAndUrl.method, GetTokenizedUrl(methodAndUrl.url));
methodAndUrl = new(methodAndUrl.Method, GraphUtils.GetTokenizedUrl(methodAndUrl.Url));
endpoints.Add(methodAndUrl);
}
}

// Remove duplicates
endpoints = [.. endpoints.Distinct(methodAndUrlComparer)];
endpoints = [.. endpoints.Distinct()];

if (endpoints.Count == 0)
{
Logger.LogInformation("No requests to Microsoft Graph endpoints recorded. Will not retrieve minimal permissions.");
return;
}

Logger.LogInformation("Retrieving minimal permissions for:\r\n{Endpoints}\r\n", string.Join(Environment.NewLine, endpoints.Select(e => $"- {e.method} {e.url}")));
Logger.LogInformation("Retrieving minimal permissions for:\r\n{Endpoints}\r\n", string.Join(Environment.NewLine, endpoints.Select(e => $"- {e.Method} {e.Url}")));

Logger.LogWarning("This plugin is in preview and may not return the correct results.\r\nPlease review the permissions and test your app before using them in production.\r\nIf you have any feedback, please open an issue at https://aka.ms/devproxy/issue.\r\n");

Expand All @@ -125,15 +123,15 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
}

private async Task<GraphMinimalPermissionsPluginReport?> DetermineMinimalScopesAsync(
IEnumerable<(string method, string url)> endpoints,
IEnumerable<MethodAndUrl> endpoints,
CancellationToken cancellationToken)
{
if (_graphUtils is null)
{
throw new InvalidOperationException("GraphUtils is not initialized. Make sure to call InitializeAsync first.");
}

var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.method, Url = e.url });
var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.Method, Url = e.Url });

try
{
Expand Down Expand Up @@ -178,54 +176,4 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
return null;
}
}

private static (string method, string url)[] GetRequestsFromBatch(string batchBody, string graphVersion, string graphHostName)
{
var requests = new List<(string, string)>();

if (string.IsNullOrEmpty(batchBody))
{
return [.. requests];
}

try
{
var batch = JsonSerializer.Deserialize<GraphBatchRequestPayload>(batchBody, ProxyUtils.JsonSerializerOptions);
if (batch == null)
{
return [.. requests];
}

foreach (var request in batch.Requests)
{
try
{
var method = request.Method;
var url = request.Url;
var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}";
requests.Add((method, GetTokenizedUrl(absoluteUrl)));
}
catch { }
}
}
catch { }

return [.. requests];
}

private static (string method, string url) GetMethodAndUrl(string message)
{
var info = message.Split(" ");
if (info.Length > 2)
{
info = [info[0], string.Join(" ", info.Skip(1))];
}
return (info[0], info[1]);
}

private static string GetTokenizedUrl(string absoluteUrl)
{
var sanitizedUrl = ProxyUtils.SanitizeUrl(absoluteUrl);
return "/" + string.Join("", new Uri(sanitizedUrl).Segments.Skip(2).Select(Uri.UnescapeDataString));
}
}
Loading