From 72cfde4f9869907da570e7f499f7fb055a8d0f26 Mon Sep 17 00:00:00 2001 From: Mayyhem <30671833+Mayyhem@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:10:32 -0500 Subject: [PATCH] Updated JSON parsing logic for CMPivot --- Program.cs | 2 +- Properties/AssemblyInfo.cs | 4 +- RELEASE_NOTES.md | 4 + lib/AdminService.cs | 146 ++++++++++++------------------------- 4 files changed, 52 insertions(+), 104 deletions(-) diff --git a/Program.cs b/Program.cs index cb84b82..6b2fa6c 100644 --- a/Program.cs +++ b/Program.cs @@ -624,7 +624,7 @@ static void Main(string[] args) invokeAdminService.AddOption(new Option(new[] { "--collection-id", "-i" }, "The collectionId to point the query to. (e.g., SMS00001 for all systems collection)") { Arity = ArgumentArity.ExactlyOne }); invokeAdminService.Add(new Option(new[] { "--resource-id", "-r" }, "The unique ResourceID of the device to point the query to. Please see command \"get resourceId\" to retrieve the ResourceID for a user or device") { Arity = ArgumentArity.ExactlyOne }); invokeAdminService.Add(new Option(new[] { "--delay", "-d" }, "Seconds between requests when checking for results from the API,(e.g., --delay 5) (default: requests are made every 5 seconds)")); - invokeAdminService.Add(new Option(new[] { "--retries", "-re" }, "The total number of attempts to check for results from the API before a timeout is thrown.\n (e.g., --timeout 5) (default: 5 attempts will be made before a timeout")); + invokeAdminService.Add(new Option(new[] { "--retries", "-re" }, "The total number of attempts to check for results from the API before a timeout is thrown.\n (e.g., --retries 5) (default: 5 attempts will be made before a timeout")); invokeAdminService.Add(new Option(new[] { "--json", "-j" }, "Get JSON output")); invokeAdminService.Handler = CommandHandler.Create( async (string smsProvider, string siteCode, string query, string collectionId, string resourceId, string delay, string retries, bool json) => diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index f5773ba..978cd04 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -27,5 +27,5 @@ // Minor Version // Revision // -[assembly: AssemblyVersion("2.0.4")] -[assembly: AssemblyFileVersion("2.0.4")] +[assembly: AssemblyVersion("2.0.5")] +[assembly: AssemblyFileVersion("2.0.5")] diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1fc0b4d..9c6ad07 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # SharpSCCM Release Notes +### Version 2.0.5 (January 29, 2024) +##### Changes +- Fixed issue 40 + ### Version 2.0.4 (January 29, 2024) ##### Changes - Fixed issue 39 diff --git a/lib/AdminService.cs b/lib/AdminService.cs index 578e58b..3fa65e2 100644 --- a/lib/AdminService.cs +++ b/lib/AdminService.cs @@ -130,9 +130,7 @@ public static string TriggerAdminServiceQuery(string managementPoint, string sit //This function will periodically check the response status we get when check that an operation has completed //By default it will make 5 attempts before exiting this value might need to be modified when working on larger environments public static async Task CheckOperationStatusAsync(string managementPoint, string sitecode, string query, string collectionName, string deviceId, string[] timeoutValues, bool json) - { - query = !string.IsNullOrEmpty(query) ? Helpers.EscapeBackslashes(query) : null; var opId = TriggerAdminServiceQuery(managementPoint, sitecode, query, collectionName, deviceId); string url = null; @@ -179,7 +177,6 @@ public static async Task CheckOperationStatusAsync(string managementPoin if (status == 200) { - //Success message after retrieving operation results data Console.WriteLine("[+] Successfully retrieved results from AdminService\r"); //Here we start deserializing the received JSON @@ -197,124 +194,71 @@ public static async Task CheckOperationStatusAsync(string managementPoin // The file content query returns files line by line. We use this to output lines together if (query.Contains("FileContent(")) { - JObject obj = JObject.Parse(jsonBody); - JArray results = (JArray)obj["value"]["Result"]; - if (results.Count != 0) - { - StringBuilder sb = new StringBuilder(); - foreach (JObject result in results) - { - sb.AppendLine(result["Content"].ToString()); - } + JObject parsedJson = JObject.Parse(jsonBody); + JArray values = (JArray)parsedJson["value"]; - string contentString = sb.ToString(); - Console.WriteLine("\r--------------- File Contents ---------------\r"); - Console.WriteLine(contentString + "\r--------------------------------------------\r"); + if (values == null) + { + Console.WriteLine("[!] The retrieved results for the FileContent operation came back empty. Make sure the file exists or check query syntax"); return null; } - Console.WriteLine("[!] The retrieved results for the FileContent operation came back empty. Make sure the file exists or check query syntax"); - return null; - } - - //This section deals with the variation in nesting between single device queries and collection queries - JToken result1 = null; - JObject jsonObject2 = JObject.Parse(reqBody); - int resultIndex = -1; // if "Result" property not found - //Find "Result" within dictionary - foreach (JToken token in jsonObject2.Descendants()) - { - if (token.Type == JTokenType.Property && ((JProperty)token).Name == "Result") + foreach (JObject valueObject in values) { - JContainer parent = token.Parent; - if (parent is JObject) - { - resultIndex = ((JObject)parent).Properties().ToList().IndexOf((JProperty)token); - result1 = ((JProperty)token).Value; - } - else if (parent is JArray) + JArray results = (JArray)valueObject["Result"]; + if (results == null) continue; + + StringBuilder fileContent = new StringBuilder(); + string device = string.Empty; + + foreach (JObject result in results) { - resultIndex = ((JArray)parent).IndexOf(token); + device = result["Device"]?.ToString(); + string contentLine = result["Content"]?.ToString(); + if (contentLine != null) + { + fileContent.AppendLine(contentLine); + } } - break; + Console.WriteLine("Device: " + device); + Console.WriteLine("Content:\n" + fileContent); + Console.WriteLine("----------------------------------------"); } + return jsonObject.ToString(); } - var output = new StringBuilder(); - int counter2 = 1; - string header; + // For other queries, print each key value pair + JObject parsedJsonA = JObject.Parse(jsonBody); + JArray valuesA = (JArray)parsedJsonA["value"]; - // Here we start parsing the JSON to display it in a command line and make it as readable as possible - foreach (var item in result1) - { - header = $"---------------- CMPivot data #{counter2} ------------------"; - output.AppendLine(string.Format(header)); - counter2++; + if (valuesA == null) return null; - foreach (JProperty property in item.Children()) + Console.WriteLine("----------------------------------------"); + foreach (JObject valueObject in valuesA) + { + // Check if 'Result' exists, is not null, and is a JArray + if (valueObject["Result"] is JArray results) { - output.AppendLine(); - int numSpaces = 30 - property.Name.Length; - string pad1 = new string(' ', numSpaces); - output.Append(property.Name + pad1 + ": "); - - //When testing against Windows EventLog queries. The EventLog message contains very long string which contains a mix of nested - //Json-like key:value pairs and some regular strings. This was difficult to parse but here follows my attempt at making it presentable in a commandline - if (property.Value is JValue jValue) + foreach (JObject result in results) { - if (jValue.Type == JTokenType.String && jValue.ToString().Contains(Environment.NewLine)) + string device = result["Device"]?.ToString(); + Console.WriteLine("Device: " + device); + + foreach (var property in result) { - output.AppendLine(); + string key = property.Key; + JToken value = property.Value; - //Separating actual JSON from strings that contain mix of key:value pairs and single strings - string[] lines = jValue.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < lines.Length; i++) + if (key != "Device") // Skip Device as it's already printed { - string _line = lines[i]; - string pattern = @".*?:[A-Za-z0-9-]*"; - - if (!Regex.IsMatch(_line, pattern)) - { - output.AppendLine(); - } - - string[] line_string = lines[i].Split(':'); - - //Here we assign padding/indentation according to nesting level - if (line_string.Length > 1) - { - for (int x = 0; x < line_string.Length - 1; x += 2) - { - int lineNumSpaces = 30 - line_string[x].Length; - string pad2 = new string(' ', Math.Max(0, lineNumSpaces)); - int lineNumSpaces2 = 15; - string pad3 = new string(' ', Math.Max(0, lineNumSpaces2)); - - if (x + 1 < line_string.Length) - { - output.AppendLine(pad3 + line_string[x] + pad2 + ": " + line_string[x + 1]); - } - else - { - output.AppendLine(line_string[x] + pad2 + ": [empty]"); - } - } - } - else - { - output.AppendLine(lines[i]); - } + Console.WriteLine($"{key}: {value}"); } } - else - { - output.AppendLine(jValue.ToString()); - } + Console.WriteLine("----------------------------------------"); } } - output.AppendLine("\n--------------------------------------------"); } - return output.ToString(); + return jsonObject.ToString(); } else { @@ -322,9 +266,9 @@ public static async Task CheckOperationStatusAsync(string managementPoin if (status == 404) { //Note we also get a 404 while results are not ready so when this message is for when 404 is received after we got an operationId and the timeout limit was reached - fail = $"[!] Received a 404 response after the set timeout was reached. It might mean that the device is not online or timeout value is too short. You can also try to retrieve results manually using the retrieved OpeartionId {opId}"; + fail = $"[!] Received a 404 response after the set timeout was reached. It might mean that the device is not online or timeout value is too short. You can also try to retrieve results manually using the retrieved OperationId {opId}"; } - return fail.ToString(); + return fail; } }