Skip to content

Commit

Permalink
Updated JSON parsing logic for CMPivot
Browse files Browse the repository at this point in the history
  • Loading branch information
Mayyhem committed Jan 29, 2024
1 parent 0489cd4 commit 72cfde4
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ static void Main(string[] args)
invokeAdminService.AddOption(new Option<string>(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<string>(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<string>(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<string>(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<string>(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<bool>(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) =>
Expand Down
4 changes: 2 additions & 2 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
146 changes: 45 additions & 101 deletions lib/AdminService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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;
Expand Down Expand Up @@ -179,7 +177,6 @@ public static async Task<string> 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
Expand All @@ -197,134 +194,81 @@ public static async Task<string> 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
{
string fail = "";
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;
}
}

Expand Down

0 comments on commit 72cfde4

Please sign in to comment.