Skip to content
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
124 changes: 88 additions & 36 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,42 +544,39 @@ static void Main(string[] args)
getSecretsFromPolicy.Handler = CommandHandler.Create(
(string managementPoint, string siteCode, string certificate, string clientId, string mediaId, string outputFile, string password, string registerClient, string username) =>
{
if (managementPoint == null || siteCode == null)
{
(managementPoint, siteCode) = ClientWmi.GetCurrentManagementPointAndSiteCode();
}
if (!string.IsNullOrEmpty(managementPoint) && !string.IsNullOrEmpty(siteCode))
{
if (!string.IsNullOrEmpty(certificate) && !string.IsNullOrEmpty(mediaId))
ResolveClientAndInvoke(managementPoint, siteCode, certificate, clientId, username, password, registerClient, outputFile,
MgmtPointMessaging.GetSecretsFromPolicies,
(resolvedManagementPoint, resolvedSiteCode) =>
{
string szHTTPProxyAddress = null;
MgmtPointMessaging.SendPolicyAssignmentRequestWithExplicitData(clientId, mediaId, certificate, managementPoint, siteCode, szHTTPProxyAddress);
}
else if (!string.IsNullOrEmpty(certificate) && !string.IsNullOrEmpty(clientId))
{
MgmtPointMessaging.GetSecretsFromPolicies(managementPoint, siteCode, certificate, clientId, null, null, null, outputFile);
}
else if (!string.IsNullOrEmpty(certificate) && string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(certificate) && !string.IsNullOrEmpty(clientId))
{
Console.WriteLine("[!] Both a certificate (-c) and SMS client GUID (-i) for a previously registered client must be specified when using this option");
}
else if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(registerClient))
{
MgmtPointMessaging.GetSecretsFromPolicies(managementPoint, siteCode, null, null, username, password, registerClient, outputFile);
}
else if (!string.IsNullOrEmpty(registerClient) && (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)))
{
Console.WriteLine("[!] Both a computer account name (-u) and computer account password (-p) must be specified when using the register client (-r) option");
}
else if (Helpers.IsHighIntegrity())
{
MgmtPointMessaging.GetSecretsFromPolicies(managementPoint, siteCode, certificate, clientId, username, password, registerClient, outputFile);
}
else
{
Console.WriteLine("[!] A client name to register (-r), computer account name (-u), and computer account password (-p) must be specified when the user is not a local administrator");
}
}
if (!string.IsNullOrEmpty(certificate) && !string.IsNullOrEmpty(mediaId))
{
string szHTTPProxyAddress = null;
MgmtPointMessaging.SendPolicyAssignmentRequestWithExplicitData(clientId, mediaId, certificate, resolvedManagementPoint, resolvedSiteCode, szHTTPProxyAddress);
return true;
}
return false;
});
});

// get policies
var getPolicies = new Command("policies", "Request the full machine policy from a management point via HTTP, including policies not flagged \"Secret\" (get secrets only returns the latter)\n" +
" Requirements:\n" +
" - Domain computer account credentials\n" +
" OR\n" +
" - Local Administrators group membership on a client");
getCommand.Add(getPolicies);
getPolicies.Add(new Option<string>(new[] { "--certificate", "-c" }, "The encoded X509 certificate blob to use that corresponds to a previously registered device"));
getPolicies.Add(new Option<string>(new[] { "--client-id", "-i" }, "The SMS client GUID to use that corresponds to a previously registered device and certificate"));
getPolicies.Add(new Option<string>(new[] { "--management-point", "-mp" }, "The IP address, FQDN, or NetBIOS name of the management point to connect to (default: the current management point of the client running SharpSCCM)"));
getPolicies.Add(new Option<string>(new[] { "--output-dir", "-o" }, "The directory to write each policy's XML to (one file per policy, named by policy ID); prints to console if omitted"));
getPolicies.Add(new Option<string>(new[] { "--password", "-p" }, "The password for the specified computer account"));
getPolicies.Add(new Option<string>(new[] { "--register-client", "-r" }, "The name of the device to register as a new client (required when user is not a local administrator)"));
getPolicies.Add(new Option<string>(new[] { "--username", "-u" }, "The name of the computer account to register the new device record with, including the trailing \"$\""));

getPolicies.Handler = CommandHandler.Create(
(string managementPoint, string siteCode, string certificate, string clientId, string outputDir, string password, string registerClient, string username) =>
{
ResolveClientAndInvoke(managementPoint, siteCode, certificate, clientId, username, password, registerClient, outputDir, MgmtPointMessaging.GetPolicies);
});

var getSiteInfo = new Command("site-info", "Get information about the site, including the site server name, from a domain controller via LDAP");
Expand Down Expand Up @@ -1402,6 +1399,61 @@ static void Main(string[] args)
Console.WriteLine(ex.InnerException);
}
}
}
}

