From c3ecfee6dc5e8a8bdf89e5772df598b95f0e755b Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 5 Dec 2023 13:13:42 -0500 Subject: [PATCH] basic functionality of replay log working --- .gitignore | 2 + src/kibali/AuthZChecker.cs | 6 +- src/kibali/PermissionsDocument.cs | 4 + src/kibaliTool/LogEntry.cs | 106 ++++++++++++++++ src/kibaliTool/Program.cs | 31 ++++- src/kibaliTool/ReplayLogCommand.cs | 118 ++++++++++++++++++ src/kibaliTool/TestCommand.cs | 99 --------------- src/kibaliTool/kibaliTool.sln | 25 ++++ test/kibaliTests/KibaliTests.csproj | 1 + .../ReplayLogTests/ParseLogtests.cs | 34 +++++ 10 files changed, 325 insertions(+), 101 deletions(-) create mode 100644 src/kibaliTool/LogEntry.cs create mode 100644 src/kibaliTool/ReplayLogCommand.cs delete mode 100644 src/kibaliTool/TestCommand.cs create mode 100644 src/kibaliTool/kibaliTool.sln create mode 100644 test/kibaliTests/ReplayLogTests/ParseLogtests.cs diff --git a/.gitignore b/.gitignore index 13cbaa0..ea57e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +graphlog.json + # User-specific files *.rsuser *.suo diff --git a/src/kibali/AuthZChecker.cs b/src/kibali/AuthZChecker.cs index 10f501a..e880738 100644 --- a/src/kibali/AuthZChecker.cs +++ b/src/kibali/AuthZChecker.cs @@ -177,7 +177,11 @@ private static OpenApiUrlTreeNode CreateUrlTree(Dictionary x.Key, x => x.Value); diff --git a/src/kibaliTool/LogEntry.cs b/src/kibaliTool/LogEntry.cs new file mode 100644 index 0000000..05b41d4 --- /dev/null +++ b/src/kibaliTool/LogEntry.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace KibaliTool +{ + /* To produce the log file you can use this Kusto query and then Export to JSON + + AggregatorServiceLogEvent + | where env_time > ago(1h) + | where tagId == 30746268 + | where responseStatusCode >= 200 and responseStatusCode < 300 + | project correlationId, requestMethod, incomingUri, tokenClaims, responseStatusCode,tagId + | limit 10000 + */ + + + // "TableName":"Results", + // "Columns":[ {"ColumnName":"correlationId","DataType":"String"}, + // {"ColumnName":"requestMethod","DataType":"String"}, + // {"ColumnName":"incomingUri","DataType":"String"}, + // {"ColumnName":"tokenClaims","DataType":"String"}, + // {"ColumnName":"responseStatusCode","DataType":"Int64"}, + // {"ColumnName":"tagId","DataType":"String"}] + + public class LogEntry + { + + public string Method; + public string Scheme; + public string Url; + public string Claims; + + private Regex scpRegex = new Regex(@"scp=([^\;]+)", RegexOptions.Compiled); + private Regex rolesRegex = new Regex(@"roles=([^\;]+)", RegexOptions.Compiled); + private Regex roleRegex = new Regex("\"(.*?)\"", RegexOptions.Compiled); + + public string[] Permissions; + + private void Process() { + + Url = NormalIzeUrl(Url); + + + // Calculate Scheme + Scheme = Claims.Contains("role") ? "Application" : "DelegatedWork"; + // Calculate Permissions + + string[] permissionList = System.Array.Empty(); + + if (Scheme == "DelegatedWork") + { + Match match = scpRegex.Match(Claims); + + if (match.Success) + { + string scpValues = match.Groups[1].Value.Trim(); + permissionList = scpValues.Split(' '); + } + } + else + { + Match match = rolesRegex.Match(Claims); + + if (match.Success) + { + string roleValues = match.Groups[1].Value.Trim(); + MatchCollection matches = roleRegex.Matches(roleValues); + permissionList = matches.Select(m => m.Groups[1].Value).ToArray(); + } + } + Permissions = permissionList; + } + + private string NormalIzeUrl(string url) + { + // Use regeg to tranform the url to use / as a segment separator + url = Regex.Replace(url, @"\(([^)]*)\)", "/$1").ToLower(); + + // Remove $value + url = Regex.Replace(url, @"\/\$value", string.Empty); + + return url; + } + + public static LogEntry Load(JsonElement row) { + var logentry = new LogEntry() + { + Method = row[RequestMethod].GetString(), + Url = row[IncomingUri].GetString(), + Claims = row[TokenClaims].GetString() + }; + logentry.Process(); + return logentry; + } + + private const int CorrelationId = 0; + private const int RequestMethod = 1; + private const int IncomingUri = 2; + + private const int TokenClaims = 3; + private const int ResponseStatusCode = 4; + private const int TagId = 5; + } +} \ No newline at end of file diff --git a/src/kibaliTool/Program.cs b/src/kibaliTool/Program.cs index d7271ed..3430f6b 100644 --- a/src/kibaliTool/Program.cs +++ b/src/kibaliTool/Program.cs @@ -53,13 +53,23 @@ static async Task Main(string[] args) documentCommand.SetHandler(DocumentCommand.Execute, new DocumentCommandBinder()); + Command replayLogCommand = new Command("replay-log") { + ReplayLogCommandBinder.LogFileOption, + ReplayLogCommandBinder.SourcePermissionsFolderOption, + ReplayLogCommandBinder.LenientMatchOption, + ReplayLogCommandBinder.CountOption, + }; + + replayLogCommand.SetHandler(ReplayLogCommand.Execute, new ReplayLogCommandBinder()); + var rootCommand = new RootCommand() { importCommand, queryCommand, exportCommand, validateCommand, - documentCommand + documentCommand, + replayLogCommand }; @@ -158,4 +168,23 @@ protected override DocumentCommandParameters GetBoundValue(BindingContext bindin }; } } + + internal class ReplayLogCommandBinder : BinderBase { + public static readonly Option LogFileOption = new(new[] { "--logFile", "--lf" }, "Log File"); + public static readonly Option SourcePermissionsFolderOption = new(new[] { "--sourcePermissionsFolder", "--fo" }, "Permission Folder"); + public static readonly Option LenientMatchOption = new(new[] { "--lenient", "--lm" }, "LenientMatch"); + public static readonly Option CountOption = new(new[] { "--count", "--c" }, "Count"); + + protected override ReplayLogCommandParameters GetBoundValue(BindingContext bindingContext) + { + return new ReplayLogCommandParameters() + { + LogFile = bindingContext.ParseResult.GetValueForOption(LogFileOption), + SourcePermissionsFolder = bindingContext.ParseResult.GetValueForOption(SourcePermissionsFolderOption), + LenientMatch = bindingContext.ParseResult.GetValueForOption(LenientMatchOption), + Count = bindingContext.ParseResult.GetValueForOption(CountOption), + }; + } + + } } diff --git a/src/kibaliTool/ReplayLogCommand.cs b/src/kibaliTool/ReplayLogCommand.cs new file mode 100644 index 0000000..291b861 --- /dev/null +++ b/src/kibaliTool/ReplayLogCommand.cs @@ -0,0 +1,118 @@ +using Kibali; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +namespace KibaliTool +{ + internal class ReplayLogCommandParameters + { + public string SourcePermissionsFolder; + public string LogFile; + public bool LenientMatch; + public int Count = 100; + } + + internal class ReplayLogCommand + { + + + public static async Task Execute(ReplayLogCommandParameters replayLogCommandParameters) + { + var doc = PermissionsDocument.LoadFromFolder(replayLogCommandParameters.SourcePermissionsFolder); + + var authZChecker = new AuthZChecker() { LenientMatch = replayLogCommandParameters.LenientMatch }; + authZChecker.Load(doc); + + // Read the JSON log file using a streaming API + using var logstream = new FileStream(replayLogCommandParameters.LogFile, FileMode.Open); + + IEnumerable entries = LoadLogEntries(logstream, replayLogCommandParameters.Count).ToList(); + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + using var writer = new Utf8JsonWriter(Console.OpenStandardOutput(), new JsonWriterOptions() { Indented = true,SkipValidation = true }); + writer.WriteStartArray(); + + int successRequests = 0; + + foreach (var entry in entries) + { + string failReason = null; + Dictionary> supportedSchemes = null; + List acceptableClaims = null; + List acceptablePermissions = new List(); + List relevantPermissions = new List(); + var resource = authZChecker.FindResource(entry.Url); + + if (resource == null) + { + failReason = "No matching resource"; + } + if (failReason == null && !resource.SupportedMethods.TryGetValue(entry.Method, out supportedSchemes)) + { + failReason = "No matching method"; + } + + if (failReason == null && !supportedSchemes.TryGetValue(entry.Scheme, out acceptableClaims)) + { + failReason = "No matching scheme"; + } + + if (failReason == null ) + { + acceptablePermissions = acceptableClaims.Select(c => c.Permission).ToList(); + relevantPermissions = entry.Permissions.Where(claim => acceptablePermissions.Contains(claim)).ToList(); + } + + if (failReason == null && !relevantPermissions.Any()) + { + failReason = "No matching permissions"; + } + if (failReason != null) { + writer.WriteStartObject(); + writer.WriteString("failReason", failReason); + writer.WriteString("url", entry.Url); + writer.WriteString("method", entry.Method); + writer.WriteString("scheme", entry.Scheme); + if (failReason == "No matching permissions") { + writer.WriteString("loggedClaims", String.Join(",", entry.Permissions)); + writer.WriteString("requiredClaims", String.Join(",", acceptablePermissions)); + } + writer.WriteEndObject(); + } else { + successRequests++; + } + } + stopwatch.Stop(); + + + writer.WriteEndArray(); + await writer.FlushAsync(); + + Console.WriteLine($"Elapsed time: {stopwatch.ElapsedMilliseconds} ms"); + Console.WriteLine($"Total success requests: {successRequests}"); + return 0; + + } + + + public static IEnumerable LoadLogEntries(Stream stream, int count) + { + var document = JsonDocument.Parse(stream); + var root = document.RootElement; + var rows = root.GetProperty("Rows"); + return rows.EnumerateArray().Take(count).Select(row => LogEntry.Load(row)); + } + + } +} diff --git a/src/kibaliTool/TestCommand.cs b/src/kibaliTool/TestCommand.cs deleted file mode 100644 index 2af55a7..0000000 --- a/src/kibaliTool/TestCommand.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Kibali; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Readers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading.Tasks; - -namespace KibaliTool -{ - internal class TestCommand - { - - - // private static async Task TestGraphAPI(PermissionsDocument permissionDocument) - // { - // var stream = new MemoryStream(); - // var result = await new OpenApiStreamReader().ReadAsync(stream); - // var openApiDoc = result.OpenApiDocument; - // var authZChecker = new AuthZChecker(); - // authZChecker.Load(permissionDocument); - - // //Create Application - - // //Create credentials to use for testing delegated permissions - // TokenCredential delegatedCredential = null; - // //Create credentials to use for testing application-only permissions - // TokenCredential appOnlyCredential = null; - - // foreach (var (name, permission) in permissionDocument.Permissions) - // { - // // Consent app to the permission - - // TestPaths(openApiDoc, (string url) => AccessRequestResult.Success == authZChecker.CanAccess(url, "GET", "DelegatedWork", new string[] { name }), delegatedCredential); - // TestPaths(openApiDoc, (string url) => AccessRequestResult.Success == authZChecker.CanAccess(url, "GET", "Application", new string[] { name }), appOnlyCredential); - - // // Remove consent from the app - // } - - // // Delete the application - // } - - // private static void TestPaths(OpenApiDocument openApiDoc, Func canAccess, TokenCredential credential) - // { - // foreach (var (url, pathItem) in openApiDoc.Paths) - // { - // foreach (var (method, operation) in pathItem.Operations) - // { - // List allowedUrls = null; - // HttpStatusCode status = HttpStatusCode.OK; - // switch (method) - // { - // case Microsoft.OpenApi.Models.OperationType.Get: - // status = TestGetOperation(operation); - // break; - // case Microsoft.OpenApi.Models.OperationType.Put: - // break; - // case Microsoft.OpenApi.Models.OperationType.Post: - // break; - // case Microsoft.OpenApi.Models.OperationType.Delete: - // break; - // case Microsoft.OpenApi.Models.OperationType.Patch: - // break; - // default: - // break; - // } - - // if (canAccess(url)) - // { - // if (status == HttpStatusCode.Forbidden) - // { - // // This is a problem - // } - // } - // else - // { - // if (status != HttpStatusCode.Forbidden) - // { - // // This is a problem - // } - // } - - // if (!allowedUrls.Contains(url) && status != HttpStatusCode.Forbidden) - // { - // // Danger will - // } - - // } - // } - // } - - private static HttpStatusCode TestGetOperation(OpenApiOperation operation) - { - throw new NotImplementedException(); - } - - } -} diff --git a/src/kibaliTool/kibaliTool.sln b/src/kibaliTool/kibaliTool.sln new file mode 100644 index 0000000..c7c00c8 --- /dev/null +++ b/src/kibaliTool/kibaliTool.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KibaliTool", "KibaliTool.csproj", "{64A3DD97-8352-4C7B-B51F-CC2395F0DF99}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {64A3DD97-8352-4C7B-B51F-CC2395F0DF99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64A3DD97-8352-4C7B-B51F-CC2395F0DF99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64A3DD97-8352-4C7B-B51F-CC2395F0DF99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64A3DD97-8352-4C7B-B51F-CC2395F0DF99}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5A2F1EEE-D868-4359-A9C0-0221F76C382A} + EndGlobalSection +EndGlobal diff --git a/test/kibaliTests/KibaliTests.csproj b/test/kibaliTests/KibaliTests.csproj index af89af5..2704e12 100644 --- a/test/kibaliTests/KibaliTests.csproj +++ b/test/kibaliTests/KibaliTests.csproj @@ -23,6 +23,7 @@ + diff --git a/test/kibaliTests/ReplayLogTests/ParseLogtests.cs b/test/kibaliTests/ReplayLogTests/ParseLogtests.cs new file mode 100644 index 0000000..b7cdb1e --- /dev/null +++ b/test/kibaliTests/ReplayLogTests/ParseLogtests.cs @@ -0,0 +1,34 @@ +// Create an xunit test that opens the graphlogs.json and parses the first fiew lines and write out the logentries + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Xunit; +using KibaliTool; + + +namespace KibaliTool.Tests +{ + public class ParseLogTests + { + [Fact] + public void ParseLog() + { + var logstream = new FileStream(@"C:\Users\darrmi\src\github\microsoftgraph\kibali\graphlog.json", FileMode.Open); + using var jsonDocument = JsonDocument.Parse(logstream); + var root = jsonDocument.RootElement; + var row = root.GetProperty("Rows").EnumerateArray().First(); + var entry = LogEntry.Load(row); + Assert.NotNull(entry.Method); + Assert.NotNull(entry.Url); + Assert.NotNull(entry.Claims); + Assert.NotNull(entry.Scheme); + Assert.NotNull(entry.Permissions); + } + + } +} \ No newline at end of file