Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue 40 and update admin-service output #49

Merged
merged 3 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 5 additions & 16 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ static void Main(string[] args)
//invoke adminService
var invokeAdminService = new Command("admin-service", "Invoke an arbitrary CMPivot query against a collection of clients or a single client via AdminService\n" +
" Requirements:\n" +
" - Full Administrator\n" +
" - \"Read\" and \"Run CMPivot\" permissions for the \"Collections\" scope\n" +
" - https://learn.microsoft.com/en-us/mem/configmgr/core/servers/manage/cmpivot#permissions\n" +
" Examples:\n" +
" - SharpSCCM_merged.exe invoke admin-service -q \"Device\" -r 16777211\n" +
Expand All @@ -624,27 +624,21 @@ 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 timing out.\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) =>
{

string[] delayTimeoutValues = new string[] { "5", "5" };



if (delay != null)
{
if (delay.Length < 1 || !uint.TryParse(delay, out uint value) || value == 0)
{
Console.WriteLine("\r\n[!] Please check your syntax for the --delay parameter (e.g., --delay 5)\r\n[!] Leave blank for the default 5 seconds wait before each attempt to retrieve results");
return;
}
else
{
delayTimeoutValues[0] = delay;
}
delayTimeoutValues[0] = delay;
}
if (retries != null)
{
Expand All @@ -653,10 +647,7 @@ static void Main(string[] args)
Console.WriteLine("\r\n[!] Please check your syntax for the --retries parameter (e.g., --retries 5)\r\n[!] Leave blank for a default of 5 retries before reaching a timeout");
return;
}
else
{
delayTimeoutValues[1] = retries;
}
delayTimeoutValues[1] = retries;
}
if ((string.IsNullOrEmpty(query)) || (string.IsNullOrEmpty(collectionId) && string.IsNullOrEmpty(resourceId)))
{
Expand All @@ -671,10 +662,8 @@ static void Main(string[] args)
if (smsProvider == null)
{
(smsProvider, _) = ClientWmi.GetCurrentManagementPointAndSiteCode();

}

await AdminService.Main(smsProvider, siteCode, query, collectionId, resourceId, delayTimeoutValues, json);
await AdminService.CheckOperationStatusAsync(smsProvider, siteCode, query, collectionId, resourceId, delayTimeoutValues, 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 31, 2024)
##### Changes
- Fixed issue 40

### Version 2.0.4 (January 29, 2024)
##### Changes
- Fixed issue 39
Expand Down
199 changes: 73 additions & 126 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 @@ -191,141 +188,76 @@ public static async Task<string> CheckOperationStatusAsync(string managementPoin
{
// Here we display the output as JSON after the user supplies the required flag
Console.WriteLine($"\r---------------- CMPivot data ------------------\r");
return jsonObject.ToString();
Console.WriteLine(jsonObject.ToString());
}

// 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")
Console.WriteLine("----------------------------------------");
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);

// 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)
// Check if 'value' is a JArray or a JObject and process accordingly
var valueToken = parsedJsonA["value"];
if (valueToken is JArray valuesA)
{
header = $"---------------- CMPivot data #{counter2} ------------------";
output.AppendLine(string.Format(header));
counter2++;

foreach (JProperty property in item.Children())
Console.WriteLine("----------------------------------------");

// Process each value in the array
foreach (JObject valueObject in valuesA)
{
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)
{
if (jValue.Type == JTokenType.String && jValue.ToString().Contains(Environment.NewLine))
{
output.AppendLine();

//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++)
{
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]);
}
}
}
else
{
output.AppendLine(jValue.ToString());
}
}
ProcessResult(valueObject);
}
output.AppendLine("\n--------------------------------------------");
}
return output.ToString();
}
else
{
string fail = "";
if (status == 404)
else if (valueToken is JObject valueObject)
{
//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}";
// Process the single value object
ProcessResult(valueObject);
}
return fail.ToString();
return string.Empty;
}

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, the query returned an error, or timeout value is too short. You can also try to retrieve results manually using the retrieved OperationId {opId}";
}
return fail;
}

public static uint InitiateClientOperationExMethodCall(string query, string managementPoint, string sitecode, string CollectionName, string deviceId)
Expand Down Expand Up @@ -376,11 +308,9 @@ public static uint InitiateClientOperationExMethodCall(string query, string mana
Console.WriteLine("[+] Fallback Method call succeeded");
return returnValue;
}
else
{
Console.WriteLine("[!] Method call failed with error code {0}.", returnValue);
return 0;
}

Console.WriteLine("[!] Method call failed with error code {0}.", returnValue);
return 0;
}
catch (ManagementException e)
{
Expand All @@ -389,16 +319,33 @@ public static uint InitiateClientOperationExMethodCall(string query, string mana
}
}

// Entry point with arguments provided by user or defaults from command handler
public static async Task Main(string managementPoint, string sitecode, string query, string collectionName, string deviceId, string[] timeoutValues, bool json)
// Method to process each 'Result' in a 'value' object
public static void ProcessResult(JObject valueObject)
{
var CMPdata = await CheckOperationStatusAsync(managementPoint, sitecode, query, collectionName, deviceId, timeoutValues, json);
if (!string.IsNullOrWhiteSpace(CMPdata))
if (valueObject["Result"] is JArray results && results != null)
{
Console.WriteLine("\r" + CMPdata + "\r");
Console.WriteLine("----------------------------------------");

foreach (JObject result in results)
{
string device = result["Device"]?.ToString();
Console.WriteLine("Device: " + device);

foreach (var property in result)
{
string key = property.Key;
JToken value = property.Value;

// Skip Device as it's already printed
if (key != "Device")
{
Console.WriteLine($"{key}: {value}");
}
}
Console.WriteLine("----------------------------------------");
}
}
}

}
public class JsonResponse
{
Expand Down
Loading