diff --git a/Posts/2022/07/ReadCMStatusMessages.md b/Posts/2022/07/ReadCMStatusMessages.md
new file mode 100644
index 00000000..d8721b9f
--- /dev/null
+++ b/Posts/2022/07/ReadCMStatusMessages.md
@@ -0,0 +1,375 @@
+---
+post_title: Reading Configuration Manager Status Messages With PowerShell
+username: francisconabas
+categories: PowerShell
+tags: SCCM, MECM, Status Message, Config Manager
+summary: This post's intent is to show how to read Configuration Manager status messages using WMI and Win32 API function FormatMessage.
+---
+
+**Q:** I can read Configuration Manager status messages using the _Monitoring_ tab. Can I do it
+using PowerShell?
+
+**A:** Yes you can! We can accomplish this using SQL/WQL queries, plus the Win32 function
+FormatMessage.
+
+## Better understanding Status Messages
+
+Before we get our hands dirty we need to understand how the Configuration Manager assembles these
+messages and why we can't just query it from some table, view or WMI class.
+
+To avoid storage or performance issues and to provide better standardization, the Config Manager
+stores only message's key information (and the ones who change from message to message), and uses a
+Win32 function called FormatMessage together with a DLL to assembly and display the full message.
+
+At first, it seems intimidating, specially with the whole Win32 function thing, but it's actually
+pretty simple. Let's take a look on one of these messages, so we can visualize what we want to
+accomplish.
+
+```
+Distribution Manager failed to connect to the distribution point ["Display=\\\\CMGRDP1.contoso.com\\"]MSWNET:["SMS_SITE=PS1"]\\\\CMGRDP1.contoso.com\\. Check your network and firewall settings.
+```
+
+This message states a failed content distribution to a Distribution Point. If we remove the part of
+the message containing the DP information,
+`["Display=\\\\CMGRDP1.contoso.com\\"]MSWNET:["SMS_SITE=PS1"]\\\\CMGRDP1.contoso.com\\`, we end up with a
+standard message that can be used every time this problem occurs.
+
+## Querying useful information
+
+Now that we have an overview of the Status Message structure, let's gather the information available
+on the Config Manager database. For the purpose of this post, we will use failed distribution
+messages, like the one we saw above.
+
+- The WMI classes that store Status Message information interesting for us are **SMS_StatusMessage**
+ and **SMS_StatMsgModuleNames**.
+- For content distribution status we will use the **SMS_DistributionDPStatus** class.
+- The SQL views for these classes are **v_StatusMessage**, **v_StatMsgModuleNames** and
+ **vSMS_distributionDPStatus** respectively.
+- For performance sake and the SQL language accepting more complex queries we are going to use it
+ for our exercise. This SQL query should return all packages from our Distribution Point which the
+ status is not _Success_ or _InProgress_
+
+```sql
+SELECT *
+FROM vSMS_DistributionDPStatus
+WHERE [Name] = 'CMGRDP1.contoso.com'
+ AND MessageState NOT IN (1,2)
+```
+
+On the result, we are interested on some key columns: **MessageID**, **LastStatusID**,
+**MessageSeverity** and the **InsString(n)**.
+
+- The **MessageID** and **MessageSeverity** we will use with the **FormatMessage** function.
+- The **LastStatusID** we will use to join with the other views, who name this column **RecordID**.
+- And perhaps the more interesting ones, the **InsString(n)** columns.
+
+These columns, **InsString1**, **InsString2**, **InsString3**, ..., **InsString10** contain the
+custom part of the message. Let's look at one row of the above query shall we?
+
+
+| ID1 | MessageID | LastStatusID | MessageSeverity | InsString12 | InsString2 |
+| :------------- | :-------- | :----------------- | :-------------- | :------------------------------------------------------------------------------ | :--------- |
+| 47365 | 2391 | 216172782348300122 | -1073741824 | ["Display=\\\\CMGRDP1.contoso.com\\"]MSWNET:["SMS_SITE=PS1"]\\\\CMGRDP1.contoso.com\\ | |
+
+- 1 The **ID** column is to help us to identify this specific message later.
+- 2 The other **InsString** columns are null
+
+Won't you look at that! The info on **InsString1** is exactly the custom part of our message! Let's
+join the other views, and we will have all the information needed to proceed. We are also including
+information from **v_Package**, or **SMS_Package** on WMI, to make the end result more meaningful.
+
+```sql
+SELECT
+ pkg.Name
+ ,pkg.PackageID
+ ,dps.LastUpdateDate
+ ,stm.ModuleName
+ ,smn.MsgDLLName
+ ,dps.MessageID
+ ,CASE
+ WHEN dps.MessageSeverity = '1073741824' THEN '1073741824' --Informational
+ WHEN dps.MessageSeverity = '-2147483648' THEN '2147483648' --Warning
+ WHEN dps.MessageSeverity = '-1073741824' THEN '3221225472' --Error
+ END AS 'SeverityCode'
+ ,dps.InsString1
+ ,dps.InsString2
+ ,dps.InsString3
+ ,dps.InsString4
+ ,dps.InsString5
+ ,dps.InsString6
+ ,dps.InsString7
+ ,dps.InsString8
+ ,dps.InsString9
+ ,dps.InsString10
+FROM vSMS_distributionDPStatus AS dps
+LEFT JOIN v_StatusMessage AS stm ON stm.RecordID = dps.LastStatusID
+LEFT JOIN v_StatMsgModuleNames AS smn ON smn.ModuleName = stm.ModuleName
+LEFT JOIN v_Package AS pkg ON pkg.PackageID = dps.PackageID
+WHERE dps.MessageState NOT IN (1,2)
+ AND dps.ID = '47365'
+```
+
+We are using the **ID** from the previous query to stick to our result. Removing this condition
+should bring all package distribution failure for that site.
+
+The *Case* statement is necessary because the Message Severity is actually hexadecimal, thus:
+
+```powershell-console
+PS C:\\> '{0:X}' -f -1073741824
+C0000000
+PS C:\\> '{0:X}' -f 3221225472
+C0000000
+PS C:\\>
+PS C:\\> '{0:X}' -f -2147483648
+80000000
+PS C:\\> '{0:X}' -f 2147483648
+80000000
+PS C:\\>
+```
+
+Let's see what the result of this query looks like.
+
+- Name : Visual Studio 2019 Professional
+- PackageID : PS100095
+- LastUpdateDate : 6/16/2022 3:49:26 AM
+- ModuleName : SMS Server
+- MsgDLLName : srvmsgs.dll
+- MessageID : 2391
+- SeverityCode : 3221225472
+- InsString1 : ["Display=\\\\CMGRDP1.contoso.com\\"]MSWNET:["SMS_SITE=PS1"]\\\\CMGRDP1.contoso.com\\
+- InsString2 :
+- InsString3 :
+- InsString4 :
+- InsString5 :
+- InsString6 :
+- InsString7 :
+- InsString8 :
+- InsString9 :
+- InsString10 :
+
+As you can see, we have additional information here, especially **ModuleName** and **MsgDLLName**.
+This DLL is the one we are going to use to format the message.
+
+## Formatting the message. Finally!
+
+To format our message to a readable format we will use the Configuration Manager SDK documentation,
+which instruct us to use the Win32 API function *FormatMessage* together with the information we
+just got. From the documentation:
+
+```cpp
+// Get the module handle for the component's message DLL. This assumes the
+// message DLL is loaded. If the DLL is not loaded, then load the DLL by using
+// the Win32 API LoadLibrary.
+hmodMessageDLL = GetModuleHandle(MsgDLLName);
+
+// The flags tell FormatMessage to allocate the memory needed for the message,
+// to get the message text from a message DLL, and that the insertion strings are
+// stored in an array, instead of a variable length argument list. The last
+// parameter, apInsertStrings, is the array of insertion strings returned by the
+// query.
+dwMsgLen = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_HMODULE |
+ FORMAT_MESSAGE_ARGUMENT_ARRAY,
+ hmodMessageDLL,
+ Severity | MessageID,
+ 0,
+ lpBuffer,
+ nSize,
+ apInsertStrings);
+
+// Free the memory after you use the message text.
+LocalFree(lpBuffer);
+```
+
+Wait a second... this is... C++? How am I supposed to call this function with PowerShell?
+
+We will borrow a platform from .NET called **PlatformInvoke** or ***Pinvoke*** for short. Combining
+this through the namespace **System.Runtime.InteropServices** and importing as a type in PowerShell
+using `Add-Type` will do the trick.
+
+> Disclaimer: Using Pinvoke to invoke unmanaged code is another beast in on itself and is beyond the
+> scope of this post, however is lot's of fun! I'll leave a couple of links at the end to get you
+> started.
+
+The first thing to do is to translate this C++ to C# so we can import into PowerShell.
+
+```csharp
+namespace Win32Api
+{
+ using System;
+ using System.Text;
+ using System.Runtime.InteropServices;
+
+ public class kernel32
+ {
+
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
+ public static extern IntPtr GetModuleHandle(
+ string lpModuleName
+ );
+
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
+ public static extern int FormatMessage(
+ uint dwFlags,
+ IntPtr lpSource,
+ uint dwMessageId,
+ uint dwLanguageId,
+ StringBuilder msgOut,
+ uint nSize,
+ string[] Arguments
+ );
+
+ [DllImport("kernel32", SetLastError=true, CharSet = CharSet.Unicode)]
+ public static extern IntPtr LoadLibrary(
+ string lpFileName
+ );
+
+ }
+
+}
+```
+
+Using `Add-Type` to import this namespace:
+
+```powershell
+Add-Type -TypeDefinition @"
+namespace Win32Api
+{
+ using System;
+ using System.Text;
+ using System.Runtime.InteropServices;
+
+ public class kernel32
+ {
+
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
+ public static extern IntPtr GetModuleHandle(
+ string lpModuleName
+ );
+
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
+ public static extern int FormatMessage(
+ uint dwFlags,
+ IntPtr lpSource,
+ uint dwMessageId,
+ uint dwLanguageId,
+ StringBuilder msgOut,
+ uint nSize,
+ string[] Arguments
+ );
+
+ [DllImport("kernel32", SetLastError=true, CharSet = CharSet.Unicode)]
+ public static extern IntPtr LoadLibrary(
+ string lpFileName
+ );
+
+ }
+
+}
+"@
+```
+
+The SDK documentation lists 4 steps:
+
+1. Load the DLL with LoadLibrary.
+2. Get a handle to this library with GetModuleHandle.
+3. Call the FormatMessage function.
+4. Free the memory after using the text with LocalFree
+
+Since we're calling this from PowerShell and the text will be loaded into a **StringBuilder**
+object, the last step isn't necessary. The session will take care of the cleaning once we finish.
+
+So let's give it a go!
+
+```powershell
+## Initializing the message and last error variables. Useful when processing lots of messages.
+$lastError = $null
+$message = $null
+
+## All modules location on the CM installation folder.
+$smsMsgsPath = "$env:SystemDrive\\Program Files\\Microsoft Configuration Manager\\bin\\X64\\system32\\smsmsgs"
+$moduleHandle = [Win32Api.kernel32]::GetModuleHandle("$smsMsgsPath\\srvmsgs.dll") ## The DLL From our query.
+
+## If the handle is zero, the module is not loaded. Checking to avoid loading the same DLL twice.
+if ($moduleHandle -eq 0) {
+ [void][Win32Api.kernel32]::LoadLibrary("$smsMsgsPath\\srvmsgs.dll")
+ $moduleHandle = [Win32Api.kernel32]::GetModuleHandle("$smsMsgsPath\\srvmsgs.dll")
+}
+
+$bufferSize = [int]16384 ## Buffer size for our output message.
+## The StringBuilder object who will hold our message.
+$bufferOutput = New-Object 'System.Text.StringBuilder' -ArgumentList $bufferSize
+
+$result = [Win32Api.kernel32]::FormatMessage(
+ 0x00000800 -bor 0x00000200 ## FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS
+ ,$moduleHandle
+ ,3221225472 -bor 2391 ## SeverityCode | MessageID
+ ,0 ## languageID. 0 = Default.
+ ,$bufferOutput
+ ,$bufferSize
+ ,$null ## Used to inject the InsStrings into the function. We'll process it later to avoid issues.
+)
+
+## If the function returns zero, means a failure. Setting our $lastError variable to troubleshoot further.
+if ($result -eq 0) { $lastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() }
+```
+
+At this point, if we did everything right the message should be stored on our **StringBuilder**
+object.
+
+```powershell-console
+PS C:\\> $result = [Win32Api.kernel32]::FormatMessage(
+>> 0x00000800 -bor 0x00000200 ## FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS
+>> ,$moduleHandle
+>> ,3221225472 -bor 2391 ## SeverityCode | MessageID
+>> ,0 ## languageID. 0 = Default.
+>> ,$bufferOutput
+>> ,$bufferSize
+>> ,$null ## Used to inject the InsStrings into the function. We'll process it later to avoid issues.
+>> )
+PS C:\\> $result
+113
+PS C:\\> $bufferOutput.ToString()
+%11Distribution Manager failed to connect to the distribution point %1. Check your network and firewall settings.
+PS C:\\>
+```
+
+Eureka! We did it!
+
+And I bet you already know what that _%1_ means. ;).
+
+It's the location of our **InsString1**.
+
+So doing a little cleaning...
+
+_Assuming the result from our SQL query is stored on the variable `$fail`_:
+
+```powershell-console
+PS C:\\> $message = $bufferOutput.ToString().Replace("%11","").Replace("%12","").Replace("%3%4%5%6%7%8%9%10","").Replace("%1",$fail.InsString1).Replace("%2",$fail.InsString2).Replace("%3",$fail.InsString3).Replace("%4",$fail.InsString4).Replace("%5",$fail.InsString5).Replace("%6",$fail.InsString6).Replace("%7",$fail.InsString7).Replace("%8",$fail.InsString8).Replace("%9",$fail.InsString9).Replace("%10",$fail.InsString10)
+PS C:\\>
+PS C:\\> $message
+Distribution Manager failed to connect to the distribution point ["Display=\\\\CMGRDP1.contoso.com\\"]MSWNET:["SMS_SITE=PS1"]\\\\CMGRDP1.contoso.com\\. Check your network and firewall settings.
+PS C:\\>
+```
+
+Now with the results of the query plus a beautifully formatted message you can store this into a
+database or create your own reports and automations. Your imagination is the limit!
+
+## Conclusion
+
+Congratulations! You not only automated Configuration Manager Status Messages, but also called a
+Win32 Native API function!
+
+I hope you had as much fun trying this as me writing it.
+
+Thank you very much, and I see you on the next trip!
+
+## Useful links
+
+- [Configuration Manager API Reference](https://docs.microsoft.com/mem/configmgr/develop/reference/configuration-manager-reference)
+- [About Component Status Messages](https://docs.microsoft.com/mem/configmgr/develop/core/servers/manage/about-configuration-manager-component-status-messages)
+- [FormatMessage Function winbase.h](https://docs.microsoft.com/windows/win32/api/winbase/nf-winbase-formatmessage)
+- [LoadLibrary Function libloaderapi.h](https://docs.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya)
+- [GetModuleHandle Function libloaderapi.h](https://docs.microsoft.com/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea)
+- [Platform Invoke (P/Invoke)](https://docs.microsoft.com/dotnet/standard/native-interop/pinvoke)
+- [FormatMessage on pinvoke.net (With examples!)](https://www.pinvoke.net/default.aspx/kernel32.formatmessage)