jonasohmsen
15 TopicsGpresult Like Tool For Intune
Hi, Jonas here! Or as we say in the north of Germany: "Moin Moin!" I had to troubleshoot a lot of Intune policies lately and I used a variety of tools for that. At the end, I built my own script to have a result which looks similar to what “GPresult /h” creates for on-premises group polices. The script is inspired by the following article: https://doitpshway.com/get-a-better-intune-policy-report-part-2 by Ondrej Sebela. It follows a similar approach, but without any module dependencies and fewer output options, as my script only generates an HTML page. What started as a script is now a module which might have more functions in the future. Feel free to read any of my other articles here: https://aka.ms/JonasOhmsenBlogs How to get the module The PowerShell module is called: "IntuneDebug" and can be installed or downloaded from the PowerShell Gallery. Install the module by running the following command: Install-Module -Name IntuneDebug The module repository can be found here https://aka.ms/IntuneDebug in case you want to download the module manually or want to contribute to it. The command to get the report is called: “Get-MDMPolicyReport” How to use Get-MDMPolicyReport The function can run without administrative permissions and without any parameters on a windows machine. But you can also start the function with administrative permissions to get more data about Intune Win32Apps and their install status. Use parameter “-MDMDiagReportPath” to load MDM report data captured on a remote machine. But more on that in section “How to use parameter -MDMDiagReportPath“ So, in summary, the function can run locally to output information specific to that device, or it can parse already captured data via the “-MDMDiagReportPath” parameter. It cannot gather data remotely, though. The function output As mentioned earlier, the only output of the function is an HTML file which will automatically open in Edge. The output is grouped into sections to make the report easier to read. The page looks like this when all sections are collapsed: Section: "DeviceInfo <Devicename>" DeviceInfo shows general information about the device and the Intune sync status: Section: "PolicyScope: Device" This section shows all the settings applied to the device grouped by area/product. Note: If you’re coming from ConfigMgr you might expect a policy ID in the report. While an Intune policy has an ID, the ID is not stored on the device. That’s by-design and that’s the reason why we just see the settings that apply to a device in this report. The following example shows some basic Defender and Delivery Optimization settings grouped together. You can also see the system's default value if there is one and the winning settings provider. This should typically be the MDM provider like Intune, but it could also be a different provider for some settings depending on the setup. Section: "PolicyScope: <SID> <UPN>" This section shows all the policies applied to a user. The user’s SID and UPN (UPN only when run locally) are visible in the policy-scope header. If there are multiple users working on a machine, each user will have their own section in the report. Section: "PolicyScope: EnterpriseDesktopAppManagement" This section shows all MSI installation policies from Intune. NOTE: Win32 and store apps are visible in the “Win32Apps” section. The application name is not available, instead I show the MSI filename to give an indication of what type of app that is. Section: "PolicyScope: Resources" Under resources we will see policies which typically contain some sort of payload. Like a certificate or Defender firewall rule. I tried to make each section as readable as possible. So, the output varies by type. Certificates for example, are shown in a different format as Defender firewall rules. NOTE: If the function runs without the parameter “-MDMDiagReportPath” it will try to enrich the policy info with as much data as possible. This is not possible when working with captured MDM-reports from a remote machine. The output might be limited in that case. Section: "PolicyScope: Local Admin Password Solution (LAPS)" This section shows all the settings applied to the device coming from a LAPS policy as well as some local settings. Section: "PolicyScope: Win32Apps" This section shows all available Win32App policies. Those apps can be installed already or just assigned as available. If you need more information about the installation status, you need to run the function with administrative permission. This only works locally and cannot be used with parameter “-MDMDiagReportPath” since the extra data is coming from the local registry. If a script is used for the detection or requirement, the script will be parsed and shown as it is. Use the copy button to copy the script and test it locally if needed. When the script is run as administrator locally, it will try to get more information about the actual installation status of an application: Section: "PolicyScope: Intune Scripts" Intune Scripts will show script policies and their current state. The example below shows a remediation script with the detection output string "Found". It does not have an remediation action and therefore no data for the related properties. Unfortunately, the script name is not part of the policy and cannot be shown here. But you can use Graph Explorer https://aka.ms/ge and use the following endpoint to get the script name by entering the script ID of your script: "https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts/<ScriptID>?$select=id,displayName" Where the data comes from The function will use the following command to generate an MDM report: MdmDiagnosticsTool.exe -out “C:\Users\PUBLIC\Documents\MDMDiagnostics\<DateTime>” NOTE: The tool MdmDiagnosticsTool.exe is part of the Windows operating system. More about it can be found HERE The tool will export the data to C:\Users\PUBLIC\Documents\MDMDiagnostics to a folder in the following format: "yyyy-MM-dd_HH-mm-ss" The function will then parse the following two files to extract the required data without administrative privileges: MDMDiagReport.html MDMDiagReport.xml Some data is directly read from the registry to enrich the output and in some cases administrator permissions are required. The Win32Apps and Intune script policy data is coming from the Intune Management Extension logfiles: C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\AppWorkload*.log C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\HealthScripts*.log NOTE: The folders under “C:\Users\PUBLIC\Documents\MDMDiagnostics” will be deleted when the creation time is older than one day. This can be changed with parameter “-CleanUpDays” set to a higher value than one day. How to use parameter “-MDMDiagReportPath” Simply generate MDM report data, either with the MdmDiagnosticsTool.exe, via the settings app or via Intune. Then copy the files to a system with the IntuneDebug module on it and unpack the report data. You can now run the function with the parameter “-MDMDiagReportPath” and point it to the unpacked report data. NOTE: The report header will contain the following when the parameter was used: “Generated from captured MDM Diagnostics Report” MdmDiagnosticsTool.exe example: mdmdiagnosticstool.exe -area "DeviceEnrollment;DeviceProvisioning;Autopilot" -zip C:\temp\MDMDiagnosticsData.zip Settings app example: Intune Example: I hope you find this tool helpful. In case of any issues or suggestions, head over to GitHub via https://aka.ms/IntuneDebug and create an issue or pull request. Stay safe! Jonas Ohmsen Code disclaimer This sample script is not supported under any Microsoft standard support program or service. This sample script is provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of this sample script and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of this script be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use this sample script or documentation, even if Microsoft has been advised of the possibility of such damages.How-To Use Graph Object Change Notifications
Hi, Jonas and Roland here! Or as we say in the north of Germany: "Moin Moin!" We recently worked on a mechanism to sync Entra ID group memberships into an Azure SQL table and would like to share our learnings. This article is part of a series of articles about Entra ID group membership synchronization. Check them out here: https://aka.ms/JonasOhmsenBlogs Introduction In this article we will dive deeper into “Option #4: Delta Notification” as described in our previous article: How-To Sync EntraID Group Memberships Into Any System TL/DR In short, we’ve built an Azure Function app to synchronize Entra ID group membership changes into an Azure SQL database using Graph object change notification functionality. The solution can be found here: LINK A quick recap: Microsoft Graph offers the ability to subscribe to change notifications. In our case, we need to track and act on membership changes made to Entra ID groups. A change notification subscription will result in two actions. The change notification in the form of a web request Will be send every time a change is made to an Entra ID object and will contain a list of those changes. Service notifications also in the form of a web request Will contain a list of information about the subscription or service states. A notification about a soon to expire subscription for example. Those can happen at any time. For this to work we need two endpoints to be able to receive those two types of notifications. A typical communication sequence between Microsoft Graph and our custom endpoints would look like this: Overview We will use an Azure Functions App with two endpoints. One for the change notifications and one for the service notifications. We already have a SQL database we use for an application, and we need to update Entra ID group memberships in a table called “GroupMembers”. That way our application always has the correct membership information available. (Please read our previous article about the application setup and why we required group memberships to be stored outside of Entra ID: How-To Sync EntraID Group Memberships Into Any System) For the function app to write to Azure SQL, we enabled a system managed identity and granted access to SQL. That managed identity does also need the “Groups.Read.All” permissions for Microsoft Graph to be able to subscribe to group changes. We will use two stored procedures in SQL to help keep the data consistent. Those stored procedures will give us the flexibility to change any SQL schema or logic without needing to change our synchronization process. NOTE: To keep the scenario simple, we will start with empty Entra ID groups. Since existing memberships will not result in a change notification and we would need a mechanism for an initial membership import. NOTE: Our function app will be written in PowerShell, but you can use any other available language to achieve the same. Initial function setup We will use some variables configured centrally for all our functions within our Functions app. That way we can keep the code clean and we are able to later adjust the settings in the Azure portal. Name: varClientState Value: Will contain a random password like string. We will use that to verify that each change notification come from a subscription we created. Name: varAzureSQLInstance Value: Will contain the azure SQL instance name where we store our database with table “GroupMembers”. Example: sampledbserver10235x.database.windows.net Name: varAzureSQLDatabase Value: The name of the database with table “GroupMembers”. Name: varMaximumSubscriptionExpirationDays Value: A subscription has a lifespan of 1 to 29 days. We will use the variable to set an initial value when we create new subscription and we will use the variable if we need to renew a subscription. Name: varNotificationUrl Value: Will contain the URL of our main function to act on member changes. We’ll come to that later. Name: varLifecycleNotificationUrl Value: Will contain the URL of our subscription lifecycle function. We’ll come to that later. NOTE: We will not use SQL bindings in our function app to be able to call SQL multiple times. That’s the reason for the SQL related variables instead of a SQL connections string. Read more about function bindings HERE. SQL table “GroupMembers” Let’s start with the SQL table we will use to store all the members of all the groups we are interested in. The table will contain the following information SQL stored procedures We will use the following stored procedures to delete or add members of groups to or from our table. The stored procedures help keep the data consistent and make changes to the database schema possible without the need to change the Function App. sproc: NewGroupMember The stored procedure has four parameters. @group_id: The id of the group @member_id: In case of a user the Entra ID object ID. In case of a device the device ID @object_id: The Entra ID object ID @object_type: Can be 'user' or 'device'. NOTE: We only work with users or devices and not with groups in groups. NOTE: To keep things simple, the stored procedure will not return any error in case the object is already there. sproc: RemoveGroupMember The stored procedure has only two parameters. One for the group ID and one for the Entra ID object ID. NOTE: It will not return an error in case the member does not exist anymore. Function App managed identity We will activate the system assigned managed identity for our function app. The identity gives us the ability to grant access to SQL and Microsoft Graph easily. Function App managed identity SQL permissions The following statements will add the Function App managed identity to our SQL database and grant just enough permissions to execute the required stored procedures: Function App managed identity Graph permissions The Function app managed identity also needs the Groups.Read.All, Users.Read.All and Devices.Read.All Microsoft Graph permission. Groups.Read.All is required to be able to create group object change subscriptions. That is the most important one. (Users.Read.All and Devices.Read.All will be explained in the “NotifyGroupChange function” section below) We will use the following PowerShell script to add those application permissions to the managed identity. We use a script because we cannot set those permissions via the Azure Portal. Function App in more detail As mentioned before, we need at least two endpoints aka functions in our Azure Function App. One for the change notifications and one for the service notifications. Both functions need to be able to reply to the initial subscription setup call. In that call Microsoft Graph will send a “ValidationToken” and expects that token to be in the reply. If the reply does not contain the validation token or if there is no reply at all, the subscription will not be activated. If one of the functions detects a validation token, we will simply reply with it in text/plain form and with http status code 200 OK. Those are our first lines of code in both functions. “NotifyGroupChange” function The NotifyGroupChange or change notification function will be the main function keeping table “GroupMembers” consistent. We will use the following code to get an access token for SQL server to be able to write to the database. And Connect-MgGraph to get an access token for Microsoft Graph to be able to manage subscriptions and get information about Entra ID objects. Both methods will use the Function app managed identity. NOTE: MSI_ENDPOINT and MSI_SECRET are built-in variables we can use to authenticate and to request an access token. NOTE: The first step is only required because we will use the SQL cmdlet “Invoke-Sqlcmd” instead of a SQL binding. Read more about function bindings HERE. That way we have more control and less limitations over the SQL commands. The following is an example of a group membership change notification sent by Microsoft Graph. The yellow arrow shows us the client secret we used to create the subscription. The purple arrow the group we subscribed to. The green arrow an added member and the red arrow a removed member. Before doing anything with that information we need to check if that message was sent from a subscription we created. We will do that by checking the “ClientState” value. If it doesn’t match, we will stop all actions and return with bad request (error 400) If the “ClientState” value matches, we can proceed. There is one important thing to note here. Every object in Entra ID has an object ID. Like the user shown in the screenshot below: But our application works with user and devices and devices also have a device ID as shown in the screenshot below: The change notification will only contain the object ID and never the device ID or any other IDs. However our application needs the device ID. One way of getting the device ID is to use a Microsoft Graph function to get an object by its object ID. We simply take all those object IDs from the change notification and send them to: https://graph.microsoft.com/v1.0/directoryObjects/microsoft.graph.getByIds as shown in the below example. That method also gives us the object type. So, we know if the member change happened for a device or a user. Or even a group within a group if we want to. NOTE: The “microsoft.graph.getByIds” functions requires at least Users.Read.All and Devices.Read.All permissions. We can then use the returned info to enrich the exiting object ID with a device ID and the type of that object. The below screenshot shows a code example to use Invoke-Sqlcmd to either add or remove members from the SQL table “GroupMembers”. (Simplified to fit in this blog) At the end of the script we will return with http status code 200 OK. If we encounter a problem, we return with http status code 400 bad request. Bad request will also mean that Microsoft Graph will re-send the change notification. “SubscriptionLifecycle” function As mentioned before, we also need an endpoint to receive service notifications from Microsoft Graph. That endpoint is called by the subscription for lifecycle events, which are different from the actual change notification. This function also needs to respond with the validation token and therefore contains the same “ValidationToken” code mentioned earlier. At the moment the function will either use the “PATCH” method to set a new expiration date to extend the lifetime of a subscription or write the event to the log. The code might look like this. We first need the new expiration date in a specific format. Then construct a small JSON file containing the new date and at the end send the JSON to the correct endpoint for that subscription. “NewSubscription” function While the other functions are triggered by Microsoft Graph, this one is meant for the admin to create a new subscription. Therefore we don’t need the ValidationToken code for this function. All we have to do is to POST a JSON file to the subscription endpoint. We will construct the file based on the Function App variables for the “notificationUrl”, “lifecycleNotificationUrl”, “expirationDateTime” and “clientState”. NOTE: Now is a good time to set those URL variables for the required functions. The following web request will then create a new subscription without the need for any other data. Because all other values are coming from the Function App variables mentioned earlier. https://<FunctionAppName>.azurewebsites.net/api/NewSubscription?GroupID=0686a5ee-3e46-49e5-82e8-8afe07ca4fa8 “ListSubscriptions” function The ListSubscriptions function is also meant for admins and does also not require the ValidationToken code. IMPORTANT: Subscriptions are only visible to the creator of the subscription. So, this function is vital for us, because it will show us all active subscriptions created by the managed identity of the Function App. The code will simply use the GET method for endpoint: GET https://graph.microsoft.com/v1.0/subscriptions and runs without any additional parameters. “DeleteSubscription” function The DeleteSubscription function is also meant for admins and does also not require the ValidationToken code. It has just one parameter called “SubscriptionID”. It will use the DELETE method to delete a subscription by ID. DELETE https://graph.microsoft.com/v1.0/subscriptions/<SubscriptionID> Putting it all together The following is an example of the features we get from those different functions. We are now able to create a “NewSubscription”. Then add or remove any objects from the subscribed group. Microsoft Graph will then call the “NotifyGroupChange” function. The “NotifyGroupChange” function will keep the table “GroupMembers” in sync. Microsoft Graph is able to notify us about service or subscription states. We can list all active subscriptions or delete them. This is it for now. But there is more and we will keep posting articles about that topic until we run out of content 😉 We hope you enjoyed our article. Let us know in the comments. Stay safe! Roland Spindeler and Jonas Ohmsen Code disclaimer This sample script is not supported under any Microsoft standard support program or service. This sample script is provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of this sample script and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of this script be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use this sample script or documentation, even if Microsoft has been advised of the possibility of such damages.