├── PowerBI Reports ├── IntuneRptSparkV09.pbix └── PowerBI-starter-example.pbix ├── README.md ├── Scripts ├── Antivirus.ps1 ├── AppInstallStates.ps1 ├── DeviceConfigurationGroups.ps1 ├── DeviceConfigurationStates.ps1 ├── Malware.ps1 └── ServicingRings.ps1 ├── Step-by-step Guide to Intune Automated Reporting with Graph, Automation, and PowerBI.pptx └── images ├── appsbydepartment.png ├── historicalservicingcomparison.png ├── operationalcompliance.png └── servicingoverview.png /PowerBI Reports/IntuneRptSparkV09.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/PowerBI Reports/IntuneRptSparkV09.pbix -------------------------------------------------------------------------------- /PowerBI Reports/PowerBI-starter-example.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/PowerBI Reports/PowerBI-starter-example.pbix -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intune Automated Reporting 2 | Azure Automation runbook scripts to grab Intune data and output CSV to Storage Account containers. 3 | 4 | ## Set up 5 | 6 | Setting up the runbook scripts to automatically grab Intune data ouput CSVs to Azure blob storage accounts can be done in six simple steps. 7 | 8 | 1. Graph API Registration 9 | 1. Create a Resource Group 10 | 1. Create a Storage Account 11 | 1. Create an Automation Account 12 | 1. Set up Automation Runbooks 13 | 1. Import data into PowerBI 14 | 15 | Check out the step-by-step guide to learn more. 16 | 17 | ## Create effective visuals in PowerBI 18 | 19 | ### Applications 20 | 21 | This visual uses the following scripts. 22 | * **AppInstallStates.ps1** 23 | * **ServicingRings.ps1** 24 | 25 | ![Applications visualization](/images/appsbydepartment.png) 26 | 27 | ### Servicing 28 | 29 | The following visuals use the **ServicingRings.ps1** script. 30 | 31 | ![Servicing visualization](/images/servicingoverview.png) 32 | 33 |
34 | 35 | ![Historical servicing visualization](/images/historicalservicingcomparison.png) 36 | 37 | -------------------------------------------------------------------------------- /Scripts/Antivirus.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .COPYRIGHT 4 | Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 5 | See LICENSE in the project root for license information. 6 | 7 | #> 8 | 9 | #################################################### 10 | 11 | # Variables 12 | $subscriptionID = Get-AutomationVariable 'subscriptionID' # Azure Subscription ID Variable 13 | $tenantID = Get-AutomationVariable 'tenantID' # Azure Tenant ID Variable 14 | $resourceGroupName = Get-AutomationVariable 'resourceGroupName' # Resource group name 15 | $storageAccountName = Get-AutomationVariable 'storageAccountName' # Storage account name 16 | 17 | # Report specific Variables 18 | $containerName = Get-AutomationVariable 'antivirusreport' # Resource group name 19 | $containerSnapshotName = Get-AutomationVariable 'antivirusreportsnapshots' # Storage account name 20 | 21 | # Graph App Registration Creds 22 | 23 | # Uses a Secret Credential named 'GraphApi' in your Automation Account 24 | $clientInfo = Get-AutomationPSCredential 'GraphApi' 25 | # Username of Automation Credential is the Graph App Registration client ID 26 | $clientID = $clientInfo.UserName 27 | # Password of Automation Credential is the Graph App Registration secret key (create one if needed) 28 | $secretPass = $clientInfo.GetNetworkCredential().Password 29 | 30 | #Required credentials - Get the client_id and client_secret from the app when creating it in Azure AD 31 | $client_id = $clientID #App ID 32 | $client_secret = $secretPass #API Access Key Password 33 | 34 | #################################################### 35 | 36 | function Get-AuthToken { 37 | 38 | <# 39 | .SYNOPSIS 40 | This function is used to authenticate with the Graph API REST interface 41 | .DESCRIPTION 42 | The function authenticate with the Graph API Interface with the tenant name 43 | .EXAMPLE 44 | Get-AuthToken 45 | Authenticates you with the Graph API interface 46 | .NOTES 47 | NAME: Get-AuthToken 48 | #> 49 | 50 | param 51 | ( 52 | [Parameter(Mandatory=$true)] 53 | $TenantID, 54 | [Parameter(Mandatory=$true)] 55 | $ClientID, 56 | [Parameter(Mandatory=$true)] 57 | $ClientSecret 58 | ) 59 | 60 | try{ 61 | # Define parameters for Microsoft Graph access token retrieval 62 | $resource = "https://graph.microsoft.com" 63 | $authority = "https://login.microsoftonline.com/$TenantID" 64 | $tokenEndpointUri = "$authority/oauth2/token" 65 | 66 | # Get the access token using grant type client_credentials for Application Permissions 67 | $content = "grant_type=client_credentials&client_id=$ClientID&client_secret=$ClientSecret&resource=$resource" 68 | $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing -Verbose:$false 69 | 70 | Write-Host "Got new Access Token!" -ForegroundColor Green 71 | Write-Host 72 | 73 | # If the accesstoken is valid then create the authentication header 74 | if($response.access_token){ 75 | 76 | # Creating header for Authorization token 77 | 78 | $authHeader = @{ 79 | 'Content-Type'='application/json' 80 | 'Authorization'="Bearer " + $response.access_token 81 | 'ExpiresOn'=$response.expires_on 82 | } 83 | 84 | return $authHeader 85 | 86 | } 87 | else{ 88 | Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." 89 | break 90 | } 91 | } 92 | catch{ 93 | FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" 94 | } 95 | 96 | } 97 | 98 | #################################################### 99 | 100 | Function Get-ValidToken { 101 | 102 | <# 103 | .SYNOPSIS 104 | This function is used to identify a possible existing Auth Token, and renew it using Get-AuthToken, if it's expired 105 | .DESCRIPTION 106 | Retreives any existing Auth Token in the session, and checks for expiration. If Expired, it will run the Get-AuthToken Fucntion to retreive a new valid Auth Token. 107 | .EXAMPLE 108 | Get-ValidToken 109 | Authenticates you with the Graph API interface by reusing a valid token if available - else a new one is requested using Get-AuthToken 110 | .NOTES 111 | NAME: Get-ValidToken 112 | #> 113 | 114 | #Fixing client_secret illegal char (+), which do't go well with web requests 115 | $client_secret = $($client_secret).Replace("+","%2B") 116 | 117 | # Checking if authToken exists before running authentication 118 | if($global:authToken){ 119 | 120 | # Get current time in (UTC) UNIX format (and ditch the milliseconds) 121 | $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] 122 | 123 | # If the authToken exists checking when it expires (converted to minutes for readability in output) 124 | $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) 125 | 126 | if($TokenExpires -le 0){ 127 | Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green 128 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 129 | } 130 | else{ 131 | Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green 132 | #Write-Host 133 | } 134 | } 135 | # Authentication doesn't exist, calling Get-AuthToken function 136 | else { 137 | # Getting the authorization token 138 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 139 | } 140 | } 141 | 142 | #################################################### 143 | 144 | Function Get-DefenderAgentsReport(){ 145 | 146 | <# 147 | .SYNOPSIS 148 | This function is used to get applications from the Graph API REST interface 149 | .DESCRIPTION 150 | The function connects to the Graph API Interface and gets any applications added 151 | .EXAMPLE 152 | Get-IntuneApplication 153 | Returns any applications configured in Intune 154 | .NOTES 155 | NAME: Get-DefenderAgentsReport 156 | #> 157 | 158 | [cmdletbinding()] 159 | 160 | param 161 | ( 162 | [parameter(Mandatory=$false)] 163 | [ValidateNotNullOrEmpty()] 164 | [string]$type 165 | ) 166 | 167 | $graphApiVersion = "Beta" 168 | $Resource = "deviceManagement/reports/exportJobs" 169 | 170 | try { 171 | 172 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 173 | 174 | $JSON = @" 175 | { 176 | "reportName": "$type", 177 | "filter": "", 178 | "select": [ 179 | "DeviceName", 180 | "DeviceId", 181 | "DeviceState", 182 | "PendingReboot", 183 | "CriticalFailure", 184 | "MalwareProtectionEnabled", 185 | "RealTimeProtectionEnabled", 186 | "RebootRequired", 187 | "FullScanRequired", 188 | "EngineVersion", 189 | "AntiMalwareVersion", 190 | "LastQuickScanDateTime", 191 | "LastFullScanDateTime", 192 | "UPN", 193 | "UserName" 194 | ], 195 | "localization": "true", 196 | "ColumnName": "ui" 197 | } 198 | "@ 199 | 200 | Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" 201 | 202 | 203 | } 204 | 205 | catch { 206 | 207 | $ex = $_.Exception 208 | Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red 209 | $errorResponse = $ex.Response.GetResponseStream() 210 | $reader = New-Object System.IO.StreamReader($errorResponse) 211 | $reader.BaseStream.Position = 0 212 | $reader.DiscardBufferedData() 213 | $responseBody = $reader.ReadToEnd(); 214 | Write-Host "Response content:`n$responseBody" -f Red 215 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 216 | write-host 217 | break 218 | 219 | } 220 | 221 | } 222 | 223 | #################################################### 224 | 225 | Function Get-ReportStatus(){ 226 | 227 | <# 228 | .SYNOPSIS 229 | This function is used to get applications from the Graph API REST interface 230 | .DESCRIPTION 231 | The function connects to the Graph API Interface and gets any applications added 232 | .EXAMPLE 233 | Get-IntuneApplication 234 | Returns any applications configured in Intune 235 | .NOTES 236 | NAME: Get-DefenderAgentsReportStatus 237 | #> 238 | 239 | [cmdletbinding()] 240 | 241 | param 242 | ( 243 | [Parameter(Mandatory=$true)] 244 | [string] $reportId 245 | ) 246 | 247 | $graphApiVersion = "Beta" 248 | $Resource = "deviceManagement/reports/exportJobs" 249 | 250 | try { 251 | 252 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)('$reportId')" 253 | 254 | Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 255 | 256 | 257 | } 258 | 259 | catch { 260 | 261 | $ex = $_.Exception 262 | Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red 263 | $errorResponse = $ex.Response.GetResponseStream() 264 | $reader = New-Object System.IO.StreamReader($errorResponse) 265 | $reader.BaseStream.Position = 0 266 | $reader.DiscardBufferedData() 267 | $responseBody = $reader.ReadToEnd(); 268 | Write-Host "Response content:`n$responseBody" -f Red 269 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 270 | write-host 271 | break 272 | 273 | } 274 | 275 | } 276 | 277 | #################################################### 278 | 279 | 280 | #region Authentication 281 | 282 | # Checking if authToken exists before running authentication 283 | if($global:authToken){ 284 | 285 | # Setting DateTime to Universal time to work in all timezones 286 | $DateTime = (Get-Date).ToUniversalTime() 287 | 288 | # If the authToken exists checking when it expires 289 | $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes 290 | 291 | if($TokenExpires -le 0){ 292 | 293 | Write-Output ("Authentication Token expired" + $TokenExpires + "minutes ago") 294 | 295 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 296 | Get-ValidToken 297 | 298 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 299 | } 300 | } 301 | 302 | # Authentication doesn't exist, calling Get-AuthToken function 303 | 304 | else { 305 | 306 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 307 | Get-ValidToken 308 | 309 | # Getting the authorization token 310 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 311 | } 312 | 313 | #endregion 314 | 315 | #################################################### 316 | 317 | $report = Get-DefenderAgentsReport -type "DefenderAgents" 318 | 319 | Write-Host "Running report..." -f Cyan 320 | 321 | $reportid = $report.id 322 | $reportstatus = $report.status 323 | 324 | # wait for report to complete 325 | while ($reportstatus -ine "completed") { 326 | $getreport = Get-ReportStatus -reportId $reportid 327 | 328 | $getreportstatus = $getreport.status 329 | 330 | Write-Host "In progress..." -f Yellow 331 | 332 | $reportstatus = $getreportstatus 333 | 334 | Start-Sleep -Seconds 5 335 | 336 | } 337 | 338 | Write-Host "Success!" -f Green 339 | 340 | $url = $getreport.url 341 | 342 | $outputfolder = ".\report.zip" 343 | 344 | # download zip folder 345 | Invoke-WebRequest -Uri $url -OutFile $outputfolder 346 | 347 | # unzip 348 | Expand-Archive -LiteralPath ".\report.zip" -DestinationPath ".\myreport" 349 | 350 | # Rename file.csv 351 | $csv = Get-ChildItem '.\myreport\*.csv' | Select-Object -ExpandProperty Name 352 | Rename-Item -Path ".\myreport\$csv" -NewName "antivirusreport.csv" 353 | 354 | 355 | $connectionName = "AzureRunAsConnection" 356 | try 357 | { 358 | # Get the connection "AzureRunAsConnection " 359 | $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName 360 | 361 | "Logging in to Azure..." 362 | Add-AzureRmAccount ` 363 | -ServicePrincipal ` 364 | -TenantId $servicePrincipalConnection.TenantId ` 365 | -ApplicationId $servicePrincipalConnection.ApplicationId ` 366 | -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 367 | } 368 | catch { 369 | if (!$servicePrincipalConnection) 370 | { 371 | $ErrorMessage = "Connection $connectionName not found." 372 | throw $ErrorMessage 373 | } else{ 374 | Write-Error -Message $_.Exception 375 | throw $_.Exception 376 | } 377 | } 378 | 379 | Select-AzureRmSubscription -SubscriptionId $subscriptionID 380 | 381 | Set-AzureRmCurrentStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $resourceGroupName 382 | 383 | Set-AzureStorageBlobContent -Container $ContainerName -File .\myreport\antivirusreport.csv -Blob AntivirusReport.csv -Force 384 | 385 | #Add snapshot file with timestamp 386 | $date = Get-Date -format "dd-MMM-yyyy_HH:mm" 387 | $timeStampFileName = "antivirusreport_" + $date + ".csv" 388 | Set-AzureStorageBlobContent -Container $containerSnapshotName -File '.\myreport\antivirusreport.csv' -Blob $timeStampFileName -Force -------------------------------------------------------------------------------- /Scripts/AppInstallStates.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .COPYRIGHT 4 | Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 5 | See LICENSE in the project root for license information. 6 | 7 | #> 8 | 9 | #################################################### 10 | 11 | # Variables 12 | $subscriptionID = Get-AutomationVariable 'subscriptionID' # Azure Subscription ID Variable 13 | $tenantID = Get-AutomationVariable 'tenantID' # Azure Tenant ID Variable 14 | $resourceGroupName = Get-AutomationVariable 'resourceGroupName' # Resource group name 15 | $storageAccountName = Get-AutomationVariable 'storageAccountName' # Storage account name 16 | 17 | # Report specific Variables 18 | $outputContainerName = Get-AutomationVariable 'appinstallstate' # Resource group name 19 | $snapshotsContainerName = Get-AutomationVariable 'appinstallstatesnapshots' # Storage account name 20 | 21 | # Graph App Registration Creds 22 | 23 | # Uses a Secret Credential named 'GraphApi' in your Automation Account 24 | $clientInfo = Get-AutomationPSCredential 'GraphApi' 25 | # Username of Automation Credential is the Graph App Registration client ID 26 | $clientID = $clientInfo.UserName 27 | # Password of Automation Credential is the Graph App Registration secret key (create one if needed) 28 | $secretPass = $clientInfo.GetNetworkCredential().Password 29 | 30 | #Required credentials - Get the client_id and client_secret from the app when creating it in Azure AD 31 | $client_id = $clientID #App ID 32 | $client_secret = $secretPass #API Access Key Password 33 | 34 | #################################################### 35 | 36 | function Get-AuthToken { 37 | 38 | <# 39 | .SYNOPSIS 40 | This function is used to authenticate with the Graph API REST interface 41 | .DESCRIPTION 42 | The function authenticate with the Graph API Interface with the tenant name 43 | .EXAMPLE 44 | Get-AuthToken 45 | Authenticates you with the Graph API interface 46 | .NOTES 47 | NAME: Get-AuthToken 48 | #> 49 | 50 | param 51 | ( 52 | [Parameter(Mandatory=$true)] 53 | $TenantID, 54 | [Parameter(Mandatory=$true)] 55 | $ClientID, 56 | [Parameter(Mandatory=$true)] 57 | $ClientSecret 58 | ) 59 | 60 | try{ 61 | # Define parameters for Microsoft Graph access token retrieval 62 | $resource = "https://graph.microsoft.com" 63 | $authority = "https://login.microsoftonline.com/$TenantID" 64 | $tokenEndpointUri = "$authority/oauth2/token" 65 | 66 | # Get the access token using grant type client_credentials for Application Permissions 67 | $content = "grant_type=client_credentials&client_id=$ClientID&client_secret=$ClientSecret&resource=$resource" 68 | $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing -Verbose:$false 69 | 70 | Write-Host "Got new Access Token!" -ForegroundColor Green 71 | Write-Host 72 | 73 | # If the accesstoken is valid then create the authentication header 74 | if($response.access_token){ 75 | 76 | # Creating header for Authorization token 77 | 78 | $authHeader = @{ 79 | 'Content-Type'='application/json' 80 | 'Authorization'="Bearer " + $response.access_token 81 | 'ExpiresOn'=$response.expires_on 82 | } 83 | 84 | return $authHeader 85 | 86 | } 87 | else{ 88 | Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." 89 | break 90 | } 91 | } 92 | catch{ 93 | FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" 94 | } 95 | 96 | } 97 | 98 | #################################################### 99 | 100 | Function Get-ValidToken { 101 | 102 | <# 103 | .SYNOPSIS 104 | This function is used to identify a possible existing Auth Token, and renew it using Get-AuthToken, if it's expired 105 | .DESCRIPTION 106 | Retreives any existing Auth Token in the session, and checks for expiration. If Expired, it will run the Get-AuthToken Fucntion to retreive a new valid Auth Token. 107 | .EXAMPLE 108 | Get-ValidToken 109 | Authenticates you with the Graph API interface by reusing a valid token if available - else a new one is requested using Get-AuthToken 110 | .NOTES 111 | NAME: Get-ValidToken 112 | #> 113 | 114 | #Fixing client_secret illegal char (+), which do't go well with web requests 115 | $client_secret = $($client_secret).Replace("+","%2B") 116 | 117 | # Checking if authToken exists before running authentication 118 | if($global:authToken){ 119 | 120 | # Get current time in (UTC) UNIX format (and ditch the milliseconds) 121 | $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] 122 | 123 | # If the authToken exists checking when it expires (converted to minutes for readability in output) 124 | $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) 125 | 126 | if($TokenExpires -le 0){ 127 | Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green 128 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 129 | } 130 | else{ 131 | Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green 132 | #Write-Host 133 | } 134 | } 135 | # Authentication doesn't exist, calling Get-AuthToken function 136 | else { 137 | # Getting the authorization token 138 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 139 | } 140 | } 141 | 142 | #################################################### 143 | 144 | Function Get-IntuneApplication(){ 145 | 146 | <# 147 | .SYNOPSIS 148 | This function is used to get applications from the Graph API REST interface 149 | .DESCRIPTION 150 | The function connects to the Graph API Interface and gets any applications added 151 | .EXAMPLE 152 | Get-IntuneApplication 153 | Returns any applications configured in Intune 154 | .NOTES 155 | NAME: Get-IntuneApplication 156 | #> 157 | 158 | [cmdletbinding()] 159 | 160 | param 161 | ( 162 | [parameter(Mandatory=$false)] 163 | [ValidateNotNullOrEmpty()] 164 | [string]$Name, 165 | [parameter(Mandatory=$false)] 166 | [ValidateNotNullOrEmpty()] 167 | [string]$type 168 | ) 169 | 170 | $graphApiVersion = "Beta" 171 | $Resource = "deviceAppManagement/mobileApps" 172 | 173 | try { 174 | 175 | if($Name){ 176 | 177 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 178 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") -and (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } 179 | 180 | } elseif($type){ 181 | 182 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 183 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("$type") } 184 | 185 | } 186 | 187 | else { 188 | 189 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 190 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { (!($_.'@odata.type').Contains("managed")) -and (!($_.'@odata.type').Contains("#microsoft.graph.iosVppApp")) } 191 | 192 | } 193 | 194 | } 195 | 196 | catch { 197 | 198 | $ex = $_.Exception 199 | Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red 200 | $errorResponse = $ex.Response.GetResponseStream() 201 | $reader = New-Object System.IO.StreamReader($errorResponse) 202 | $reader.BaseStream.Position = 0 203 | $reader.DiscardBufferedData() 204 | $responseBody = $reader.ReadToEnd(); 205 | Write-Host "Response content:`n$responseBody" -f Red 206 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 207 | write-host 208 | break 209 | 210 | } 211 | 212 | } 213 | 214 | #################################################### 215 | 216 | Function Get-ApplicationAssignment(){ 217 | 218 | <# 219 | .SYNOPSIS 220 | This function is used to get an application assignment from the Graph API REST interface 221 | .DESCRIPTION 222 | The function connects to the Graph API Interface and gets an application assignment 223 | .EXAMPLE 224 | Get-ApplicationAssignment 225 | Returns an Application Assignment configured in Intune 226 | .NOTES 227 | NAME: Get-ApplicationAssignment 228 | #> 229 | 230 | [cmdletbinding()] 231 | 232 | param 233 | ( 234 | $ApplicationId 235 | ) 236 | 237 | $graphApiVersion = "Beta" 238 | $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assignments" 239 | 240 | try { 241 | 242 | if(!$ApplicationId){ 243 | 244 | write-host "No Application Id specified, specify a valid Application Id" -f Red 245 | break 246 | 247 | } 248 | 249 | else { 250 | 251 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 252 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 253 | 254 | } 255 | 256 | } 257 | 258 | catch { 259 | 260 | $ex = $_.Exception 261 | $errorResponse = $ex.Response.GetResponseStream() 262 | $reader = New-Object System.IO.StreamReader($errorResponse) 263 | $reader.BaseStream.Position = 0 264 | $reader.DiscardBufferedData() 265 | $responseBody = $reader.ReadToEnd(); 266 | Write-Host "Response content:`n$responseBody" -f Red 267 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 268 | write-host 269 | break 270 | 271 | } 272 | 273 | } 274 | 275 | #################################################### 276 | 277 | Function Get-AADGroup(){ 278 | 279 | <# 280 | .SYNOPSIS 281 | This function is used to get AAD Groups from the Graph API REST interface 282 | .DESCRIPTION 283 | The function connects to the Graph API Interface and gets any Groups registered with AAD 284 | .EXAMPLE 285 | Get-AADGroup 286 | Returns all users registered with Azure AD 287 | .NOTES 288 | NAME: Get-AADGroup 289 | #> 290 | 291 | [cmdletbinding()] 292 | 293 | param 294 | ( 295 | $GroupName, 296 | $id, 297 | [switch]$Members 298 | ) 299 | 300 | # Defining Variables 301 | $graphApiVersion = "v1.0" 302 | $Group_resource = "groups" 303 | 304 | try { 305 | 306 | if($id){ 307 | 308 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" 309 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 310 | 311 | } 312 | 313 | elseif($GroupName -eq "" -or $GroupName -eq $null){ 314 | 315 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" 316 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 317 | 318 | } 319 | 320 | else { 321 | 322 | if(!$Members){ 323 | 324 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 325 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 326 | 327 | } 328 | 329 | elseif($Members){ 330 | 331 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 332 | $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 333 | 334 | if($Group){ 335 | 336 | $GID = $Group.id 337 | 338 | $Group.displayName 339 | write-host 340 | 341 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" 342 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 343 | 344 | } 345 | 346 | } 347 | 348 | } 349 | 350 | } 351 | 352 | catch { 353 | 354 | $ex = $_.Exception 355 | $errorResponse = $ex.Response.GetResponseStream() 356 | $reader = New-Object System.IO.StreamReader($errorResponse) 357 | $reader.BaseStream.Position = 0 358 | $reader.DiscardBufferedData() 359 | $responseBody = $reader.ReadToEnd(); 360 | Write-Host "Response content:`n$responseBody" -f Red 361 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 362 | write-host 363 | break 364 | 365 | } 366 | 367 | } 368 | 369 | #################################################### 370 | 371 | Function Get-InstallStatusForApp { 372 | 373 | <# 374 | .SYNOPSIS 375 | This function will get the installation status of an application given the application's ID. 376 | .DESCRIPTION 377 | If you want to track your managed intune application installation stats as you roll them out in your environment, use this commandlet to get the insights. 378 | .EXAMPLE 379 | Get-InstallStatusForApp -AppId a1a2a-b1b2b3b4-c1c2c3c4 380 | This will return the installation status of the application with the ID of a1a2a-b1b2b3b4-c1c2c3c4 381 | .NOTES 382 | NAME: Get-InstallStatusForApp 383 | #> 384 | 385 | [cmdletbinding()] 386 | 387 | param 388 | ( 389 | [Parameter(Mandatory=$true)] 390 | [string]$AppId 391 | ) 392 | 393 | $graphApiVersion = "Beta" 394 | $Resource = "deviceAppManagement/mobileApps/$AppId/installSummary" 395 | 396 | try 397 | { 398 | 399 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 400 | Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 401 | 402 | } 403 | 404 | catch 405 | { 406 | 407 | $ex = $_.Exception 408 | $errorResponse = $ex.Response.GetResponseStream() 409 | $reader = New-Object System.IO.StreamReader($errorResponse) 410 | $reader.BaseStream.Position = 0 411 | $reader.DiscardBufferedData() 412 | $responseBody = $reader.ReadToEnd(); 413 | Write-Host "Response content:`n$responseBody" -f Red 414 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 415 | write-host 416 | break 417 | 418 | } 419 | 420 | } 421 | 422 | #################################################### 423 | 424 | Function Get-DeviceStatusForApp { 425 | 426 | <# 427 | .SYNOPSIS 428 | This function will get the devices installation status of an application given the application's ID. 429 | .DESCRIPTION 430 | If you want to track your managed intune application installation stats as you roll them out in your environment, use this commandlet to get the insights. 431 | .EXAMPLE 432 | Get-DeviceStatusForApp -AppId a1a2a-b1b2b3b4-c1c2c3c4 433 | This will return devices and their installation status of the application with the ID of a1a2a-b1b2b3b4-c1c2c3c4 434 | .NOTES 435 | NAME: Get-DeviceStatusForApp 436 | #> 437 | 438 | [cmdletbinding()] 439 | 440 | param 441 | ( 442 | [Parameter(Mandatory=$true)] 443 | [string]$AppId 444 | ) 445 | 446 | $graphApiVersion = "Beta" 447 | $Resource = "deviceAppManagement/mobileApps/$AppId/deviceStatuses" 448 | 449 | try 450 | { 451 | 452 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 453 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 454 | 455 | } 456 | 457 | catch 458 | { 459 | 460 | $ex = $_.Exception 461 | $errorResponse = $ex.Response.GetResponseStream() 462 | $reader = New-Object System.IO.StreamReader($errorResponse) 463 | $reader.BaseStream.Position = 0 464 | $reader.DiscardBufferedData() 465 | $responseBody = $reader.ReadToEnd(); 466 | Write-Host "Response content:`n$responseBody" -f Red 467 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 468 | write-host 469 | break 470 | 471 | } 472 | 473 | } 474 | 475 | 476 | #################################################### 477 | 478 | 479 | #region Authentication 480 | 481 | # Checking if authToken exists before running authentication 482 | if($global:authToken){ 483 | 484 | # Setting DateTime to Universal time to work in all timezones 485 | $DateTime = (Get-Date).ToUniversalTime() 486 | 487 | # If the authToken exists checking when it expires 488 | $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes 489 | 490 | if($TokenExpires -le 0){ 491 | 492 | Write-Output ("Authentication Token expired" + $TokenExpires + "minutes ago") 493 | 494 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 495 | Get-ValidToken 496 | 497 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 498 | } 499 | } 500 | 501 | # Authentication doesn't exist, calling Get-AuthToken function 502 | 503 | else { 504 | 505 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 506 | Get-ValidToken 507 | 508 | # Getting the authorization token 509 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 510 | } 511 | 512 | #endregion 513 | 514 | #################################################### 515 | 516 | $outputArray = @() 517 | 518 | write-host "Getting all application install status per device" -f Green 519 | 520 | $Applications = @() 521 | $Applications += Get-IntuneApplication -type "windowsStoreApp" 522 | $Applications += Get-IntuneApplication -type "StoreForBusinessApp" 523 | $Applications += Get-IntuneApplication -type "windowsUniversalAppX" 524 | $Applications += Get-IntuneApplication -type "win32LobApp" 525 | $Applications += Get-IntuneApplication -type "webApp" 526 | $Applications += Get-IntuneApplication -type "windowsMobileMSI" 527 | 528 | 529 | foreach($app in $Applications) { 530 | 531 | $appName = $app.displayname 532 | $appID = $app.id 533 | 534 | write-host "App name: $appName" -f Yellow 535 | 536 | $deviceStatus = Get-DeviceStatusForApp -AppId $appID 537 | $assignments = Get-ApplicationAssignment -ApplicationId $appID 538 | 539 | foreach($assignment in $assignments) { 540 | 541 | $intent = $assignment.intent 542 | 543 | $groupName = (Get-AADGroup -id $assignment.target.groupID).displayName 544 | 545 | foreach($device in $deviceStatus) { 546 | 547 | $deviceName = $device.deviceName 548 | $installState = $device.installState 549 | $errorCode = $device.errorCode 550 | 551 | if($Assignment.target.'@odata.type' -eq "#microsoft.graph.allDevicesAssignmentTarget") { 552 | 553 | if($installState -inotlike "notApplicable") { 554 | 555 | $outputArray += New-Object PSObject -Property @{ 556 | 557 | DeviceName = $deviceName 558 | AppName = $appName 559 | InstallState = $installState 560 | ErrorCode = $errorCode 561 | Intent = $assignment.intent 562 | GroupName = "All Devices" 563 | } 564 | } 565 | 566 | } elseif($Assignment.target.'@odata.type' -eq "#microsoft.graph.allLicensedUsersAssignmentTarget") { 567 | 568 | if($installState -inotlike "notApplicable") { 569 | 570 | $outputArray += New-Object PSObject -Property @{ 571 | 572 | DeviceName = $deviceName 573 | AppName = $appName 574 | InstallState = $installState 575 | ErrorCode = $errorCode 576 | Intent = $assignment.intent 577 | GroupName = "All Users" 578 | } 579 | } 580 | 581 | } else { 582 | 583 | if($installState -inotlike "notApplicable") { 584 | 585 | $outputArray += New-Object PSObject -Property @{ 586 | 587 | DeviceName = $deviceName 588 | AppName = $appName 589 | InstallState = $installState 590 | ErrorCode = $errorCode 591 | Intent = $assignment.intent 592 | GroupName = $groupName 593 | } 594 | 595 | } 596 | 597 | } 598 | 599 | } 600 | 601 | } 602 | } 603 | 604 | $outputArray | Export-Csv 'appInstallStates.csv' -NoTypeInformation -Force 605 | 606 | 607 | $connectionName = "AzureRunAsConnection" 608 | try 609 | { 610 | # Get the connection "AzureRunAsConnection " 611 | $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName 612 | 613 | "Logging in to Azure..." 614 | Add-AzureRmAccount ` 615 | -ServicePrincipal ` 616 | -TenantId $servicePrincipalConnection.TenantId ` 617 | -ApplicationId $servicePrincipalConnection.ApplicationId ` 618 | -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 619 | } 620 | catch { 621 | if (!$servicePrincipalConnection) 622 | { 623 | $ErrorMessage = "Connection $connectionName not found." 624 | throw $ErrorMessage 625 | } else{ 626 | Write-Error -Message $_.Exception 627 | throw $_.Exception 628 | } 629 | } 630 | 631 | Select-AzureRmSubscription -SubscriptionId $subscriptionID 632 | 633 | Set-AzureRmCurrentStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $resourceGroupName 634 | 635 | Set-AzureStorageBlobContent -Container $outputContainerName -File appInstallStates.csv -Blob appInstallStates.csv -Force 636 | 637 | #Add snapshot file with timestamp 638 | $date = Get-Date -format "dd-MMM-yyyy_HH:mm" 639 | $timeStampFileName = "appInstallStates_" + $date + ".csv" 640 | Set-AzureStorageBlobContent -Container $snapshotsContainerName -File appInstallStates.csv -Blob $timeStampFileName -Force 641 | 642 | -------------------------------------------------------------------------------- /Scripts/DeviceConfigurationGroups.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .COPYRIGHT 4 | Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 5 | See LICENSE in the project root for license information. 6 | 7 | #> 8 | 9 | #################################################### 10 | 11 | # Variables 12 | $subscriptionID = Get-AutomationVariable 'subscriptionID' # Azure Subscription ID Variable 13 | $tenantID = Get-AutomationVariable 'tenantID' # Azure Tenant ID Variable 14 | $resourceGroupName = Get-AutomationVariable 'resourceGroupName' # Resource group name 15 | $storageAccountName = Get-AutomationVariable 'storageAccountName' # Storage account name 16 | 17 | # Report specific Variables 18 | $outputContainerName = Get-AutomationVariable 'deviceconfigurationgroup' # Resource group name 19 | $snapshotsContainerName = Get-AutomationVariable 'deviceconfigurationgroupsnapshots' # Storage account name 20 | 21 | # Graph App Registration Creds 22 | 23 | # Uses a Secret Credential named 'GraphApi' in your Automation Account 24 | $clientInfo = Get-AutomationPSCredential 'GraphApi' 25 | # Username of Automation Credential is the Graph App Registration client ID 26 | $clientID = $clientInfo.UserName 27 | # Password of Automation Credential is the Graph App Registration secret key (create one if needed) 28 | $secretPass = $clientInfo.GetNetworkCredential().Password 29 | 30 | #Required credentials - Get the client_id and client_secret from the app when creating it in Azure AD 31 | $client_id = $clientID #App ID 32 | $client_secret = $secretPass #API Access Key Password 33 | 34 | #################################################### 35 | 36 | function Get-AuthToken { 37 | 38 | <# 39 | .SYNOPSIS 40 | This function is used to authenticate with the Graph API REST interface 41 | .DESCRIPTION 42 | The function authenticate with the Graph API Interface with the tenant name 43 | .EXAMPLE 44 | Get-AuthToken 45 | Authenticates you with the Graph API interface 46 | .NOTES 47 | NAME: Get-AuthToken 48 | #> 49 | 50 | param 51 | ( 52 | [Parameter(Mandatory=$true)] 53 | $TenantID, 54 | [Parameter(Mandatory=$true)] 55 | $ClientID, 56 | [Parameter(Mandatory=$true)] 57 | $ClientSecret 58 | ) 59 | 60 | try{ 61 | # Define parameters for Microsoft Graph access token retrieval 62 | $resource = "https://graph.microsoft.com" 63 | $authority = "https://login.microsoftonline.com/$TenantID" 64 | $tokenEndpointUri = "$authority/oauth2/token" 65 | 66 | # Get the access token using grant type client_credentials for Application Permissions 67 | $content = "grant_type=client_credentials&client_id=$ClientID&client_secret=$ClientSecret&resource=$resource" 68 | $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing -Verbose:$false 69 | 70 | Write-Host "Got new Access Token!" -ForegroundColor Green 71 | Write-Host 72 | 73 | # If the accesstoken is valid then create the authentication header 74 | if($response.access_token){ 75 | 76 | # Creating header for Authorization token 77 | 78 | $authHeader = @{ 79 | 'Content-Type'='application/json' 80 | 'Authorization'="Bearer " + $response.access_token 81 | 'ExpiresOn'=$response.expires_on 82 | } 83 | 84 | return $authHeader 85 | 86 | } 87 | else{ 88 | Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." 89 | break 90 | } 91 | } 92 | catch{ 93 | FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" 94 | } 95 | 96 | } 97 | 98 | #################################################### 99 | 100 | Function Get-ValidToken { 101 | 102 | <# 103 | .SYNOPSIS 104 | This function is used to identify a possible existing Auth Token, and renew it using Get-AuthToken, if it's expired 105 | .DESCRIPTION 106 | Retreives any existing Auth Token in the session, and checks for expiration. If Expired, it will run the Get-AuthToken Fucntion to retreive a new valid Auth Token. 107 | .EXAMPLE 108 | Get-ValidToken 109 | Authenticates you with the Graph API interface by reusing a valid token if available - else a new one is requested using Get-AuthToken 110 | .NOTES 111 | NAME: Get-ValidToken 112 | #> 113 | 114 | #Fixing client_secret illegal char (+), which do't go well with web requests 115 | $client_secret = $($client_secret).Replace("+","%2B") 116 | 117 | # Checking if authToken exists before running authentication 118 | if($global:authToken){ 119 | 120 | # Get current time in (UTC) UNIX format (and ditch the milliseconds) 121 | $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] 122 | 123 | # If the authToken exists checking when it expires (converted to minutes for readability in output) 124 | $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) 125 | 126 | if($TokenExpires -le 0){ 127 | Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green 128 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 129 | } 130 | else{ 131 | Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green 132 | #Write-Host 133 | } 134 | } 135 | # Authentication doesn't exist, calling Get-AuthToken function 136 | else { 137 | # Getting the authorization token 138 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 139 | } 140 | } 141 | 142 | #################################################### 143 | 144 | Function Get-DeviceConfigurationPolicy(){ 145 | 146 | <# 147 | .SYNOPSIS 148 | This function is used to get device configuration policies from the Graph API REST interface 149 | .DESCRIPTION 150 | The function connects to the Graph API Interface and gets any device configuration policies 151 | .EXAMPLE 152 | Get-DeviceConfigurationPolicy 153 | Returns any device configuration policies configured in Intune 154 | .NOTES 155 | NAME: Get-DeviceConfigurationPolicy 156 | #> 157 | 158 | [cmdletbinding()] 159 | 160 | param 161 | ( 162 | $name 163 | ) 164 | 165 | $graphApiVersion = "Beta" 166 | $DCP_resource = "deviceManagement/deviceConfigurations" 167 | 168 | try { 169 | 170 | if($Name){ 171 | 172 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 173 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } 174 | 175 | } 176 | 177 | else { 178 | 179 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 180 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 181 | 182 | } 183 | 184 | } 185 | 186 | catch { 187 | 188 | $ex = $_.Exception 189 | $errorResponse = $ex.Response.GetResponseStream() 190 | $reader = New-Object System.IO.StreamReader($errorResponse) 191 | $reader.BaseStream.Position = 0 192 | $reader.DiscardBufferedData() 193 | $responseBody = $reader.ReadToEnd(); 194 | Write-Host "Response content:`n$responseBody" -f Red 195 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 196 | write-host 197 | break 198 | 199 | } 200 | 201 | } 202 | 203 | #################################################### 204 | 205 | Function Get-DeviceConfigurationPolicyAssignment(){ 206 | 207 | <# 208 | .SYNOPSIS 209 | This function is used to get device configuration policy assignment from the Graph API REST interface 210 | .DESCRIPTION 211 | The function connects to the Graph API Interface and gets a device configuration policy assignment 212 | .EXAMPLE 213 | Get-DeviceConfigurationPolicyAssignment $id guid 214 | Returns any device configuration policy assignment configured in Intune 215 | .NOTES 216 | NAME: Get-DeviceConfigurationPolicyAssignment 217 | #> 218 | 219 | [cmdletbinding()] 220 | 221 | param 222 | ( 223 | [Parameter(Mandatory=$true,HelpMessage="Enter id (guid) for the Device Configuration Policy you want to check assignment")] 224 | $id 225 | ) 226 | 227 | $graphApiVersion = "Beta" 228 | $DCP_resource = "deviceManagement/deviceConfigurations" 229 | 230 | try { 231 | 232 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" 233 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 234 | 235 | } 236 | 237 | catch { 238 | 239 | $ex = $_.Exception 240 | $errorResponse = $ex.Response.GetResponseStream() 241 | $reader = New-Object System.IO.StreamReader($errorResponse) 242 | $reader.BaseStream.Position = 0 243 | $reader.DiscardBufferedData() 244 | $responseBody = $reader.ReadToEnd(); 245 | Write-Host "Response content:`n$responseBody" -f Red 246 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 247 | write-host 248 | break 249 | 250 | } 251 | 252 | } 253 | 254 | #################################################### 255 | 256 | Function Get-AADGroup(){ 257 | 258 | <# 259 | .SYNOPSIS 260 | This function is used to get AAD Groups from the Graph API REST interface 261 | .DESCRIPTION 262 | The function connects to the Graph API Interface and gets any Groups registered with AAD 263 | .EXAMPLE 264 | Get-AADGroup 265 | Returns all users registered with Azure AD 266 | .NOTES 267 | NAME: Get-AADGroup 268 | #> 269 | 270 | [cmdletbinding()] 271 | 272 | param 273 | ( 274 | $GroupName, 275 | $id, 276 | [switch]$Members 277 | ) 278 | 279 | # Defining Variables 280 | $graphApiVersion = "v1.0" 281 | $Group_resource = "groups" 282 | # pseudo-group identifiers for all users and all devices 283 | [string]$AllUsers = "acacacac-9df4-4c7d-9d50-4ef0226f57a9" 284 | [string]$AllDevices = "adadadad-808e-44e2-905a-0b7873a8a531" 285 | 286 | try { 287 | 288 | if($id){ 289 | 290 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" 291 | switch ( $id ) { 292 | $AllUsers { $grp = [PSCustomObject]@{ displayName = "All users"}; $grp } 293 | $AllDevices { $grp = [PSCustomObject]@{ displayName = "All devices"}; $grp } 294 | default { (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } 295 | } 296 | 297 | } 298 | 299 | elseif($GroupName -eq "" -or $GroupName -eq $null){ 300 | 301 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" 302 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 303 | 304 | } 305 | 306 | else { 307 | 308 | if(!$Members){ 309 | 310 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 311 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 312 | 313 | } 314 | 315 | elseif($Members){ 316 | 317 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 318 | $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 319 | 320 | if($Group){ 321 | 322 | $GID = $Group.id 323 | 324 | $Group.displayName 325 | write-host 326 | 327 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" 328 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 329 | 330 | } 331 | 332 | } 333 | 334 | } 335 | 336 | } 337 | 338 | catch { 339 | 340 | $ex = $_.Exception 341 | $errorResponse = $ex.Response.GetResponseStream() 342 | $reader = New-Object System.IO.StreamReader($errorResponse) 343 | $reader.BaseStream.Position = 0 344 | $reader.DiscardBufferedData() 345 | $responseBody = $reader.ReadToEnd(); 346 | Write-Host "Response content:`n$responseBody" -f Red 347 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 348 | write-host 349 | break 350 | 351 | } 352 | 353 | } 354 | 355 | #################################################### 356 | 357 | #region Authentication 358 | 359 | # Checking if authToken exists before running authentication 360 | if($global:authToken){ 361 | 362 | # Setting DateTime to Universal time to work in all timezones 363 | $DateTime = (Get-Date).ToUniversalTime() 364 | 365 | # If the authToken exists checking when it expires 366 | $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes 367 | 368 | if($TokenExpires -le 0){ 369 | 370 | Write-Output ("Authentication Token expired" + $TokenExpires + "minutes ago") 371 | 372 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 373 | Get-ValidToken 374 | 375 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 376 | } 377 | } 378 | 379 | 380 | # Authentication doesn't exist, calling Get-AuthToken function 381 | 382 | else { 383 | 384 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 385 | Get-ValidToken 386 | 387 | # Getting the authorization token 388 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 389 | } 390 | 391 | #endregion 392 | 393 | 394 | #################################################### 395 | 396 | $DCPs = Get-DeviceConfigurationPolicy 397 | 398 | write-host 399 | 400 | $outputArray = @() 401 | 402 | foreach($DCP in $DCPs){ 403 | 404 | write-host "Device Configuration Policy:"$DCP.displayName -f Yellow 405 | write-host 406 | 407 | $id = $DCP.id 408 | 409 | $DCPA = Get-DeviceConfigurationPolicyAssignment -id $id 410 | write-host "Getting Configuration Policy assignment..." -f Cyan 411 | 412 | $DCPA 413 | 414 | if($DCPA){ 415 | 416 | if($DCPA.count -gt 1){ 417 | 418 | foreach($group in $DCPA){ 419 | 420 | $groupname = (Get-AADGroup -id $group.targetGroupId).displayName 421 | 422 | $outputArray += New-Object PSObject -Property @{ 423 | 424 | PolicyName = $DCP.displayName 425 | LastModifiedDate = $DCP.lastModifiedDateTime 426 | CreatedDateTime = $DCP.createdDateTime 427 | GroupName = $groupname 428 | IsExcludeGroup = $group.excludeGroup 429 | 430 | } 431 | } 432 | 433 | } 434 | 435 | else { 436 | 437 | $groupname = (Get-AADGroup -id $DCPA.targetGroupId).displayName 438 | 439 | $outputArray += New-Object PSObject -Property @{ 440 | 441 | PolicyName = $DCP.displayName 442 | LastModifiedDate = $DCP.lastModifiedDateTime 443 | CreatedDateTime = $DCP.createdDateTime 444 | GroupName = $groupname 445 | IsExcludeGroup = $group.excludeGroup 446 | } 447 | 448 | } 449 | 450 | } 451 | 452 | else { 453 | 454 | Write-Host "No assignments found." 455 | 456 | } 457 | 458 | Write-Host 459 | 460 | } 461 | 462 | $outputArray | Export-Csv 'DeviceConfigurationGroups.csv' -NoTypeInformation -Force 463 | 464 | 465 | $connectionName = "AzureRunAsConnection" 466 | try 467 | { 468 | # Get the connection "AzureRunAsConnection " 469 | $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName 470 | 471 | "Logging in to Azure..." 472 | Add-AzureRmAccount ` 473 | -ServicePrincipal ` 474 | -TenantId $servicePrincipalConnection.TenantId ` 475 | -ApplicationId $servicePrincipalConnection.ApplicationId ` 476 | -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 477 | } 478 | catch { 479 | if (!$servicePrincipalConnection) 480 | { 481 | $ErrorMessage = "Connection $connectionName not found." 482 | throw $ErrorMessage 483 | } else{ 484 | Write-Error -Message $_.Exception 485 | throw $_.Exception 486 | } 487 | } 488 | 489 | Select-AzureRmSubscription -SubscriptionId $subscriptionID 490 | 491 | Set-AzureRmCurrentStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $resourceGroupName 492 | 493 | Set-AzureStorageBlobContent -Container $outputContainerName -File DeviceConfigurationGroups.csv -Blob DeviceConfigurationGroups.csv -Force 494 | 495 | #Add snapshot file with timestamp 496 | $date = Get-Date -format "dd-MMM-yyyy_HH:mm" 497 | $timeStampFileName = "DeviceConfiguration_" + $date + ".csv" 498 | Set-AzureStorageBlobContent -Container $snapshotsContainerName -File DeviceConfigurationGroups.csv -Blob $timeStampFileName -Force -------------------------------------------------------------------------------- /Scripts/DeviceConfigurationStates.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .COPYRIGHT 4 | Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 5 | See LICENSE in the project root for license information. 6 | 7 | #> 8 | 9 | #################################################### 10 | 11 | # Variables 12 | $subscriptionID = Get-AutomationVariable 'subscriptionID' # Azure Subscription ID Variable 13 | $tenantID = Get-AutomationVariable 'tenantID' # Azure Tenant ID Variable 14 | $resourceGroupName = Get-AutomationVariable 'resourceGroupName' # Resource group name 15 | $storageAccountName = Get-AutomationVariable 'storageAccountName' # Storage account name 16 | 17 | # Report specific Variables 18 | $outputContainerName = Get-AutomationVariable 'deviceconfigurationstatus' # active container 19 | $snapshotsContainerName = Get-AutomationVariable 'deviceconfigurationstatussnapshots' # historical 20 | 21 | # Graph App Registration Creds 22 | 23 | # Uses a Secret Credential named 'GraphApi' in your Automation Account 24 | $clientInfo = Get-AutomationPSCredential 'GraphApi' 25 | # Username of Automation Credential is the Graph App Registration client ID 26 | $clientID = $clientInfo.UserName 27 | # Password of Automation Credential is the Graph App Registration secret key (create one if needed) 28 | $secretPass = $clientInfo.GetNetworkCredential().Password 29 | 30 | #Required credentials - Get the client_id and client_secret from the app when creating it in Azure AD 31 | $client_id = $clientID #App ID 32 | $client_secret = $secretPass #API Access Key Password 33 | 34 | #################################################### 35 | 36 | function Get-AuthToken { 37 | 38 | <# 39 | .SYNOPSIS 40 | This function is used to authenticate with the Graph API REST interface 41 | .DESCRIPTION 42 | The function authenticate with the Graph API Interface with the tenant name 43 | .EXAMPLE 44 | Get-AuthToken 45 | Authenticates you with the Graph API interface 46 | .NOTES 47 | NAME: Get-AuthToken 48 | #> 49 | 50 | param 51 | ( 52 | [Parameter(Mandatory=$true)] 53 | $TenantID, 54 | [Parameter(Mandatory=$true)] 55 | $ClientID, 56 | [Parameter(Mandatory=$true)] 57 | $ClientSecret 58 | ) 59 | 60 | try{ 61 | # Define parameters for Microsoft Graph access token retrieval 62 | $resource = "https://graph.microsoft.com" 63 | $authority = "https://login.microsoftonline.com/$TenantID" 64 | $tokenEndpointUri = "$authority/oauth2/token" 65 | 66 | # Get the access token using grant type client_credentials for Application Permissions 67 | $content = "grant_type=client_credentials&client_id=$ClientID&client_secret=$ClientSecret&resource=$resource" 68 | $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing -Verbose:$false 69 | 70 | Write-Host "Got new Access Token!" -ForegroundColor Green 71 | Write-Host 72 | 73 | # If the accesstoken is valid then create the authentication header 74 | if($response.access_token){ 75 | 76 | # Creating header for Authorization token 77 | 78 | $authHeader = @{ 79 | 'Content-Type'='application/json' 80 | 'Authorization'="Bearer " + $response.access_token 81 | 'ExpiresOn'=$response.expires_on 82 | } 83 | 84 | return $authHeader 85 | 86 | } 87 | else{ 88 | Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." 89 | break 90 | } 91 | } 92 | catch{ 93 | FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" 94 | } 95 | 96 | } 97 | 98 | #################################################### 99 | 100 | Function Get-ValidToken { 101 | 102 | <# 103 | .SYNOPSIS 104 | This function is used to identify a possible existing Auth Token, and renew it using Get-AuthToken, if it's expired 105 | .DESCRIPTION 106 | Retreives any existing Auth Token in the session, and checks for expiration. If Expired, it will run the Get-AuthToken Fucntion to retreive a new valid Auth Token. 107 | .EXAMPLE 108 | Get-ValidToken 109 | Authenticates you with the Graph API interface by reusing a valid token if available - else a new one is requested using Get-AuthToken 110 | .NOTES 111 | NAME: Get-ValidToken 112 | #> 113 | 114 | #Fixing client_secret illegal char (+), which do't go well with web requests 115 | $client_secret = $($client_secret).Replace("+","%2B") 116 | 117 | # Checking if authToken exists before running authentication 118 | if($global:authToken){ 119 | 120 | # Get current time in (UTC) UNIX format (and ditch the milliseconds) 121 | $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] 122 | 123 | # If the authToken exists checking when it expires (converted to minutes for readability in output) 124 | $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) 125 | 126 | if($TokenExpires -le 0){ 127 | Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green 128 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 129 | } 130 | else{ 131 | Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green 132 | #Write-Host 133 | } 134 | } 135 | # Authentication doesn't exist, calling Get-AuthToken function 136 | else { 137 | # Getting the authorization token 138 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 139 | } 140 | } 141 | 142 | #################################################### 143 | 144 | Function Get-DeviceConfigurationPolicy(){ 145 | 146 | <# 147 | .SYNOPSIS 148 | This function is used to get device configuration policies from the Graph API REST interface 149 | .DESCRIPTION 150 | The function connects to the Graph API Interface and gets any device configuration policies 151 | .EXAMPLE 152 | Get-DeviceConfigurationPolicy 153 | Returns any device configuration policies configured in Intune 154 | .NOTES 155 | NAME: Get-DeviceConfigurationPolicy 156 | #> 157 | 158 | [cmdletbinding()] 159 | 160 | param 161 | ( 162 | $name 163 | ) 164 | 165 | $graphApiVersion = "Beta" 166 | $DCP_resource = "deviceManagement/deviceConfigurations" 167 | 168 | try { 169 | 170 | if($Name){ 171 | 172 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 173 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } 174 | 175 | } 176 | 177 | else { 178 | 179 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 180 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 181 | 182 | } 183 | 184 | } 185 | 186 | catch { 187 | 188 | $ex = $_.Exception 189 | $errorResponse = $ex.Response.GetResponseStream() 190 | $reader = New-Object System.IO.StreamReader($errorResponse) 191 | $reader.BaseStream.Position = 0 192 | $reader.DiscardBufferedData() 193 | $responseBody = $reader.ReadToEnd(); 194 | Write-Host "Response content:`n$responseBody" -f Red 195 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 196 | write-host 197 | break 198 | 199 | } 200 | 201 | } 202 | 203 | #################################################### 204 | 205 | Function Get-EndpointConfigurationPolicy(){ 206 | 207 | <# 208 | .SYNOPSIS 209 | This function is used to get device configuration policies from the Graph API REST interface 210 | .DESCRIPTION 211 | The function connects to the Graph API Interface and gets any device configuration policies 212 | .EXAMPLE 213 | Get-DeviceConfigurationPolicy 214 | Returns any device configuration policies configured in Intune 215 | .NOTES 216 | NAME: Get-DeviceConfigurationPolicy 217 | #> 218 | 219 | [cmdletbinding()] 220 | 221 | param 222 | ( 223 | $name 224 | ) 225 | 226 | $graphApiVersion = "Beta" 227 | $DCP_resource = "deviceManagement/intents" 228 | 229 | try { 230 | 231 | if($Name){ 232 | 233 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 234 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } 235 | 236 | } 237 | 238 | else { 239 | 240 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 241 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 242 | 243 | } 244 | 245 | } 246 | 247 | catch { 248 | 249 | $ex = $_.Exception 250 | $errorResponse = $ex.Response.GetResponseStream() 251 | $reader = New-Object System.IO.StreamReader($errorResponse) 252 | $reader.BaseStream.Position = 0 253 | $reader.DiscardBufferedData() 254 | $responseBody = $reader.ReadToEnd(); 255 | Write-Host "Response content:`n$responseBody" -f Red 256 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 257 | write-host 258 | break 259 | 260 | } 261 | 262 | } 263 | 264 | 265 | #################################################### 266 | 267 | Function Get-DeviceConfigurationPolicyAssignmentStatus(){ 268 | 269 | <# 270 | .SYNOPSIS 271 | This function is used to get device configuration policy assignment from the Graph API REST interface 272 | .DESCRIPTION 273 | The function connects to the Graph API Interface and gets a device configuration policy assignment 274 | .EXAMPLE 275 | Get-DeviceConfigurationPolicyAssignment $id guid 276 | Returns any device configuration policy assignment configured in Intune 277 | .NOTES 278 | NAME: Get-DeviceConfigurationPolicyAssignment 279 | #> 280 | 281 | [cmdletbinding()] 282 | 283 | param 284 | ( 285 | [Parameter(Mandatory=$true,HelpMessage="Enter id (guid) for the Device Configuration Policy you want to check assignment")] 286 | $id 287 | ) 288 | 289 | $graphApiVersion = "Beta" 290 | $DCP_resource = "deviceManagement/deviceConfigurations" 291 | 292 | try { 293 | 294 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/deviceStatuses" 295 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 296 | 297 | } 298 | 299 | catch { 300 | 301 | $ex = $_.Exception 302 | $errorResponse = $ex.Response.GetResponseStream() 303 | $reader = New-Object System.IO.StreamReader($errorResponse) 304 | $reader.BaseStream.Position = 0 305 | $reader.DiscardBufferedData() 306 | $responseBody = $reader.ReadToEnd(); 307 | Write-Host "Response content:`n$responseBody" -f Red 308 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 309 | write-host 310 | break 311 | 312 | } 313 | 314 | } 315 | 316 | #################################################### 317 | 318 | Function Get-EndpointConfigurationPolicyAssignmentStatus(){ 319 | 320 | <# 321 | .SYNOPSIS 322 | This function is used to get device configuration policy assignment from the Graph API REST interface 323 | .DESCRIPTION 324 | The function connects to the Graph API Interface and gets a device configuration policy assignment 325 | .EXAMPLE 326 | Get-DeviceConfigurationPolicyAssignment $id guid 327 | Returns any device configuration policy assignment configured in Intune 328 | .NOTES 329 | NAME: Get-DeviceConfigurationPolicyAssignment 330 | #> 331 | 332 | [cmdletbinding()] 333 | 334 | param 335 | ( 336 | [Parameter(Mandatory=$true,HelpMessage="Enter id (guid) for the Device Configuration Policy you want to check assignment")] 337 | $id 338 | ) 339 | 340 | $graphApiVersion = "Beta" 341 | $DCP_resource = "deviceManagement/intents" 342 | 343 | try { 344 | 345 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/deviceStates" 346 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 347 | 348 | } 349 | 350 | catch { 351 | 352 | $ex = $_.Exception 353 | $errorResponse = $ex.Response.GetResponseStream() 354 | $reader = New-Object System.IO.StreamReader($errorResponse) 355 | $reader.BaseStream.Position = 0 356 | $reader.DiscardBufferedData() 357 | $responseBody = $reader.ReadToEnd(); 358 | Write-Host "Response content:`n$responseBody" -f Red 359 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 360 | write-host 361 | break 362 | 363 | } 364 | 365 | } 366 | 367 | #################################################### 368 | 369 | Function Get-AADGroup(){ 370 | 371 | <# 372 | .SYNOPSIS 373 | This function is used to get AAD Groups from the Graph API REST interface 374 | .DESCRIPTION 375 | The function connects to the Graph API Interface and gets any Groups registered with AAD 376 | .EXAMPLE 377 | Get-AADGroup 378 | Returns all users registered with Azure AD 379 | .NOTES 380 | NAME: Get-AADGroup 381 | #> 382 | 383 | [cmdletbinding()] 384 | 385 | param 386 | ( 387 | $GroupName, 388 | $id, 389 | [switch]$Members 390 | ) 391 | 392 | # Defining Variables 393 | $graphApiVersion = "v1.0" 394 | $Group_resource = "groups" 395 | # pseudo-group identifiers for all users and all devices 396 | [string]$AllUsers = "acacacac-9df4-4c7d-9d50-4ef0226f57a9" 397 | [string]$AllDevices = "adadadad-808e-44e2-905a-0b7873a8a531" 398 | 399 | try { 400 | 401 | if($id){ 402 | 403 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" 404 | switch ( $id ) { 405 | $AllUsers { $grp = [PSCustomObject]@{ displayName = "All users"}; $grp } 406 | $AllDevices { $grp = [PSCustomObject]@{ displayName = "All devices"}; $grp } 407 | default { (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value } 408 | } 409 | 410 | } 411 | 412 | elseif($GroupName -eq "" -or $GroupName -eq $null){ 413 | 414 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" 415 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 416 | 417 | } 418 | 419 | else { 420 | 421 | if(!$Members){ 422 | 423 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 424 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 425 | 426 | } 427 | 428 | elseif($Members){ 429 | 430 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 431 | $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 432 | 433 | if($Group){ 434 | 435 | $GID = $Group.id 436 | 437 | $Group.displayName 438 | write-host 439 | 440 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" 441 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 442 | 443 | } 444 | 445 | } 446 | 447 | } 448 | 449 | } 450 | 451 | catch { 452 | 453 | $ex = $_.Exception 454 | $errorResponse = $ex.Response.GetResponseStream() 455 | $reader = New-Object System.IO.StreamReader($errorResponse) 456 | $reader.BaseStream.Position = 0 457 | $reader.DiscardBufferedData() 458 | $responseBody = $reader.ReadToEnd(); 459 | Write-Host "Response content:`n$responseBody" -f Red 460 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 461 | write-host 462 | break 463 | 464 | } 465 | 466 | } 467 | 468 | #################################################### 469 | 470 | #region Authentication 471 | 472 | # Checking if authToken exists before running authentication 473 | if($global:authToken){ 474 | 475 | # Setting DateTime to Universal time to work in all timezones 476 | $DateTime = (Get-Date).ToUniversalTime() 477 | 478 | # If the authToken exists checking when it expires 479 | $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes 480 | 481 | if($TokenExpires -le 0){ 482 | 483 | Write-Output ("Authentication Token expired" + $TokenExpires + "minutes ago") 484 | 485 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 486 | Get-ValidToken 487 | 488 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 489 | } 490 | } 491 | 492 | 493 | # Authentication doesn't exist, calling Get-AuthToken function 494 | 495 | else { 496 | 497 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 498 | Get-ValidToken 499 | 500 | # Getting the authorization token 501 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 502 | } 503 | 504 | #endregion 505 | 506 | 507 | #################################################### 508 | 509 | $DCPs = Get-DeviceConfigurationPolicy 510 | 511 | write-host 512 | 513 | $outputArray = @() 514 | 515 | foreach($DCP in $DCPs){ 516 | 517 | write-host "Device Configuration Policy:"$DCP.displayName -f Yellow 518 | write-host 519 | 520 | $id = $DCP.id 521 | 522 | $DCPA = Get-DeviceConfigurationPolicyAssignmentStatus -id $id 523 | write-host "Getting Configuration Policy assignment status..." -f Cyan 524 | 525 | 526 | if($DCPA){ 527 | 528 | if($DCPA.count -gt 1){ 529 | 530 | foreach($device in $DCPA){ 531 | 532 | $outputArray += New-Object PSObject -Property @{ 533 | 534 | PolicyName = $DCP.displayName 535 | DeviceName = $device.deviceDisplayName 536 | UserName = $device.userName 537 | Status = $device.status 538 | } 539 | } 540 | 541 | } 542 | 543 | else { 544 | 545 | $outputArray += New-Object PSObject -Property @{ 546 | 547 | PolicyName = $DCP.displayName 548 | DeviceName = $device.deviceDisplayName 549 | UserName = $device.userName 550 | Status = $device.status 551 | } 552 | 553 | } 554 | 555 | 556 | } 557 | 558 | else { 559 | 560 | Write-Host "No assignments found." 561 | 562 | } 563 | 564 | Write-Host 565 | 566 | } 567 | 568 | $ECPs = Get-EndpointConfigurationPolicy 569 | 570 | write-host 571 | 572 | foreach($ECP in $ECPs){ 573 | 574 | write-host "Endpoint Configuration Policy:"$ECP.displayName -f Yellow 575 | write-host 576 | #$DCP 577 | 578 | $id = $ECP.id 579 | 580 | $ECPA = Get-EndpointConfigurationPolicyAssignmentStatus -id $id 581 | write-host "Getting Configuration Policy assignment status..." -f Cyan 582 | 583 | 584 | if($ECPA){ 585 | 586 | if($ECPA.count -gt 1){ 587 | 588 | foreach($device in $ECPA){ 589 | 590 | $outputArray += New-Object PSObject -Property @{ 591 | 592 | PolicyName = $ECP.displayName 593 | DeviceName = $device.deviceDisplayName 594 | UserName = $device.userName 595 | Status = $device.state 596 | } 597 | } 598 | 599 | } 600 | 601 | else { 602 | 603 | $outputArray += New-Object PSObject -Property @{ 604 | 605 | PolicyName = $ECP.displayName 606 | DeviceName = $device.deviceDisplayName 607 | UserName = $device.userName 608 | Status = $device.state 609 | } 610 | 611 | } 612 | 613 | 614 | } 615 | 616 | else { 617 | 618 | Write-Host "No assignments found." 619 | 620 | } 621 | 622 | Write-Host 623 | 624 | } 625 | 626 | $outputArray | Export-Csv 'DeviceConfigurationStatus.csv' -NoTypeInformation -Force 627 | 628 | 629 | 630 | $connectionName = "AzureRunAsConnection" 631 | try 632 | { 633 | # Get the connection "AzureRunAsConnection " 634 | $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName 635 | 636 | "Logging in to Azure..." 637 | Add-AzureRmAccount ` 638 | -ServicePrincipal ` 639 | -TenantId $servicePrincipalConnection.TenantId ` 640 | -ApplicationId $servicePrincipalConnection.ApplicationId ` 641 | -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 642 | } 643 | catch { 644 | if (!$servicePrincipalConnection) 645 | { 646 | $ErrorMessage = "Connection $connectionName not found." 647 | throw $ErrorMessage 648 | } else{ 649 | Write-Error -Message $_.Exception 650 | throw $_.Exception 651 | } 652 | } 653 | 654 | Select-AzureRmSubscription -SubscriptionId $subscriptionID 655 | 656 | Set-AzureRmCurrentStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $resourceGroupName 657 | 658 | Set-AzureStorageBlobContent -Container $outputContainerName -File DeviceConfigurationStatus.csv -Blob DeviceConfigurationStatus.csv -Force 659 | 660 | #Add snapshot file with timestamp 661 | $date = Get-Date -format "dd-MMM-yyyy_HH:mm" 662 | $timeStampFileName = "DeviceConfigurationStatus_" + $date + ".csv" 663 | Set-AzureStorageBlobContent -Container $snapshotsContainerName -File DeviceConfigurationStatus.csv -Blob $timeStampFileName -Force -------------------------------------------------------------------------------- /Scripts/Malware.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .COPYRIGHT 4 | Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 5 | See LICENSE in the project root for license information. 6 | 7 | #> 8 | 9 | #################################################### 10 | 11 | # Variables 12 | $subscriptionID = Get-AutomationVariable 'subscriptionID' # Azure Subscription ID Variable 13 | $tenantID = Get-AutomationVariable 'tenantID' # Azure Tenant ID Variable 14 | $resourceGroupName = Get-AutomationVariable 'resourceGroupName' # Resource group name 15 | $storageAccountName = Get-AutomationVariable 'storageAccountName' # Storage account name 16 | 17 | # Report specific Variables 18 | $containerName = Get-AutomationVariable 'malwarereport' # Resource group name 19 | $containerSnapshotName = Get-AutomationVariable 'malwarereportsnapshots' # Storage account name 20 | 21 | # Graph App Registration Creds 22 | 23 | # Uses a Secret Credential named 'GraphApi' in your Automation Account 24 | $clientInfo = Get-AutomationPSCredential 'GraphApi' 25 | # Username of Automation Credential is the Graph App Registration client ID 26 | $clientID = $clientInfo.UserName 27 | # Password of Automation Credential is the Graph App Registration secret key (create one if needed) 28 | $secretPass = $clientInfo.GetNetworkCredential().Password 29 | 30 | #Required credentials - Get the client_id and client_secret from the app when creating it in Azure AD 31 | $client_id = $clientID #App ID 32 | $client_secret = $secretPass #API Access Key Password 33 | 34 | #################################################### 35 | 36 | function Get-AuthToken { 37 | 38 | <# 39 | .SYNOPSIS 40 | This function is used to authenticate with the Graph API REST interface 41 | .DESCRIPTION 42 | The function authenticate with the Graph API Interface with the tenant name 43 | .EXAMPLE 44 | Get-AuthToken 45 | Authenticates you with the Graph API interface 46 | .NOTES 47 | NAME: Get-AuthToken 48 | #> 49 | 50 | param 51 | ( 52 | [Parameter(Mandatory=$true)] 53 | $TenantID, 54 | [Parameter(Mandatory=$true)] 55 | $ClientID, 56 | [Parameter(Mandatory=$true)] 57 | $ClientSecret 58 | ) 59 | 60 | try{ 61 | # Define parameters for Microsoft Graph access token retrieval 62 | $resource = "https://graph.microsoft.com" 63 | $authority = "https://login.microsoftonline.com/$TenantID" 64 | $tokenEndpointUri = "$authority/oauth2/token" 65 | 66 | # Get the access token using grant type client_credentials for Application Permissions 67 | $content = "grant_type=client_credentials&client_id=$ClientID&client_secret=$ClientSecret&resource=$resource" 68 | $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing -Verbose:$false 69 | 70 | Write-Host "Got new Access Token!" -ForegroundColor Green 71 | Write-Host 72 | 73 | # If the accesstoken is valid then create the authentication header 74 | if($response.access_token){ 75 | 76 | # Creating header for Authorization token 77 | 78 | $authHeader = @{ 79 | 'Content-Type'='application/json' 80 | 'Authorization'="Bearer " + $response.access_token 81 | 'ExpiresOn'=$response.expires_on 82 | } 83 | 84 | return $authHeader 85 | 86 | } 87 | else{ 88 | Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." 89 | break 90 | } 91 | } 92 | catch{ 93 | FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" 94 | } 95 | 96 | } 97 | 98 | #################################################### 99 | 100 | Function Get-ValidToken { 101 | 102 | <# 103 | .SYNOPSIS 104 | This function is used to identify a possible existing Auth Token, and renew it using Get-AuthToken, if it's expired 105 | .DESCRIPTION 106 | Retreives any existing Auth Token in the session, and checks for expiration. If Expired, it will run the Get-AuthToken Fucntion to retreive a new valid Auth Token. 107 | .EXAMPLE 108 | Get-ValidToken 109 | Authenticates you with the Graph API interface by reusing a valid token if available - else a new one is requested using Get-AuthToken 110 | .NOTES 111 | NAME: Get-ValidToken 112 | #> 113 | 114 | #Fixing client_secret illegal char (+), which do't go well with web requests 115 | $client_secret = $($client_secret).Replace("+","%2B") 116 | 117 | # Checking if authToken exists before running authentication 118 | if($global:authToken){ 119 | 120 | # Get current time in (UTC) UNIX format (and ditch the milliseconds) 121 | $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] 122 | 123 | # If the authToken exists checking when it expires (converted to minutes for readability in output) 124 | $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) 125 | 126 | if($TokenExpires -le 0){ 127 | Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green 128 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 129 | } 130 | else{ 131 | Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green 132 | #Write-Host 133 | } 134 | } 135 | # Authentication doesn't exist, calling Get-AuthToken function 136 | else { 137 | # Getting the authorization token 138 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 139 | } 140 | } 141 | 142 | #################################################### 143 | 144 | Function Get-MalwareReport(){ 145 | 146 | <# 147 | .SYNOPSIS 148 | This function is used to get applications from the Graph API REST interface 149 | .DESCRIPTION 150 | The function connects to the Graph API Interface and gets any applications added 151 | .EXAMPLE 152 | Get-MalwareReport 153 | Returns any applications configured in Intune 154 | .NOTES 155 | NAME: Get-MalwareReport 156 | #> 157 | 158 | [cmdletbinding()] 159 | 160 | param 161 | ( 162 | [parameter(Mandatory=$false)] 163 | [ValidateNotNullOrEmpty()] 164 | [string]$type 165 | ) 166 | 167 | $graphApiVersion = "Beta" 168 | $Resource = "deviceManagement/reports/exportJobs" 169 | 170 | try { 171 | 172 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 173 | 174 | $JSON = @" 175 | { 176 | "reportName": "$type", 177 | "filter": "", 178 | "select": [ 179 | "DeviceName", 180 | "DeviceId", 181 | "MalwareId", 182 | "MalwareName", 183 | "Severity", 184 | "MalwareCategory", 185 | "ExecutionState", 186 | "State", 187 | "InitialDetectionDateTime", 188 | "LastStateChangeDateTime", 189 | "DetectionCount", 190 | "UPN", 191 | "UserName" 192 | ], 193 | "localization": "true", 194 | "ColumnName": "ui" 195 | } 196 | "@ 197 | 198 | Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json" 199 | 200 | 201 | } 202 | 203 | catch { 204 | 205 | $ex = $_.Exception 206 | Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red 207 | $errorResponse = $ex.Response.GetResponseStream() 208 | $reader = New-Object System.IO.StreamReader($errorResponse) 209 | $reader.BaseStream.Position = 0 210 | $reader.DiscardBufferedData() 211 | $responseBody = $reader.ReadToEnd(); 212 | Write-Host "Response content:`n$responseBody" -f Red 213 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 214 | write-host 215 | break 216 | 217 | } 218 | 219 | } 220 | 221 | #################################################### 222 | 223 | Function Get-ReportStatus(){ 224 | 225 | <# 226 | .SYNOPSIS 227 | This function is used to get applications from the Graph API REST interface 228 | .DESCRIPTION 229 | The function connects to the Graph API Interface and gets any applications added 230 | .EXAMPLE 231 | Get-IntuneApplication 232 | Returns any applications configured in Intune 233 | .NOTES 234 | NAME: Get-DefenderAgentsReportStatus 235 | #> 236 | 237 | [cmdletbinding()] 238 | 239 | param 240 | ( 241 | [Parameter(Mandatory=$true)] 242 | [string] $reportId 243 | ) 244 | 245 | $graphApiVersion = "Beta" 246 | $Resource = "deviceManagement/reports/exportJobs" 247 | 248 | try { 249 | 250 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)('$reportId')" 251 | 252 | Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 253 | 254 | 255 | } 256 | 257 | catch { 258 | 259 | $ex = $_.Exception 260 | Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red 261 | $errorResponse = $ex.Response.GetResponseStream() 262 | $reader = New-Object System.IO.StreamReader($errorResponse) 263 | $reader.BaseStream.Position = 0 264 | $reader.DiscardBufferedData() 265 | $responseBody = $reader.ReadToEnd(); 266 | Write-Host "Response content:`n$responseBody" -f Red 267 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 268 | write-host 269 | break 270 | 271 | } 272 | 273 | } 274 | 275 | #################################################### 276 | 277 | 278 | #region Authentication 279 | 280 | # Checking if authToken exists before running authentication 281 | if($global:authToken){ 282 | 283 | # Setting DateTime to Universal time to work in all timezones 284 | $DateTime = (Get-Date).ToUniversalTime() 285 | 286 | # If the authToken exists checking when it expires 287 | $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes 288 | 289 | if($TokenExpires -le 0){ 290 | 291 | Write-Output ("Authentication Token expired" + $TokenExpires + "minutes ago") 292 | 293 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 294 | Get-ValidToken 295 | 296 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 297 | } 298 | } 299 | 300 | # Authentication doesn't exist, calling Get-AuthToken function 301 | 302 | else { 303 | 304 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 305 | Get-ValidToken 306 | 307 | # Getting the authorization token 308 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 309 | } 310 | 311 | #endregion 312 | 313 | #################################################### 314 | 315 | $report = Get-MalwareReport -type "Malware" 316 | 317 | Write-Host "Running report..." -f Cyan 318 | 319 | $reportid = $report.id 320 | $reportstatus = $report.status 321 | 322 | #Write-Host $report 323 | 324 | 325 | while ($reportstatus -ine "completed") { 326 | $getreport = Get-ReportStatus -reportId $reportid 327 | 328 | $getreportstatus = $getreport.status 329 | 330 | Write-Host "In progress..." -f Yellow 331 | 332 | $reportstatus = $getreportstatus 333 | 334 | Start-Sleep -Seconds 5 335 | 336 | } 337 | 338 | Write-Host "Success!" -f Green 339 | 340 | $url = $getreport.url 341 | 342 | $outputfolder = ".\report.zip" 343 | 344 | # download zip folder 345 | Invoke-WebRequest -Uri $url -OutFile $outputfolder 346 | 347 | # unzip 348 | Expand-Archive -LiteralPath ".\report.zip" -DestinationPath ".\myreport" 349 | 350 | # Rename file.csv 351 | $csv = Get-ChildItem '.\myreport\*.csv' | Select-Object -ExpandProperty Name 352 | Rename-Item -Path ".\myreport\$csv" -NewName "malwarereport.csv" 353 | 354 | 355 | $connectionName = "AzureRunAsConnection" 356 | try 357 | { 358 | # Get the connection "AzureRunAsConnection " 359 | $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName 360 | 361 | "Logging in to Azure..." 362 | Add-AzureRmAccount ` 363 | -ServicePrincipal ` 364 | -TenantId $servicePrincipalConnection.TenantId ` 365 | -ApplicationId $servicePrincipalConnection.ApplicationId ` 366 | -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 367 | } 368 | catch { 369 | if (!$servicePrincipalConnection) 370 | { 371 | $ErrorMessage = "Connection $connectionName not found." 372 | throw $ErrorMessage 373 | } else{ 374 | Write-Error -Message $_.Exception 375 | throw $_.Exception 376 | } 377 | } 378 | 379 | Select-AzureRmSubscription -SubscriptionId $subscriptionID 380 | 381 | Set-AzureRmCurrentStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $resourceGroupName 382 | 383 | Set-AzureStorageBlobContent -Container $ContainerName -File .\myreport\malwarereport.csv -Blob MalwareReport.csv -Force 384 | 385 | #Add snapshot file with timestamp 386 | $date = Get-Date -format "dd-MMM-yyyy_HH:mm" 387 | $timeStampFileName = "malwarereport_" + $date + ".csv" 388 | Set-AzureStorageBlobContent -Container $containerSnapshotName -File '.\myreport\malwarereport.csv' -Blob $timeStampFileName -Force -------------------------------------------------------------------------------- /Scripts/ServicingRings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .COPYRIGHT 4 | Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 5 | See LICENSE in the project root for license information. 6 | 7 | #> 8 | 9 | #################################################### 10 | 11 | # Variables 12 | $subscriptionID = Get-AutomationVariable 'subscriptionID' # Azure Subscription ID Variable 13 | $tenantID = Get-AutomationVariable 'tenantID' # Azure Tenant ID Variable 14 | $resourceGroupName = Get-AutomationVariable 'resourceGroupName' # Resource group name 15 | $storageAccountName = Get-AutomationVariable 'storageAccountName' # Storage account name 16 | 17 | # Report specific Variables 18 | $outputContainerName = Get-AutomationVariable 'servicingring' # Resource group name 19 | $snapshotsContainerName = Get-AutomationVariable 'servicingringsnapshots' # Storage account name 20 | 21 | # Graph App Registration Creds 22 | 23 | # Uses a Secret Credential named 'GraphApi' in your Automation Account 24 | $clientInfo = Get-AutomationPSCredential 'GraphApi' 25 | # Username of Automation Credential is the Graph App Registration client ID 26 | $clientID = $clientInfo.UserName 27 | # Password of Automation Credential is the Graph App Registration secret key (create one if needed) 28 | $secretPass = $clientInfo.GetNetworkCredential().Password 29 | 30 | #Required credentials - Get the client_id and client_secret from the app when creating it in Azure AD 31 | $client_id = $clientID #App ID 32 | $client_secret = $secretPass #API Access Key Password 33 | 34 | #################################################### 35 | 36 | function Get-AuthToken { 37 | 38 | <# 39 | .SYNOPSIS 40 | This function is used to authenticate with the Graph API REST interface 41 | .DESCRIPTION 42 | The function authenticate with the Graph API Interface with the tenant name 43 | .EXAMPLE 44 | Get-AuthToken 45 | Authenticates you with the Graph API interface 46 | .NOTES 47 | NAME: Get-AuthToken 48 | #> 49 | 50 | param 51 | ( 52 | [Parameter(Mandatory=$true)] 53 | $TenantID, 54 | [Parameter(Mandatory=$true)] 55 | $ClientID, 56 | [Parameter(Mandatory=$true)] 57 | $ClientSecret 58 | ) 59 | 60 | try{ 61 | # Define parameters for Microsoft Graph access token retrieval 62 | $resource = "https://graph.microsoft.com" 63 | $authority = "https://login.microsoftonline.com/$TenantID" 64 | $tokenEndpointUri = "$authority/oauth2/token" 65 | 66 | # Get the access token using grant type client_credentials for Application Permissions 67 | $content = "grant_type=client_credentials&client_id=$ClientID&client_secret=$ClientSecret&resource=$resource" 68 | $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing -Verbose:$false 69 | 70 | Write-Host "Got new Access Token!" -ForegroundColor Green 71 | Write-Host 72 | 73 | # If the accesstoken is valid then create the authentication header 74 | if($response.access_token){ 75 | 76 | # Creating header for Authorization token 77 | 78 | $authHeader = @{ 79 | 'Content-Type'='application/json' 80 | 'Authorization'="Bearer " + $response.access_token 81 | 'ExpiresOn'=$response.expires_on 82 | } 83 | 84 | return $authHeader 85 | 86 | } 87 | else{ 88 | Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." 89 | break 90 | } 91 | } 92 | catch{ 93 | FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" 94 | } 95 | 96 | } 97 | 98 | #################################################### 99 | 100 | Function Get-ValidToken { 101 | 102 | <# 103 | .SYNOPSIS 104 | This function is used to identify a possible existing Auth Token, and renew it using Get-AuthToken, if it's expired 105 | .DESCRIPTION 106 | Retreives any existing Auth Token in the session, and checks for expiration. If Expired, it will run the Get-AuthToken Fucntion to retreive a new valid Auth Token. 107 | .EXAMPLE 108 | Get-ValidToken 109 | Authenticates you with the Graph API interface by reusing a valid token if available - else a new one is requested using Get-AuthToken 110 | .NOTES 111 | NAME: Get-ValidToken 112 | #> 113 | 114 | #Fixing client_secret illegal char (+), which do't go well with web requests 115 | $client_secret = $($client_secret).Replace("+","%2B") 116 | 117 | # Checking if authToken exists before running authentication 118 | if($global:authToken){ 119 | 120 | # Get current time in (UTC) UNIX format (and ditch the milliseconds) 121 | $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] 122 | 123 | # If the authToken exists checking when it expires (converted to minutes for readability in output) 124 | $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) 125 | 126 | if($TokenExpires -le 0){ 127 | Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green 128 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 129 | } 130 | else{ 131 | Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green 132 | #Write-Host 133 | } 134 | } 135 | # Authentication doesn't exist, calling Get-AuthToken function 136 | else { 137 | # Getting the authorization token 138 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 139 | } 140 | } 141 | 142 | #################################################### 143 | 144 | Function Get-AADUser(){ 145 | 146 | <# 147 | .SYNOPSIS 148 | This function is used to get AAD Users from the Graph API REST interface 149 | .DESCRIPTION 150 | The function connects to the Graph API Interface and gets any users registered with AAD 151 | .EXAMPLE 152 | Get-AADUser 153 | Returns all users registered with Azure AD 154 | .EXAMPLE 155 | Get-AADUser -userPrincipleName user@domain.com 156 | Returns specific user by UserPrincipalName registered with Azure AD 157 | .NOTES 158 | NAME: Get-AADUser 159 | #> 160 | 161 | [cmdletbinding()] 162 | 163 | param 164 | ( 165 | $userPrincipalName, 166 | $Property 167 | ) 168 | 169 | # Defining Variables 170 | $graphApiVersion = "beta" 171 | $User_resource = "users" 172 | 173 | try { 174 | 175 | if($userPrincipalName -eq "" -or $userPrincipalName -eq $null){ 176 | 177 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)" 178 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 179 | 180 | } 181 | 182 | else { 183 | 184 | if($Property -eq "" -or $Property -eq $null){ 185 | 186 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName" 187 | Write-Verbose $uri 188 | Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 189 | 190 | } 191 | 192 | else { 193 | 194 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($User_resource)/$userPrincipalName/$Property" 195 | Write-Verbose $uri 196 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 197 | 198 | } 199 | 200 | } 201 | 202 | } 203 | 204 | catch { 205 | 206 | return $null 207 | 208 | <# 209 | $ex = $_.Exception 210 | $errorResponse = $ex.Response.GetResponseStream() 211 | $reader = New-Object System.IO.StreamReader($errorResponse) 212 | $reader.BaseStream.Position = 0 213 | $reader.DiscardBufferedData() 214 | $responseBody = $reader.ReadToEnd(); 215 | Write-Output ("Response content:`n$responseBody") 216 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 217 | break 218 | #> 219 | 220 | } 221 | 222 | } 223 | 224 | #################################################### 225 | 226 | Function Get-AADGroup(){ 227 | 228 | <# 229 | .SYNOPSIS 230 | This function is used to get AAD Groups from the Graph API REST interface 231 | .DESCRIPTION 232 | The function connects to the Graph API Interface and gets any Groups registered with AAD 233 | .EXAMPLE 234 | Get-AADGroup 235 | Returns all users registered with Azure AD 236 | .NOTES 237 | NAME: Get-AADGroup 238 | #> 239 | 240 | [cmdletbinding()] 241 | 242 | param 243 | ( 244 | $GroupName, 245 | $id, 246 | [switch]$Members 247 | ) 248 | 249 | # Defining Variables 250 | $graphApiVersion = "beta" 251 | $Group_resource = "groups" 252 | 253 | # try { 254 | 255 | if($id -and !$Members){ 256 | 257 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'" 258 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 259 | 260 | } 261 | 262 | elseif(($GroupName -eq "" -or $GroupName -eq $null) -and !$Members){ 263 | 264 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)" 265 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 266 | 267 | } 268 | 269 | else { 270 | 271 | if(!$Members){ 272 | 273 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 274 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 275 | 276 | } 277 | 278 | elseif($Members){ 279 | 280 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'" 281 | $Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 282 | 283 | if($Group){ 284 | 285 | $GID = $Group.id 286 | 287 | $Group.displayName 288 | #write-host 289 | 290 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members" 291 | 292 | $groupResponse = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 293 | 294 | $groupMems = $groupResponse.Value 295 | 296 | $groupNextLink = $groupResponse."@odata.nextLink" 297 | 298 | while($groupNextLink -ne $null) { 299 | 300 | $groupResponse = Invoke-RestMethod -Uri $groupNextLink -Headers $authToken -Method Get 301 | $groupNextLink = $groupResponse."@odata.nextLink" 302 | $groupMems += $groupResponse.value 303 | 304 | } 305 | 306 | } 307 | return $groupMems 308 | 309 | } 310 | 311 | } 312 | <# 313 | } catch { 314 | 315 | $ex = $_.Exception 316 | $errorResponse = $ex.Response.GetResponseStream() 317 | $reader = New-Object System.IO.StreamReader($errorResponse) 318 | $reader.BaseStream.Position = 0 319 | $reader.DiscardBufferedData() 320 | $responseBody = $reader.ReadToEnd(); 321 | Write-Host "Response content:`n$responseBody" -f Red 322 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 323 | write-host 324 | break 325 | 326 | }#> 327 | 328 | } 329 | 330 | #################################################### 331 | 332 | function Get-Win10IntuneManagedDevice { 333 | 334 | <# 335 | .SYNOPSIS 336 | This gets information on Intune managed devices 337 | .DESCRIPTION 338 | This gets information on Intune managed devices 339 | .EXAMPLE 340 | Get-Win10IntuneManagedDevice 341 | .NOTES 342 | NAME: Get-Win10IntuneManagedDevice 343 | #> 344 | 345 | [cmdletbinding()] 346 | 347 | param 348 | ( 349 | [parameter(Mandatory=$false)] 350 | [ValidateNotNullOrEmpty()] 351 | [string]$deviceName 352 | ) 353 | 354 | $graphApiVersion = "beta" 355 | 356 | try { 357 | 358 | if($deviceName){ 359 | 360 | $Resource = "deviceManagement/managedDevices?`$filter=deviceName eq '$deviceName'" 361 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 362 | 363 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value 364 | 365 | } 366 | 367 | else { 368 | 369 | $Resource = "deviceManagement/managedDevices?`$filter=(((deviceType%20eq%20%27desktop%27)%20or%20(deviceType%20eq%20%27windowsRT%27)%20or%20(deviceType%20eq%20%27winEmbedded%27)%20or%20(deviceType%20eq%20%27surfaceHub%27)))" 370 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" 371 | 372 | $DevicesResponse = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 373 | 374 | $Devices = $DevicesResponse.value 375 | 376 | $DevicesNextLink = $DevicesResponse."@odata.nextLink" 377 | 378 | while ($DevicesNextLink -ne $null){ 379 | 380 | $DevicesResponse = Invoke-RestMethod -Uri $DevicesNextLink -Headers $authToken -Method Get 381 | $DevicesNextLink = $DevicesResponse."@odata.nextLink" 382 | $Devices += $DevicesResponse.value 383 | } 384 | 385 | return $Devices 386 | 387 | } 388 | 389 | } catch { 390 | $ex = $_.Exception 391 | $errorResponse = $ex.Response.GetResponseStream() 392 | $reader = New-Object System.IO.StreamReader($errorResponse) 393 | $reader.BaseStream.Position = 0 394 | $reader.DiscardBufferedData() 395 | $responseBody = $reader.ReadToEnd(); 396 | Write-Output ("Response content:`n$responseBody") 397 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 398 | throw "Get-IntuneManagedDevices error" 399 | } 400 | 401 | } 402 | 403 | #################################################### 404 | 405 | function Get-IntuneDevicePrimaryUser { 406 | 407 | <# 408 | .SYNOPSIS 409 | This lists the Intune device primary user 410 | .DESCRIPTION 411 | This lists the Intune device primary user 412 | .EXAMPLE 413 | Get-IntuneDevicePrimaryUser 414 | .NOTES 415 | NAME: Get-IntuneDevicePrimaryUser 416 | #> 417 | 418 | [cmdletbinding()] 419 | 420 | param 421 | ( 422 | [Parameter(Mandatory=$true)] 423 | [string] $deviceId 424 | ) 425 | $graphApiVersion = "beta" 426 | $Resource = "deviceManagement/managedDevices" 427 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)" + "/" + $deviceId + "/users" 428 | 429 | try { 430 | 431 | $primaryUser = Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get 432 | 433 | return $primaryUser.value."id" 434 | 435 | } catch { 436 | $ex = $_.Exception 437 | $errorResponse = $ex.Response.GetResponseStream() 438 | $reader = New-Object System.IO.StreamReader($errorResponse) 439 | $reader.BaseStream.Position = 0 440 | $reader.DiscardBufferedData() 441 | $responseBody = $reader.ReadToEnd(); 442 | Write-Output ("Response content:`n$responseBody") 443 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 444 | throw "Get-IntuneDevicePrimaryUser error" 445 | } 446 | } 447 | 448 | #################################################### 449 | 450 | Function Get-DeviceConfigurationPolicy(){ 451 | 452 | <# 453 | .SYNOPSIS 454 | This function is used to get device configuration policies from the Graph API REST interface 455 | .DESCRIPTION 456 | The function connects to the Graph API Interface and gets any device configuration policies 457 | .EXAMPLE 458 | Get-DeviceConfigurationPolicy 459 | Returns any device configuration policies configured in Intune 460 | .NOTES 461 | NAME: Get-DeviceConfigurationPolicy 462 | #> 463 | 464 | [cmdletbinding()] 465 | 466 | param 467 | ( 468 | $name, 469 | $type 470 | ) 471 | 472 | $graphApiVersion = "Beta" 473 | $DCP_resource = "deviceManagement/deviceConfigurations" 474 | 475 | try { 476 | 477 | if($Name){ 478 | 479 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 480 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'displayName').contains("$Name") } 481 | 482 | } 483 | elseif($type){ 484 | 485 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 486 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | Where-Object { ($_.'@odata.type').contains("$type") } 487 | 488 | } 489 | else { 490 | 491 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" 492 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 493 | 494 | } 495 | 496 | } 497 | 498 | catch { 499 | 500 | $ex = $_.Exception 501 | $errorResponse = $ex.Response.GetResponseStream() 502 | $reader = New-Object System.IO.StreamReader($errorResponse) 503 | $reader.BaseStream.Position = 0 504 | $reader.DiscardBufferedData() 505 | $responseBody = $reader.ReadToEnd(); 506 | Write-Host "Response content:`n$responseBody" -f Red 507 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 508 | write-host 509 | break 510 | 511 | } 512 | 513 | } 514 | 515 | #################################################### 516 | 517 | Function Get-DeviceConfigurationPolicyAssignment(){ 518 | 519 | <# 520 | .SYNOPSIS 521 | This function is used to get device configuration policy assignment from the Graph API REST interface 522 | .DESCRIPTION 523 | The function connects to the Graph API Interface and gets a device configuration policy assignment 524 | .EXAMPLE 525 | Get-DeviceConfigurationPolicyAssignment $id guid 526 | Returns any device configuration policy assignment configured in Intune 527 | .NOTES 528 | NAME: Get-DeviceConfigurationPolicyAssignment 529 | #> 530 | 531 | [cmdletbinding()] 532 | 533 | param 534 | ( 535 | [Parameter(Mandatory=$true,HelpMessage="Enter id (guid) for the Device Configuration Policy you want to check assignment")] 536 | $id 537 | ) 538 | 539 | $graphApiVersion = "Beta" 540 | $DCP_resource = "deviceManagement/deviceConfigurations" 541 | 542 | try { 543 | 544 | $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)/$id/groupAssignments" 545 | (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value 546 | 547 | } 548 | 549 | catch { 550 | 551 | $ex = $_.Exception 552 | $errorResponse = $ex.Response.GetResponseStream() 553 | $reader = New-Object System.IO.StreamReader($errorResponse) 554 | $reader.BaseStream.Position = 0 555 | $reader.DiscardBufferedData() 556 | $responseBody = $reader.ReadToEnd(); 557 | Write-Host "Response content:`n$responseBody" -f Red 558 | Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)" 559 | write-host 560 | break 561 | 562 | } 563 | 564 | } 565 | 566 | #################################################### 567 | 568 | #region Authentication 569 | 570 | # Checking if authToken exists before running authentication 571 | if($global:authToken){ 572 | 573 | # Setting DateTime to Universal time to work in all timezones 574 | $DateTime = (Get-Date).ToUniversalTime() 575 | 576 | # If the authToken exists checking when it expires 577 | $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes 578 | 579 | if($TokenExpires -le 0){ 580 | 581 | Write-Output ("Authentication Token expired" + $TokenExpires + "minutes ago") 582 | 583 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 584 | Get-ValidToken 585 | 586 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 587 | } 588 | } 589 | 590 | # Authentication doesn't exist, calling Get-AuthToken function 591 | 592 | else { 593 | 594 | #Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. 595 | Get-ValidToken 596 | 597 | # Getting the authorization token 598 | $global:authToken = Get-AuthToken -TenantID $tenantID -ClientID $client_id -ClientSecret $client_secret 599 | } 600 | 601 | #endregion 602 | 603 | #################################################### 604 | 605 | 606 | 607 | # Grab all servicing ring device config policies 608 | $DCPs = Get-DeviceConfigurationPolicy -type "windowsUpdateForBusiness" 609 | 610 | $outputArray = @() 611 | 612 | write-host 613 | write-host "Getting Windows 10 Update Ring policy assignments for servicing rings with names like: "" $ServicingRing """ -f Yellow 614 | write-host 615 | 616 | foreach($DCP in $DCPs){ 617 | 618 | write-host "Windows 10 Update Ring policy name:"$DCP.displayName -f Yellow 619 | 620 | $id = $DCP.id 621 | $DCPA = Get-DeviceConfigurationPolicyAssignment -id $id 622 | 623 | if($DCPA){ 624 | 625 | $excludedDevices = @() 626 | 627 | foreach($group in $DCPA){ 628 | 629 | if($group.excludeGroup){ 630 | $groupID = Get-AADGroup -id $group.targetGroupId 631 | $groupMembers = (Get-AADGroup -id $groupID.id -Members -GroupName $groupID.displayName).displayName 632 | $groupMembers = $groupMembers | Get-Unique 633 | 634 | Write-Host "Excluded group Name : " $groupID.displayName -f Cyan 635 | 636 | foreach($member in $groupMembers){ 637 | 638 | $excludedDevices += $member 639 | 640 | } 641 | 642 | } 643 | } 644 | 645 | Write-Host 646 | 647 | foreach($group2 in $DCPA) { 648 | 649 | if(!$group2.excludeGroup) { 650 | $groupID2 = Get-AADGroup -id $group2.targetGroupId 651 | $groupMembers2 = (Get-AADGroup -id $groupID2.id -Members -GroupName $groupID2.displayName).displayName 652 | $groupMembers2 = $groupMembers2 | Get-Unique 653 | 654 | Write-Host "Assigned group Name : " $groupID2.displayName -f Cyan 655 | Write-Host 656 | 657 | foreach($member in $groupMembers2){ 658 | 659 | if(!$excludedDevices.Contains($member)) { 660 | 661 | $device = Get-Win10IntuneManagedDevice -deviceName $member 662 | if($device -ne $null) { 663 | $primaryUser = Get-IntuneDevicePrimaryUser -deviceId $device.id 664 | try { 665 | $primaryUser = Get-IntuneDevicePrimaryUser -deviceId $device.id 666 | } catch { 667 | Write-Output "Error on: $member" 668 | Write-Output $device.id 669 | } 670 | $userName = Get-AADUser -userPrincipalName $primaryUser 671 | 672 | if($userName -ne $null) { 673 | 674 | if($username.Count -gt 1) { 675 | 676 | $outputArray += New-Object PSObject -Property @{ 677 | DeviceName = $member 678 | OSVersion = $device.osVersion 679 | Compliance = $device.complianceState 680 | LastSync = $device.lastSyncDateTime 681 | EnrollmentDate = $device.enrolledDateTime 682 | UserName = "null" 683 | UPN = "null" 684 | JobTitle = "null" 685 | Department = "null" 686 | Manufacturer = $device.manufacturer 687 | Model = $device.model 688 | GroupName = $groupID2.displayName 689 | ServicingRingName = $DCP.displayName 690 | RingExclusion = "False" 691 | JoinType = $device.joinType 692 | } 693 | 694 | } else { 695 | 696 | $outputArray += New-Object PSObject -Property @{ 697 | DeviceName = $member 698 | OSVersion = $device.osVersion 699 | Compliance = $device.complianceState 700 | LastSync = $device.lastSyncDateTime 701 | EnrollmentDate = $device.enrolledDateTime 702 | UserName = $userName.displayName 703 | UPN = $userName.userPrincipalName 704 | JobTitle = $userName.jobTitle 705 | Department = $userName.department 706 | Manufacturer = $device.manufacturer 707 | Model = $device.model 708 | GroupName = $groupID2.displayName 709 | ServicingRingName = $DCP.displayName 710 | RingExclusion = "False" 711 | JoinType = $device.joinType 712 | } 713 | } 714 | 715 | $output = $member + " : " + $userName.displayName 716 | #Write-Output $output 717 | } 718 | } 719 | } 720 | } 721 | } 722 | } 723 | } 724 | else { 725 | Write-Host "No assignments found." 726 | } 727 | 728 | 729 | 730 | } 731 | 732 | $multiple_output = $outputArray | Out-GridView -Title "Servicing Ring devices" 733 | 734 | $outputArray | Export-Csv 'ServicingOutput.csv' -NoTypeInformation -Force 735 | 736 | 737 | $connectionName = "AzureRunAsConnection" 738 | try 739 | { 740 | # Get the connection "AzureRunAsConnection " 741 | $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName 742 | 743 | "Logging in to Azure..." 744 | Add-AzureRmAccount ` 745 | -ServicePrincipal ` 746 | -TenantId $servicePrincipalConnection.TenantId ` 747 | -ApplicationId $servicePrincipalConnection.ApplicationId ` 748 | -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 749 | } 750 | catch { 751 | if (!$servicePrincipalConnection) 752 | { 753 | $ErrorMessage = "Connection $connectionName not found." 754 | throw $ErrorMessage 755 | } else{ 756 | Write-Error -Message $_.Exception 757 | throw $_.Exception 758 | } 759 | } 760 | 761 | Select-AzureRmSubscription -SubscriptionId $subscriptionID 762 | 763 | Set-AzureRmCurrentStorageAccount -StorageAccountName $storageAccountName -ResourceGroupName $resourceGroupName 764 | 765 | Set-AzureStorageBlobContent -Container $outputContainerName -File ServicingOutput.csv -Blob ServicingOutput.csv -Force 766 | 767 | #Add snapshot file with timestamp 768 | $date = Get-Date -format "dd-MMM-yyyy_HH:mm" 769 | $timeStampFileName = "ServicingOutput_" + $date + ".csv" 770 | Set-AzureStorageBlobContent -Container $snapshotsContainerName -File ServicingOutput.csv -Blob $timeStampFileName -Force 771 | 772 | -------------------------------------------------------------------------------- /Step-by-step Guide to Intune Automated Reporting with Graph, Automation, and PowerBI.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/Step-by-step Guide to Intune Automated Reporting with Graph, Automation, and PowerBI.pptx -------------------------------------------------------------------------------- /images/appsbydepartment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/images/appsbydepartment.png -------------------------------------------------------------------------------- /images/historicalservicingcomparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/images/historicalservicingcomparison.png -------------------------------------------------------------------------------- /images/operationalcompliance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/images/operationalcompliance.png -------------------------------------------------------------------------------- /images/servicingoverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phmehta94/IntuneAutomatedReporting/7d195be38e810ac5759e10e9a6307ce632c20d98/images/servicingoverview.png --------------------------------------------------------------------------------