// Shared by "get secrets" and "get policies": resolves the management point/site code if not
// provided, then picks the right client-auth path (existing cert+client-id, register a new
// client, or fall back to the local machine's own cert when running as admin) and invokes the
// matching MgmtPointMessaging method. A caller with an extra auth path of its own (e.g. "get
// secrets"'s PXE cert+media-GUID flow) can handle it via trySpecialCase, checked after
// resolution but before the standard ladder - matching where that check ran previously.
static void ResolveClientAndInvoke(
string managementPoint,
string siteCode,
string certificate,
string clientId,
string username,
string password,
string registerClient,
string outputPath,
Action<string, string, string, string, string, string, string, string> invoke,
Func<string, string, bool> trySpecialCase = null)
{
if (managementPoint == null || siteCode == null)
{
(managementPoint, siteCode) = ClientWmi.GetCurrentManagementPointAndSiteCode();
}
if (!string.IsNullOrEmpty(managementPoint) && !string.IsNullOrEmpty(siteCode))
{
if (trySpecialCase != null && trySpecialCase(managementPoint, siteCode))
{
return;
}
if (!string.IsNullOrEmpty(certificate) && !string.IsNullOrEmpty(clientId))
{
invoke(managementPoint, siteCode, certificate, clientId, null, null, null, outputPath);
}
else if (!string.IsNullOrEmpty(certificate) && string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(certificate) && !string.IsNullOrEmpty(clientId))
{
Console.WriteLine("[!] Both a certificate (-c) and SMS client GUID (-i) for a previously registered client must be specified when using this option");
}
else if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(registerClient))
{
invoke(managementPoint, siteCode, null, null, username, password, registerClient, outputPath);
}
else if (!string.IsNullOrEmpty(registerClient) && (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)))
{
Console.WriteLine("[!] Both a computer account name (-u) and computer account password (-p) must be specified when using the register client (-r) option");
}
else if (Helpers.IsHighIntegrity())
{
invoke(managementPoint, siteCode, certificate, clientId, username, password, registerClient, outputPath);
}
else
{
Console.WriteLine("[!] A client name to register (-r), computer account name (-u), and computer account password (-p) must be specified when the user is not a local administrator");
}
}
}
}
}
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.13")]
[assembly: AssemblyFileVersion("2.0.13")]
[assembly: AssemblyVersion("2.0.14")]
[assembly: AssemblyFileVersion("2.0.14")]
8 changes: 8 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# SharpSCCM Release Notes

### Version 2.0.14 (August 27, 2026)
##### New Commands
- Added `get policies` to retrieve all machine policy assignments
##### Changes
- Wait for policy downloads to complete before requesting the next policy
- Handle policy decompression and decoding failures without terminating the command
- Preserve valid XML when decompressed policy content includes an XML declaration

### Version 2.0.13 (August 20, 2025)
##### Changes
- Added --delay-exit option instead of holding open when debugging by default
Expand Down
40 changes: 40 additions & 0 deletions UnitTests/HelpersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml;

namespace SharpSCCM.UnitTests
{
[TestClass]
public class HelpersTests
{
[TestMethod]
public void DecompressXMLNodes_NestedXmlDeclaration_ExpandsWithoutDeclaration()
{
string nestedXml = "<?xml version=\"1.0\" encoding=\"utf-16\"?><Inner><Value>test</Value></Inner>";
byte[] nestedXmlBytes = Encoding.Unicode.GetPreamble().Concat(Encoding.Unicode.GetBytes(nestedXml)).ToArray();
byte[] compressedBytes;

using (MemoryStream output = new MemoryStream())
{
using (GZipStream compressor = new GZipStream(output, CompressionMode.Compress, true))
{
compressor.Write(nestedXmlBytes, 0, nestedXmlBytes.Length);
}
compressedBytes = output.ToArray();
}

XmlDocument document = new XmlDocument();
string compressedHex = BitConverter.ToString(compressedBytes).Replace("-", string.Empty);
document.LoadXml($"<Root><Policy Compression=\"zlib\">{compressedHex}</Policy></Root>");

Helpers.DecompressXMLNodes(document.DocumentElement);

Assert.AreEqual("test", document.SelectSingleNode("/Root/Policy/Inner/Value").InnerText);
Assert.IsFalse(document.OuterXml.Contains("<?xml"));
}
}
}
4 changes: 3 additions & 1 deletion UnitTests/UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Management" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ClientWmiTests.cs" />
<Compile Include="HelpersTests.cs" />
<Compile Include="MgmtUtilTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SmsProviderWmiTests.cs" />
Expand All @@ -104,4 +106,4 @@
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.2.1.2\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.2.1.2\build\net45\MSTest.TestAdapter.targets'))" />
</Target>
<Import Project="..\packages\MSTest.TestAdapter.2.1.2\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.2.1.2\build\net45\MSTest.TestAdapter.targets')" />
</Project>
</Project>
19 changes: 19 additions & 0 deletions lib/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,30 @@ public static void DecompressXMLNodes(System.Xml.XmlNode xmlNode)
Array.Copy(decompressedBytes, 2, decompressedXMLBytes, 0, decompressedBytes.Length - 2);
szDecompressedStr = System.Text.Encoding.Unicode.GetString(decompressedXMLBytes);
}
else
{
// Content didn't start with the UTF-16LE BOM we know how to decode; leaving the node's
// raw (still-compressed) content in place rather than silently dropping it unannounced.
Console.WriteLine($"[!] Decompressed a \"{compressionNode.Name}\" node's content but it didn't start with the expected UTF-16 BOM, so it was left un-decompressed");
}
// Update node content
if (szDecompressedStr.Length > 0)
{
// remove "\r", "\n", "\t", etc.
string szCleanedXmlStr = new string(szDecompressedStr.Where(c => !char.IsControl(c)).ToArray());
// Decompressed content is often itself a full XML document starting with its own
// "<?xml ...?>" declaration. Since it's about to become InnerXml of an existing
// element rather than a standalone document, a nested declaration would make the
// final serialized document invalid (XML disallows a declaration except at the very start).
string szTrimmedXmlStr = szCleanedXmlStr.TrimStart();
if (szTrimmedXmlStr.StartsWith("<?xml", StringComparison.OrdinalIgnoreCase))
{
int declEndIndex = szTrimmedXmlStr.IndexOf("?>");
if (declEndIndex >= 0)
{
szCleanedXmlStr = szTrimmedXmlStr.Substring(declEndIndex + 2);
}
}
compressionNode.InnerXml = szCleanedXmlStr;
// Recursive decompress
foreach (System.Xml.XmlNode childNode in compressionNode.ChildNodes)
Expand Down
Loading
Loading