├── Group-Documentation.ps1 ├── LICENSE ├── Mermaid-Mindmap-Example.md ├── README.md ├── SECURITY.md └── changelog.md /Group-Documentation.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | GCD - Group Centric Documentation will create a summary of assignments to entra groups. 4 | 5 | .DESCRIPTION 6 | ATTENTION: This is a v1.0 script. As such, bugs should be expected, please report them to me via GitHub or X (aka Twitter). 7 | 8 | This script was made, because Intune is missing a crucial point of view right now. Its impossible to figure out, what exactly is assigned to a group! 9 | For more details visit https://manima.de/2023/10/group-centric-documentation-for-intune-part-1 10 | 11 | .PARAMETER WorkingDirectory 12 | Provide a folder that will be used as work directory. This will also be the output folder for any files. 13 | 14 | .PARAMETER LogDirectory 15 | Provide a folder where log files will be put. 16 | ATTENTION: No error handling is currently implemented, so no logs to write just yet! 17 | 18 | .PARAMETER TenantAPIToUse 19 | This can either be '/v1.0' or '/beta' - by default the script will use beta, as some assignments are not yet available in v1.0. 20 | ATTENTION: You should never have to change this, because /v1.0 does not contain each request used. 21 | 22 | .PARAMETER CertificateThumbprint 23 | To Connect to Graph with a Certificate, please provide the thumbprint of the certificate used. 24 | 25 | .PARAMETER ClientID 26 | Please provide the client ID of the app registration or the Microsoft Graph powershell app registration will be used 27 | 28 | .PARAMETER TenantID 29 | Provide the tenant ID. 30 | 31 | .PARAMETER MultiFileResult 32 | Switch that lets you decide if the output should be one file or multiple files. 33 | ATTENTION: This will use the group name as filename. Thus, the filename needs to be sanitized and might miss crucial letters. 34 | 35 | .PARAMETER ConvertToMermaid 36 | Not implemented yet! 37 | 38 | .EXAMPLE 39 | Group-Documentation.ps1 -CertificateThumbprint 'afb945e0d88e2b1f70b8ffa501144f3a5cef8dee' -ClientID '22bcb6ed-c278-443a-aa17-01e9f478318d' -TenantID 'd73eed9c-6ef6-45dc-9813-0061b0b8730d' -MultiFileResult 40 | This would connect to graph using the provided parameters. The output would create multiple JSON files, one for each group, containing found assignments. 41 | 42 | .EXAMPLE 43 | Connect-MgGraph -Scopes DeviceManagementApps.Read.All,DeviceManagementConfiguration.Read.All,DeviceManagementServiceConfig.Read.All,Group.Read.All 44 | .\Group-Documentation.ps1 -WorkingDirectory C:\temp\ -LogDirectory C:\temp\logs -TenantAPIToUse "/v1.0" 45 | This would connect to the Graph API frist using the v1.0 interface and the minimum scopes required forthis script to run. 46 | This is useful, if you didn't set up your own application but instead rely on the Microsoft Graph Command Line Tools. 47 | 48 | .NOTES 49 | Version: 1.0 50 | Versionname: F.O.C.U.S. 51 | Intial creation date: 18.08.2023 52 | Last change date: 31.10.2023 53 | Latest changes: https://github.com/MHimken/GroupCentricDocumentation/blob/master/README.md 54 | 55 | Currently being worked on (aka TODO): 56 | * Add more information about groups 57 | * Membership count (https://graph.microsoft.com/beta/groups/5e8cb718-fb72-4ef0-9e0d-3e43d232780f/transitiveMembers/$count) 58 | * Creation Date 59 | * Make visualization happen using Mermaid 60 | * Improve error reporting 61 | * Handle Error in Line 222 (Get-MgGroup) to accomodate for the group not existing anymore (catch the error) 62 | ... more at my blog! 63 | #> 64 | [CmdletBinding(DefaultParameterSetName = 'SignInAuth')] 65 | param( 66 | 67 | [Parameter(Mandatory = $false)] 68 | [System.IO.DirectoryInfo]$WorkingDirectory = "$(Get-location)\GroupDocumentation\", 69 | 70 | [Parameter(Mandatory = $false)] 71 | [System.IO.DirectoryInfo]$LogDirectory = "$WorkingDirectory\Logs\", 72 | 73 | [Parameter(Mandatory = $false)] 74 | [ValidateSet('/beta', '/v1.0')] 75 | [String]$TenantAPIToUse = '/beta', 76 | 77 | [Parameter(Mandatory = $True, ParameterSetName = 'CertificateAuth')] 78 | [String]$CertificateThumbprint, 79 | 80 | [Parameter(Mandatory = $True, ParameterSetName = 'CertificateAuth')] 81 | [Parameter(Mandatory = $True, ParameterSetName = 'SignInAuthCustom')] 82 | [String]$ClientID, 83 | 84 | [Parameter(Mandatory = $True, ParameterSetName = 'CertificateAuth')] 85 | [Parameter(Mandatory = $False, ParameterSetName = 'SignInAuth')] 86 | [Parameter(Mandatory = $True, ParameterSetName = 'SignInAuthCustom')] 87 | [String]$TenantID, 88 | 89 | 90 | [Parameter(Mandatory = $true, ParameterSetName = 'AccessTokenAuth')] 91 | [Securestring]$AccessToken, 92 | 93 | [Parameter(Mandatory = $false) ] 94 | [switch]$MultiFileResult, 95 | 96 | [Parameter(Mandatory = $false)] 97 | [switch]$ConvertToMermaid 98 | ) 99 | Set-Variable RequiredGraphScopes -Scope "Script" -Option ReadOnly @( 100 | "DeviceManagementApps.Read.All", 101 | "DeviceManagementConfiguration.Read.All", 102 | "DeviceManagementServiceConfig.Read.All", 103 | "Group.Read.All" 104 | ) 105 | 106 | 107 | 108 | #Prepare folders and files 109 | $Script:TimeStampStart = Get-Date 110 | $Script:DateTime = Get-Date -Format ddMMyyyy_hhmmss 111 | if (-not(Test-Path $LogDirectory)) { New-Item $LogDirectory -ItemType Directory -Force | Out-Null } 112 | $LogPrefix = 'GCD_' 113 | $LogFile = Join-Path -Path $LogDirectory -ChildPath ('{0}_{1}.log' -f $LogPrefix, $DateTime) 114 | 115 | $Script:PathToScript = if ( $PSScriptRoot ) { 116 | # Console or VS Code debug/run button/F5 temp console 117 | $PSScriptRoot 118 | } else { 119 | if ( $psISE ) { Split-Path -Path $psISE.CurrentFile.FullPath } 120 | else { 121 | if ($profile -match 'VScode') { 122 | # VS Code "Run Code Selection" button/F8 in integrated console 123 | Split-Path $psEditor.GetEditorContext().CurrentFile.Path 124 | } else { 125 | Write-Output 'unknown directory to set path variable. exiting script.' 126 | exit 127 | } 128 | } 129 | } 130 | 131 | #Prepare arraylists - these are needed to multiple purposes 132 | $Script:ResultArray = [System.Collections.ArrayList]::new() 133 | $Script:GroupCache = [System.Collections.ArrayList]::new() 134 | $Script:Filters = [System.Collections.ArrayList]::new() 135 | $Script:StaticObjects = [System.Collections.ArrayList]::new() 136 | $Script:BatchRequests = [System.Collections.ArrayList]::new() 137 | $Script:BatchRequestsAnalyze = [System.Collections.ArrayList]::new() 138 | 139 | $CurrentLocation = Get-Location 140 | Set-Location $Script:PathToScript 141 | function Write-Log { 142 | <# 143 | .DESCRIPTION 144 | This is a modified version of Ryan Ephgrave's script 145 | .LINK 146 | https://www.ephingadmin.com/powershell-cmtrace-log-function/ 147 | #> 148 | Param ( 149 | [Parameter(Mandatory = $false)] 150 | $Message, 151 | $Component, 152 | # Type: 1 = Normal, 2 = Warning (yellow), 3 = Error (red) 153 | [ValidateSet('1', '2', '3')][int]$Type 154 | ) 155 | $Time = Get-Date -Format 'HH:mm:ss.ffffff' 156 | $Date = Get-Date -Format 'MM-dd-yyyy' 157 | if (-not($Component)) { $Component = 'Runner' } 158 | if (-not($Type)) { $Type = 1 } 159 | $LogMessage = "" 160 | $LogMessage | Out-File -Append -Encoding UTF8 -FilePath $LogFile 161 | if ($Verbose) { 162 | switch ($Type) { 163 | 1 { Write-Host $Message } 164 | 2 { Write-Warning $Message } 165 | 3 { Write-Error $Message } 166 | default { Write-Host $Message } 167 | } 168 | } 169 | } 170 | 171 | 172 | function Get-nextLinkData { 173 | param( 174 | $OriginalObject 175 | ) 176 | $nextLink = $OriginalObject.'@odata.nextLink' 177 | $Results = $OriginalObject 178 | while ($nextLink) { 179 | $Request = Invoke-MgGraphRequest -Uri $nextLink 180 | $Results.value += $Request.value 181 | $nextLink = '' 182 | $nextLink = $Request.'@odata.nextLink' 183 | } 184 | return $Results 185 | } 186 | function Add-BatchRequestObjectToQueue { 187 | param( 188 | [string]$Method, 189 | [string]$URL, 190 | $Headers, 191 | $Body 192 | ) 193 | $ID = $Script:BatchRequests.count 194 | $BatchObject = [PSCustomObject]@{ 195 | id = $ID 196 | method = $Method 197 | URL = $URL 198 | headers = $Headers 199 | body = $Body 200 | } 201 | $Script:BatchRequests.add($BatchObject) | Out-Null 202 | } 203 | function Invoke-BatchRequest { 204 | param( 205 | [string]$Method, 206 | [string]$URL, 207 | $Headers, 208 | $Body, 209 | [switch]$SendNow 210 | ) 211 | Add-BatchRequestObjectToQueue -Method $method -URL $URL -Headers $Headers -Body $Body 212 | if ($Script:BatchRequests.count -eq 20 -or $SendNow) { 213 | $BatchRequestBody = [PSCustomObject]@{requests = $Script:BatchRequests } 214 | $JSONRequests = $BatchRequestBody | ConvertTo-Json -Depth 10 215 | $Results = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com$TenantAPIToUse/`$batch" -Body $JSONRequests -ContentType 'application/json' -ErrorAction Stop 216 | $Script:BatchRequestsAnalyze = $Script:BatchRequests 217 | $Script:BatchRequests = [System.Collections.ArrayList]::new() 218 | } 219 | return $Results 220 | } 221 | function Add-NewFilterToCache { 222 | param( 223 | [string]$FilterID, 224 | [string]$FilterName 225 | ) 226 | $Script:Filters.add([PSCustomObject]@{ 227 | FilterID = $FilterID 228 | Filtername = $FilterName 229 | }) | Out-Null 230 | } 231 | function Add-NewGroupToResults { 232 | param( 233 | [string]$GroupID, 234 | [string]$GroupName 235 | ) 236 | $Script:ResultArray.add([PSCustomObject]@{ 237 | GroupID = $GroupID 238 | DisplayName = $GroupName 239 | Assignments = [System.Collections.ArrayList]@{} 240 | }) | Out-Null 241 | } 242 | function Register-GroupInResults { 243 | param ( 244 | [string]$GroupID, 245 | [string]$OdataType 246 | ) 247 | if ($OdataType) { 248 | switch ($OdataType) { 249 | "#microsoft.graph.allLicensedUsersAssignmentTarget" { $GroupName = 'All users'; $GroupID = 'acacacac-9df4-4c7d-9d50-4ef0226f57a9' } 250 | "#microsoft.graph.allDevicesAssignmentTarget" { $GroupName = 'All devices'; $GroupID = 'adadadad-808e-44e2-905a-0b7873a8a531' } 251 | } 252 | $GroupIDToReturn = $GroupID 253 | } 254 | if ($GroupID -notin $Script:ResultArray.GroupID) { 255 | if ($GroupID) { 256 | $GroupName = ($Script:GroupCache | Where-Object { $_.GroupID -eq $GroupID }).GroupName 257 | } 258 | if (-not($GroupName)) { 259 | $GroupName = (Get-MgGroup -GroupId ($GroupID)).DisplayName 260 | } 261 | Add-NewGroupToResults -GroupID $GroupID -GroupName $GroupName 262 | } 263 | return $GroupIDToReturn 264 | } 265 | function Find-FilterInCache { 266 | param ( 267 | [string]$FilterID 268 | ) 269 | $FilterName = ($Script:Filters | Where-Object { $_.FilterID -eq $FilterID }).FilterName 270 | if (-not($FilterName)) { 271 | $FilterName = (Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/assignmentFilters/$FilterID/?`$select=displayName,id").displayname 272 | Add-NewFilterToCache -FilterID $FilterID -FilterName $FilterName 273 | } 274 | return $FilterName 275 | } 276 | function Add-NewObjectToGroupInResults { 277 | param( 278 | $GroupID, 279 | $ObjectName, 280 | $ObjectType, 281 | $ObjectID, 282 | $AssignmentIntent, 283 | $GroupModeOData, 284 | $GroupMode, 285 | $FilterIntent, 286 | $FilterID, 287 | $OdataType 288 | ) 289 | if (-not($OdataType)) { 290 | Register-GroupInResults -GroupID $GroupID 291 | } else { 292 | $GroupID = Register-GroupInResults -OdataType $OdataType 293 | } 294 | if (-not($FilterID -eq '00000000-0000-0000-0000-000000000000')) { 295 | $FilterName = if ($FilterID) { Find-FilterInCache -FilterID $FilterID } 296 | } else { 297 | $FilterID = $null 298 | } 299 | $GroupMode = if ($GroupModeOData) { Get-GroupMode -GroupMode $GroupModeOData } 300 | ($Script:ResultArray | Where-Object { $_.GroupID -eq $GroupID }).Assignments.add([PSCustomObject]@{ 301 | ObjectName = $ObjectName 302 | ObjectType = $ObjectType 303 | ObjectID = $ObjectID 304 | AssignmentIntent = $AssignmentIntent 305 | GroupMode = $GroupMode 306 | FilterIntent = $FilterIntent 307 | FilterID = $FilterID 308 | FilterName = $FilterName 309 | } 310 | ) | Out-Null 311 | } 312 | function Initialize-Data { 313 | <# 314 | .NOTES 315 | * Adds static group with ID to the result array (All Users and All Devices) 316 | * Adds all groups with IDs to a local cache (this runs very quick even with thousands of groups it shouldn't be an issue) 317 | * Adds all currently available filters to filter cache (because there's a maximum of 200, this shouldn't impact perfomance) 318 | * Add static default deviceEnrollmentConfigurations to a cache 319 | #> 320 | Add-NewGroupToResults -GroupID 'acacacac-9df4-4c7d-9d50-4ef0226f57a9' -GroupName 'All users' 321 | Add-NewGroupToResults -GroupID 'adadadad-808e-44e2-905a-0b7873a8a531' -GroupName 'All devices' 322 | $groupsAll = Invoke-MgGraphRequest -Uri 'https://graph.microsoft.com/beta/groups/?$select=id,displayName,createdDateTime&$top=999' 323 | if ($groupsAll.'@odata.nextLink') { 324 | $groupsAll = Get-nextLinkData -OriginalObject $groupsAll 325 | } 326 | $groupsAll.value | ForEach-Object { $Script:GroupCache.add([PSCustomObject]@{ 327 | GroupID = $_.id 328 | GroupName = $_.displayName 329 | CreatedDateTime = $_.createdDateTime 330 | } 331 | ) | Out-Null 332 | } 333 | (Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/assignmentFilters/$FilterID/?`$select=displayName,id").value | ForEach-Object { 334 | Add-NewFilterToCache -FilterID $_.id -FilterName $_.displayName 335 | } 336 | $Script:StaticObjects.add([PSCustomObject]@{ 337 | Identifier = '#microsoft.graph.deviceEnrollmentLimitConfiguration' 338 | ObjectName = 'Default Enrollment Limit Configuration' 339 | GroupName = 'All devices' 340 | GroupID = 'adadadad-808e-44e2-905a-0b7873a8a531' 341 | }) | Out-Null 342 | $Script:StaticObjects.add([PSCustomObject]@{ 343 | Identifier = '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration' 344 | ObjectName = 'Default Platform Restriction Configuration' 345 | GroupName = 'All devices' 346 | GroupID = 'adadadad-808e-44e2-905a-0b7873a8a531' 347 | }) | Out-Null 348 | $Script:StaticObjects.add([PSCustomObject]@{ 349 | Identifier = '#microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration' 350 | ObjectName = 'Default Hello for Business Configuration' 351 | GroupName = 'All devices' 352 | GroupID = 'adadadad-808e-44e2-905a-0b7873a8a531' 353 | }) | Out-Null 354 | $Script:StaticObjects.add([PSCustomObject]@{ 355 | Identifier = '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' 356 | ObjectName = 'Default Enrollment Status Page' 357 | GroupName = 'All devices' 358 | GroupID = 'adadadad-808e-44e2-905a-0b7873a8a531' 359 | }) | Out-Null 360 | 361 | } 362 | function Convert-ObjectTypesMermaid { 363 | param( 364 | [string]$ObjectType 365 | ) 366 | switch ($ObjectType) { 367 | '#microsoft.graph.androidManagedAppProtection' { $Result = "Apps" } 368 | '#microsoft.graph.configurationPolicies' { $Result = "Configuration" } 369 | '#microsoft.graph.deviceCompliancePolicy' { $Result = "Compliance" } 370 | '#microsoft.graph.deviceEnrollmentLimitConfiguration' { $Result = "Other" } 371 | '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration' { $Result = "Other" } 372 | '#microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration' { $Result = "Other" } 373 | '#microsoft.graph.deviceHealthScript' { $Result = "Scripts" } 374 | '#microsoft.graph.deviceManagementScript' { $Result = "" } 375 | '#microsoft.graph.iosManagedAppProtection' { $Result = "" } 376 | '#microsoft.graph.iosMobileAppConfiguration' { $Result = "" } 377 | '#microsoft.graph.iosTrustedRootCertificate' { $Result = "" } 378 | '#microsoft.graph.iosVppApp' { $Result = "Apps" } 379 | '#microsoft.graph.macOSCompliancePolicy' { $Result = "" } 380 | '#microsoft.graph.macOSDeviceFeaturesConfiguration' { $Result = "" } 381 | '#microsoft.graph.macOSEndpointProtectionConfiguration' { $Result = "" } 382 | '#microsoft.graph.macOSGeneralDeviceConfiguration' { $Result = "" } 383 | '#microsoft.graph.macOSLobApp' { $Result = "" } 384 | '#microsoft.graph.macOSMicrosoftEdgeApp' { $Result = "" } 385 | '#microsoft.graph.macOSOfficeSuiteApp' { $Result = "" } 386 | '#microsoft.graph.macOSSoftwareUpdateConfiguration' { $Result = "" } 387 | '#microsoft.graph.macOsVppApp' { $Result = "" } 388 | '#microsoft.graph.mdmWindowsInformationProtectionPolicy' { $Result = "" } 389 | '#microsoft.graph.officeSuiteApp' { $Result = "Apps" } 390 | '#microsoft.graph.targetedManagedAppConfiguration' { $Result = "" } 391 | '#microsoft.graph.win32LobApp' { $Result = "Apps" } 392 | '#microsoft.graph.windows10CompliancePolicy' { $Result = "" } 393 | '#microsoft.graph.windows10CustomConfiguration' { $Result = "" } 394 | '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' { $Result = "" } 395 | '#microsoft.graph.windows10GeneralConfiguration' { $Result = "" } 396 | '#microsoft.graph.windows10PkcsCertificateProfile' { $Result = "" } 397 | '#microsoft.graph.windows81TrustedRootCertificate' { $Result = "" } 398 | '#microsoft.graph.windowsDomainJoinConfiguration' { $Result = "" } 399 | '#microsoft.graph.windowsDriverUpdateProfiles' { $Result = "" } 400 | '#microsoft.graph.windowsHealthMonitoringConfiguration' { $Result = "" } 401 | '#microsoft.graph.windowsIdentityProtectionConfiguration' { $Result = "" } 402 | '#microsoft.graph.windowsManagedAppProtection' { $Result = "Other" } 403 | '#microsoft.graph.windowsMicrosoftEdgeApp' { $Result = "Apps" } 404 | '#microsoft.graph.windowsUpdateForBusinessConfiguration' { $Result = "Configuration" } 405 | '#microsoft.graph.winGetApp' { $Result = "Apps" } 406 | default { $Result = $false } 407 | } 408 | return $Result 409 | } 410 | function Test-BaselineTemplate { 411 | <# 412 | .NOTES 413 | The Apps for Enterprise and Edge Baseline aren't actually a baselines, but are created as configurationPolicy and will therefore be caught 414 | somewhere else. 'Intents' are not just security baselines. They're basically everything that could be called "template". BitLocker and 415 | imported firewall rules are such an example. 416 | #> 417 | param( 418 | $templateID 419 | ) 420 | switch ($templateID) { 421 | "034ccd46-190c-4afc-adf1-ad7cc11262eb" { $Result = @{exists = $true; ObjectType = "SecurityBaseline" } } 422 | "4356d05c-a4ab-4a07-9ece-739f7c792910" { $Result = @{exists = $true; ObjectType = "ImportedFirewallRule" } } 423 | "d1174162-1dd2-4976-affc-6667049ab0ae" { $Result = @{exists = $true; ObjectType = "BitLockerOld" } } 424 | "2209e067-9c8c-462e-9981-5a8c79165dcc" { $Result = @{exists = $true; ObjectType = "DefenderForEndpoint" } } 425 | "cef15778-c3b9-4d53-a00a-042929f0aad0" { $Result = @{exists = $true; ObjectType = "W365Security" } } 426 | #"" { $Result = @{exists = $true; name = "BitLockerTemplateOld" } } 427 | default { $Result = @{exists = $false; ObjectType = "" } } 428 | } 429 | return $Result 430 | } 431 | function Get-GroupMode { 432 | <# 433 | .NOTES 434 | These are the currently known GroupMode Types. 435 | All Users and All Devices cannot be excluded and should not exist 436 | #> 437 | param( 438 | $GroupMode 439 | ) 440 | switch ($GroupMode) { 441 | "#microsoft.graph.groupAssignmentTarget" { return [string]"Included" } 442 | "#microsoft.graph.allLicensedUsersAssignmentTarget" { return [string]"Included" } 443 | "#microsoft.graph.allDevicesAssignmentTarget" { return [string]"Included" } 444 | "#microsoft.graph.exclusionGroupAssignmentTarget" { return [string]"Excluded" } 445 | default { return $false } 446 | } 447 | } 448 | #Get content functions 449 | function Get-AppRelations { 450 | <# 451 | .NOTES 452 | Apps can includes filters, but not necessarily 453 | #> 454 | $AllApps = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/mobileApps/?`$top=999&`$select=displayname,id&`$filter=isassigned eq true&`$expand=assignments(`$select=id,intent,target)" 455 | $AllApps = Get-nextLinkData -OriginalObject $AllApps 456 | #Add apps to ResultArray 457 | foreach ($App in $AllApps.value) { 458 | if (-not($App.assignments)) { 459 | #Write-Log! 460 | continue 461 | } 462 | foreach ($Assignment in $App.assignments) { 463 | $params = @{ 464 | GroupID = $Assignment.target.groupId 465 | ObjectName = $App.DisplayName 466 | ObjectType = $App.'@odata.type' 467 | ObjectID = $App.id 468 | AssignmentIntent = $Assignment.Intent 469 | GroupModeOData = $Assignment.target.'@odata.type' 470 | FilterIntent = $Assignment.target.deviceAndAppManagementAssignmentFilterType 471 | FilterID = $Assignment.target.deviceAndAppManagementAssignmentFilterId 472 | } 473 | if ($Assignment.target.groupId) { 474 | Add-NewObjectToGroupInResults @params 475 | } else { 476 | Add-NewObjectToGroupInResults @params -OdataType $Assignment.target.'@odata.type' 477 | } 478 | } 479 | } 480 | } 481 | function Get-DeviceEnrollmentConfigurationRelations { 482 | $DefaultAssignmentCounter = 0 483 | $AllDeviceEnrollmentConfiguration = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/deviceEnrollmentConfigurations/?`$select=displayname,id&`$top=999&`$expand=assignments" 484 | $AllDeviceEnrollmentConfiguration = Get-nextLinkData -OriginalObject $AllDeviceEnrollmentConfiguration 485 | foreach ($DeviceEnrollmentConfiguration in $AllDeviceEnrollmentConfiguration.value) { 486 | #TODO maybe parallelize this for speed 487 | #$DeviceEnrollmentAssignments = 488 | if (-not($DeviceEnrollmentConfiguration.assignments)) { 489 | #Write-Log! 490 | continue 491 | } 492 | if ($DeviceEnrollmentConfiguration.DisplayName -eq 'All users and all devices') { 493 | $ObjectName = ($Script:StaticObjects | Where-Object { $_.Identifier -eq $DeviceEnrollmentConfiguration.'@odata.type' }).ObjectName 494 | } else { 495 | $ObjectName = $DeviceEnrollmentConfiguration.DisplayName 496 | } 497 | foreach ($DeviceEnrollmentAssignment in $DeviceEnrollmentConfiguration.assignments) { 498 | $params = @{ 499 | GroupID = $DeviceEnrollmentAssignment.target.groupId 500 | ObjectName = $ObjectName 501 | ObjectType = $DeviceEnrollmentConfiguration.'@odata.type' 502 | ObjectID = $DeviceEnrollmentConfiguration.id 503 | GroupModeOData = $DeviceEnrollmentAssignment.target.'@odata.type'#'Included' #This is fixed and cannot be changed 504 | FilterIntent = $DeviceEnrollmentAssignment.target.deviceAndAppManagementAssignmentFilterType 505 | FilterID = $DeviceEnrollmentAssignment.target.deviceAndAppManagementAssignmentFilterId 506 | } 507 | if ($DeviceEnrollmentAssignment.target.groupId) { 508 | Add-NewObjectToGroupInResults @params 509 | } else { 510 | Add-NewObjectToGroupInResults @params -OdataType $DeviceEnrollmentAssignment.target.'@odata.type' 511 | } 512 | } 513 | 514 | } 515 | if ($DefaultAssignmentCounter -ge 5) { 516 | Write-Log -Message 'This component exceeded the expected four default DeviceEnrollmentConfigurations! Please contact the author of this script with this info!' -Component 'DeviceEnrollmentConfigurationRelations' -Type 2 517 | } 518 | <# Enrollment 519 | https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations 520 | Relevante Informationen 521 | id 522 | displayName 523 | Typ - siehe: 524 | #microsoft.graph.deviceEnrollmentLimitConfiguration 525 | #microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration 526 | #microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration 527 | #microsoft.graph.windows10EnrollmentCompletionPageConfiguration 528 | https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/0e0b9342-8bef-463c-a42b-ff781f8b56d0_Windows10EnrollmentCompletionPageConfiguration/assignments 529 | #microsoft.graph.deviceComanagementAuthorityConfiguration 530 | #> 531 | } 532 | function Get-DeviceConfigurationRelations { 533 | <# 534 | .NOTES 535 | We need to add something to handle filters, excludes 536 | filters should be part of the target property 537 | #> 538 | $AllDeviceConfiguration = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/deviceConfigurations/?`$select=displayname,id&`$top=999&`$expand=assignments" 539 | $AllDeviceConfiguration = Get-nextLinkData -OriginalObject $AllDeviceConfiguration 540 | foreach ($DeviceConfiguration in $AllDeviceConfiguration.value) { 541 | if (-not($DeviceConfiguration.assignments)) { 542 | #Write-Log! 543 | continue 544 | } 545 | foreach ($DeviceConfigurationAssignment in $DeviceConfiguration.assignments) { 546 | $params = @{ 547 | GroupID = $DeviceConfigurationAssignment.target.groupId 548 | ObjectName = $DeviceConfiguration.DisplayName 549 | ObjectType = $DeviceConfiguration.'@odata.type' 550 | ObjectID = $DeviceConfiguration.id 551 | AssignmentIntent = $DeviceConfigurationAssignment.Intent 552 | GroupModeOData = $DeviceConfigurationAssignment.target.'@odata.type' 553 | FilterIntent = $DeviceConfigurationAssignment.target.deviceAndAppManagementAssignmentFilterType 554 | FilterID = $DeviceConfigurationAssignment.target.deviceAndAppManagementAssignmentFilterId 555 | } 556 | if ($DeviceConfigurationAssignment.target.groupId) { 557 | Add-NewObjectToGroupInResults @params 558 | } else { 559 | Add-NewObjectToGroupInResults @params -OdataType $DeviceConfigurationAssignment.target.'@odata.type' 560 | } 561 | } 562 | } 563 | 564 | <# Device 565 | https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations/691c1591-52f2-45bd-aa02-c6f0714d02cf/assignments 566 | Relevante Informationen 567 | id 568 | displayName 569 | Typ - siehe: 570 | #microsoft.graph.windowsUpdateForBusinessConfiguration 571 | #microsoft.graph.iosTrustedRootCertificate 572 | #microsoft.graph.iosUpdateConfiguration 573 | #microsoft.graph.macOSDeviceFeaturesConfiguration 574 | #microsoft.graph.macOSGeneralDeviceConfiguration 575 | #microsoft.graph.macOSEndpointProtectionConfiguration 576 | #microsoft.graph.macOSSoftwareUpdateConfiguration 577 | #microsoft.graph.windowsDomainJoinConfiguration 578 | #microsoft.graph.windowsHealthMonitoringConfiguration 579 | #microsoft.graph.windows10CustomConfiguration 580 | #microsoft.graph.windows81TrustedRootCertificate 581 | #microsoft.graph.windowsIdentityProtectionConfiguration 582 | #microsoft.graph.windows10CustomConfiguration 583 | #microsoft.graph.windows10PkcsCertificateProfile 584 | #microsoft.graph.windows10GeneralConfiguration 585 | #microsoft.graph.windowsUpdateForBusinessConfiguration 586 | #microsoft.graph.windowsUpdateForBusinessConfiguration 587 | #microsoft.graph.windows10CustomConfiguration 588 | #microsoft.graph.windows10GeneralConfiguration 589 | #microsoft.graph.windows10EndpointProtectionConfiguration 590 | #microsoft.graph.windows81SCEPCertificateProfile 591 | #microsoft.graph.windows10VpnConfiguration 592 | #> 593 | } 594 | function Get-ScriptRelations { 595 | $AllScripts = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/deviceManagementScripts/?`$select=displayname,id&`$top=999&`$expand=assignments" 596 | $AllScripts = Get-nextLinkData -OriginalObject $AllScripts 597 | foreach ($Script in $AllScripts.value) { 598 | if (-not($Script.assignments)) { 599 | continue 600 | } 601 | foreach ($ScriptAssignment in $Script.assignments) { 602 | $params = @{ 603 | GroupID = $ScriptAssignment.target.groupId 604 | ObjectName = $Script.displayName 605 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-shared-devicemanagementscript-create?view=graph-rest-beta#request 606 | ObjectType = '#microsoft.graph.deviceManagementScript' 607 | ObjectID = $Script.id 608 | GroupModeOData = $ScriptAssignment.target.'@odata.type' 609 | FilterIntent = $ScriptAssignment.target.deviceAndAppManagementAssignmentFilterType 610 | FilterID = $ScriptAssignment.target.deviceAndAppManagementAssignmentFilterId 611 | } 612 | if ($ScriptAssignment.target.groupId) { 613 | Add-NewObjectToGroupInResults @params 614 | } else { 615 | Add-NewObjectToGroupInResults @params -OdataType $ScriptAssignment.target.'@odata.type' 616 | } 617 | } 618 | } 619 | #https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts 620 | #https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts/893a7230-e904-4bd1-a757-1d1db25a0eda/assignments 621 | } 622 | function Get-Remediations { 623 | $AllRemediations = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/deviceHealthScripts/?`$select=displayname,id&`$top=999&`$expand=assignments" 624 | foreach ($Remediation in $AllRemediations.value) { 625 | if (-not($Remediation.assignments)) { 626 | continue 627 | } 628 | foreach ($RemediationAssignment in $Remediation.assignments) { 629 | $params = @{ 630 | GroupID = $RemediationAssignment.target.groupId 631 | ObjectName = $Remediation.displayName 632 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/resources/intune-devices-devicehealthscript?view=graph-rest-beta 633 | ObjectType = '#microsoft.graph.deviceHealthScript' 634 | ObjectID = $Remediation.id 635 | GroupModeOData = $RemediationAssignment.target.'@odata.type' 636 | FilterIntent = $RemediationAssignment.target.deviceAndAppManagementAssignmentFilterType 637 | FilterID = $RemediationAssignment.target.deviceAndAppManagementAssignmentFilterId 638 | } 639 | if ($RemediationAssignment.target.groupId) { 640 | Add-NewObjectToGroupInResults @params 641 | } else { 642 | Add-NewObjectToGroupInResults @params -OdataType $RemediationAssignment.target.'@odata.type' 643 | } 644 | } 645 | } 646 | #https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts 647 | #https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts/02a4e7e8-195a-4824-8044-08b3a7f2d555/assignments 648 | } 649 | function Get-DeviceComplianceRelations { 650 | $AllDeviceCompliancePolicies = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/deviceCompliancePolicies/?`$select=displayname,id&`$top=999&`$expand=assignments" 651 | $AllDeviceCompliancePolicies = Get-nextLinkData -OriginalObject $AllDeviceCompliancePolicies 652 | foreach ($DeviceCompliancePolicy in $AllDeviceCompliancePolicies.value) { 653 | if (-not($DeviceCompliancePolicy.assignments)) { 654 | continue 655 | } 656 | foreach ($DeviceCompliancePolicyAssignment in $DeviceCompliancePolicy.assignments) { 657 | $params = @{ 658 | GroupID = $DeviceCompliancePolicyAssignment.target.groupId 659 | ObjectName = $DeviceCompliancePolicy.displayName 660 | ObjectType = $DeviceCompliancePolicy.'@odata.type' 661 | ObjectID = $DeviceCompliancePolicy.id 662 | GroupModeOData = $DeviceCompliancePolicyAssignment.target.'@odata.type' 663 | FilterIntent = $DeviceCompliancePolicyAssignment.target.deviceAndAppManagementAssignmentFilterType 664 | FilterID = $DeviceCompliancePolicyAssignment.target.deviceAndAppManagementAssignmentFilterId 665 | } 666 | if ($DeviceCompliancePolicyAssignment.target.groupId) { 667 | Add-NewObjectToGroupInResults @params 668 | } else { 669 | Add-NewObjectToGroupInResults @params -OdataType $DeviceCompliancePolicyAssignment.target.'@odata.type' 670 | } 671 | } 672 | } 673 | #https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies 674 | #https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies/b800eaad-28b7-466e-a261-58f0ae66a43c/assignments 675 | } 676 | function Get-ComplianceRelations { 677 | $AllCompliancePolicies = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/compliancePolicies/?`$select=name,id&`top=999&`$expand=assignments" 678 | $AllCompliancePolicies = Get-nextLinkData -OriginalObject $AllCompliancePolicies 679 | foreach ($CompliancePolicy in $AllCompliancePolicies.value) { 680 | if (-not($CompliancePolicy.assignments)) { 681 | continue 682 | } 683 | foreach ($CompliancePolicyAssignment in $CompliancePolicy.assignments) { 684 | $params = @{ 685 | GroupID = $CompliancePolicyAssignment.target.groupId 686 | ObjectName = $CompliancePolicy.name 687 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-deviceconfig-devicecompliancepolicy-list?view=graph-rest-1.0&viewFallbackFrom=graph-rest-beta&tabs=powershell 688 | ObjectType = '#microsoft.graph.deviceCompliancePolicy' 689 | ObjectID = $CompliancePolicy.id 690 | GroupModeOData = $CompliancePolicyAssignment.target.'@odata.type' 691 | FilterIntent = $CompliancePolicyAssignment.target.deviceAndAppManagementAssignmentFilterType 692 | FilterID = $CompliancePolicyAssignment.target.deviceAndAppManagementAssignmentFilterId 693 | } 694 | if ($CompliancePolicyAssignment.target.groupId) { 695 | Add-NewObjectToGroupInResults @params 696 | } else { 697 | Add-NewObjectToGroupInResults @params -OdataType $CompliancePolicyAssignment.target.'@odata.type' 698 | } 699 | } 700 | } 701 | #https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies 702 | #https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies/b800eaad-28b7-466e-a261-58f0ae66a43c/assignments 703 | } 704 | function Get-DriverUpdateRelations { 705 | <# 706 | .NOTES 707 | These are limited to 200 results. 708 | #> 709 | $AllDriverUpdatePolicies = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/windowsDriverUpdateProfiles/?`$select=displayname,id&`$top=200&`$expand=assignments" 710 | $AllDriverUpdatePolicies = Get-nextLinkData -OriginalObject $AllDriverUpdatePolicies 711 | foreach ($DriverUpdatePolicy in $AllDriverUpdatePolicies.value) { 712 | if (-not($DriverUpdatePolicy.assignments)) { 713 | continue 714 | } 715 | foreach ($DriverUpdatePolicyAssignment in $DriverUpdatePolicy.assignments) { 716 | $params = @{ 717 | GroupID = $DriverUpdatePolicyAssignment.target.groupId 718 | ObjectName = $DriverUpdatePolicy.displayName 719 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-softwareupdate-windowsdriverupdateprofile-list?view=graph-rest-beta 720 | ObjectType = '#microsoft.graph.windowsDriverUpdateProfiles' 721 | ObjectID = $DriverUpdatePolicy.id 722 | GroupModeOData = $DriverUpdatePolicyAssignment.target.'@odata.type' 723 | FilterIntent = $DriverUpdatePolicyAssignment.target.deviceAndAppManagementAssignmentFilterType 724 | FilterID = $DriverUpdatePolicyAssignment.target.deviceAndAppManagementAssignmentFilterId 725 | } 726 | if ($DriverUpdatePolicyAssignment.target.groupId) { 727 | Add-NewObjectToGroupInResults @params 728 | } else { 729 | Add-NewObjectToGroupInResults @params -OdataType $DriverUpdatePolicyAssignment.target.'@odata.type' 730 | } 731 | } 732 | } 733 | } 734 | function Get-FeatureUpdateRelations { 735 | $AllFeatureUpdatePolicies = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/windowsFeatureUpdateProfiles/?`$select=displayname,id&`$expand=assignments" 736 | $AllFeatureUpdatePolicies = Get-nextLinkData -OriginalObject $AllFeatureUpdatePolicies 737 | foreach ($FeatureUpdatePolicy in $AllFeatureUpdatePolicies.value) { 738 | if (-not($FeatureUpdatePolicy.assignments)) { 739 | continue 740 | } 741 | foreach ($FeatureUpdatePolicyAssignment in $FeatureUpdatePolicy.assignments) { 742 | $params = @{ 743 | GroupID = $FeatureUpdatePolicyAssignment.target.groupId 744 | ObjectName = $FeatureUpdatePolicy.displayName 745 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-deviceconfig-devicecompliancepolicy-list?view=graph-rest-1.0&viewFallbackFrom=graph-rest-beta&tabs=powershell 746 | ObjectType = '#microsoft.graph.deviceCompliancePolicy' 747 | ObjectID = $FeatureUpdatePolicy.id 748 | GroupModeOData = $FeatureUpdatePolicyAssignment.target.'@odata.type' 749 | FilterIntent = $FeatureUpdatePolicyAssignment.target.deviceAndAppManagementAssignmentFilterType 750 | FilterID = $FeatureUpdatePolicyAssignment.target.deviceAndAppManagementAssignmentFilterId 751 | } 752 | if ($FeatureUpdatePolicyAssignment.target.groupId) { 753 | Add-NewObjectToGroupInResults @params 754 | } else { 755 | Add-NewObjectToGroupInResults @params -OdataType $FeatureUpdatePolicyAssignment.target.'@odata.type' 756 | } 757 | } 758 | } 759 | } 760 | function Get-IntentRelations { 761 | <# 762 | .NOTES 763 | This is slow if there are many baselines, because we need to catch the assignments - once for each "intent" aka baseline 764 | This should also catch disk encryption policies that are old. 765 | #> 766 | $AllIntents = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/intents/?`$select=id,displayName,templateId&`$top=999&`$filter=isAssigned eq true" 767 | $AllIntents = Get-nextLinkData -OriginalObject $AllIntents 768 | for ($i = 0; $i -lt $AllIntents.value.count; $i++) { 769 | #This function is a check for known template types and will return the object types accordingly along with it 770 | if (-not($(Test-BaselineTemplate -templateID $AllIntents.value[$i].templateId).exists)) { 771 | continue 772 | } 773 | $params = @{ 774 | Method = "GET" 775 | URL = "/deviceManagement/intents/$($AllIntents.value[$i].id)/assignments" 776 | } 777 | if ($AllIntents.value.count - $i -gt 1) { 778 | $IntentAssignments = Invoke-BatchRequest @params 779 | } else { 780 | $IntentAssignments = Invoke-BatchRequest -SendNow @params 781 | } 782 | if ($IntentAssignments) { 783 | foreach ($IntentAssignment in $IntentAssignments.responses) { 784 | foreach ($Target in $IntentAssignment.body.value.target) { 785 | if (-not($Target.groupId)) { 786 | continue 787 | } 788 | $params = @{ 789 | GroupID = $Target.groupId 790 | ObjectName = $AllIntents.value[$IntentAssignment.id].displayname 791 | #Apparently there are no different ObjectTypes for intents 792 | ObjectType = (Test-BaselineTemplate -templateID ($AllIntents.value[$IntentAssignment.id]).templateId).ObjectType 793 | ObjectID = $AllIntents.value[$IntentAssignment.id].id 794 | GroupModeOData = $Target.'@odata.type' 795 | FilterIntent = $Target.deviceAndAppManagementAssignmentFilterType 796 | FilterID = $Target.deviceAndAppManagementAssignmentFilterId 797 | } 798 | if ($Target.groupId) { 799 | Add-NewObjectToGroupInResults @params 800 | } else { 801 | Add-NewObjectToGroupInResults @params -OdataType $Target.'@odata.type' 802 | } 803 | } 804 | } 805 | $Script:BatchRequestsAnalyze = [System.Collections.ArrayList]::new() 806 | } 807 | } 808 | #GET https://graph.microsoft.com/beta/deviceManagement/intents/?$expand=assignments 809 | } 810 | function Get-ConfigurationPoliciesRelations { 811 | $AllconfigurationPolicies = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceManagement/configurationPolicies?`$select=id,name&`$top=999&`$expand=assignments" 812 | $AllconfigurationPolicies = Get-nextLinkData -OriginalObject $AllconfigurationPolicies 813 | foreach ($configurationPolicy in $AllconfigurationPolicies.value) { 814 | if (-not($configurationPolicy.assignments)) { 815 | continue 816 | } 817 | foreach ($configurationPolicyAssignment in $configurationPolicy.assignments) { 818 | $params = @{ 819 | GroupID = $configurationPolicyAssignment.target.groupId 820 | ObjectName = $configurationPolicy.name 821 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-deviceconfig-devicecompliancepolicy-list?view=graph-rest-1.0&viewFallbackFrom=graph-rest-beta&tabs=powershell 822 | ObjectType = '#microsoft.graph.configurationPolicies' 823 | ObjectID = $configurationPolicy.id 824 | GroupModeOData = $configurationPolicyAssignment.target.'@odata.type' 825 | FilterIntent = $configurationPolicyAssignment.target.deviceAndAppManagementAssignmentFilterType 826 | FilterID = $configurationPolicyAssignment.target.deviceAndAppManagementAssignmentFilterId 827 | } 828 | if ($configurationPolicyAssignment.target.groupId) { 829 | Add-NewObjectToGroupInResults @params 830 | } else { 831 | Add-NewObjectToGroupInResults @params -OdataType $configurationPolicyAssignment.target.'@odata.type' 832 | } 833 | } 834 | } 835 | 836 | #https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$select=id,name&$top=999&$filter=templateReference/TemplateFamily eq 'endpointSecurityAntivirus'&$expand=assignments 837 | 838 | #https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$select=id,name&$filter=(technologies eq 'configManager' and creationSource eq 'SccmAV' or creationSource eq 'WindowsSecurity') 839 | #https://graph.microsoft.com/beta/deviceManagement/templates?$filter=templateType eq 'SecurityTemplate' 840 | 841 | #https://graph.microsoft.com/beta/deviceManagement/windowsMalwareOverview 842 | #https://graph.microsoft.com/beta/deviceManagement/configurationPolicyTemplates?$top=500&$filter=lifecycleState%20eq%20%27active%27 843 | } 844 | function Get-AppProtectionPolicyRelations { 845 | <# 846 | .NOTES 847 | This requires multiple requests, each for another OS - iosManagedAppProtections, androidManagedAppProtections, windowsInformationProtectionPolicies 848 | Additionally, there's targetedManagedAppConfigurations (this is done in Get-AppConfigurationPolicyRelations) and mdmWindowsInformationProtectionPolicies 849 | https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies - will give us all policies, but no assignments 850 | #> 851 | $AlliOSappProtections = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/iosManagedAppProtections/?`$select=displayname,id&`$expand=assignments" 852 | $AlliOSappProtections = Get-nextLinkData -OriginalObject $AlliOSappProtections 853 | foreach ($iOSAppProtection in $AlliOSappProtections.value) { 854 | if (-not($iOSAppProtection.assignments)) { 855 | continue 856 | } 857 | foreach ($iOSAppProtectionAssignment in $iOSAppProtection.assignments) { 858 | $params = @{ 859 | GroupID = $iOSAppProtectionAssignment.target.groupId 860 | ObjectName = $iOSAppProtection.displayName 861 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-mam-targetedmanagedapppolicyassignment-get?view=graph-rest-1.0&tabs=http 862 | ObjectType = '#microsoft.graph.iosManagedAppProtection' 863 | ObjectID = $iOSAppProtection.id 864 | GroupModeOData = $iOSAppProtectionAssignment.target.'@odata.type' 865 | FilterIntent = $iOSAppProtectionAssignment.target.deviceAndAppManagementAssignmentFilterType 866 | FilterID = $iOSAppProtectionAssignment.target.deviceAndAppManagementAssignmentFilterId 867 | } 868 | if ($iOSAppProtectionAssignment.target.groupId) { 869 | Add-NewObjectToGroupInResults @params 870 | } else { 871 | Add-NewObjectToGroupInResults @params -OdataType $iOSAppProtectionAssignment.target.'@odata.type' 872 | } 873 | } 874 | } 875 | $AllAndroidappProtections = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/androidManagedAppProtections/?`$select=displayname,id&`$expand=assignments" 876 | $AllAndroidappProtections = Get-nextLinkData -OriginalObject $AllAndroidappProtections 877 | foreach ($AndroidAppProtection in $AllAndroidappProtections.value) { 878 | if (-not($AndroidAppProtection.assignments)) { 879 | continue 880 | } 881 | foreach ($AndroidAppProtectionAssignment in $AndroidAppProtection.assignments) { 882 | $params = @{ 883 | GroupID = $AndroidAppProtectionAssignment.target.groupId 884 | ObjectName = $AndroidAppProtection.displayName 885 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-mam-targetedmanagedapppolicyassignment-get?view=graph-rest-1.0&tabs=http 886 | ObjectType = '#microsoft.graph.androidManagedAppProtection' 887 | ObjectID = $AndroidAppProtection.id 888 | GroupModeOData = $AndroidAppProtectionAssignment.target.'@odata.type' 889 | FilterIntent = $AndroidAppProtectionAssignment.target.deviceAndAppManagementAssignmentFilterType 890 | FilterID = $AndroidAppProtectionAssignment.target.deviceAndAppManagementAssignmentFilterId 891 | } 892 | if ($AndroidAppProtectionAssignment.target.groupId) { 893 | Add-NewObjectToGroupInResults @params 894 | } else { 895 | Add-NewObjectToGroupInResults @params -OdataType $AndroidAppProtectionAssignment.target.'@odata.type' 896 | } 897 | } 898 | } 899 | $windowsManagedAppProtections = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/windowsManagedAppProtections/?`$select=displayname,id&`$expand=assignments" 900 | $windowsManagedAppProtections = Get-nextLinkData -OriginalObject $windowsManagedAppProtections 901 | foreach ($windowsManagedAppProtection in $windowsManagedAppProtections.value) { 902 | if (-not($windowsManagedAppProtection.assignments)) { 903 | continue 904 | } 905 | foreach ($windowsManagedAppProtectionAssignment in $windowsManagedAppProtection.assignments) { 906 | $params = @{ 907 | GroupID = $windowsManagedAppProtectionAssignment.target.groupId 908 | ObjectName = $windowsManagedAppProtection.displayName 909 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-mam-targetedmanagedapppolicyassignment-get?view=graph-rest-1.0&tabs=http 910 | ObjectType = '#microsoft.graph.windowsManagedAppProtection' 911 | ObjectID = $windowsManagedAppProtection.id 912 | GroupModeOData = $windowsManagedAppProtectionAssignment.target.'@odata.type' 913 | FilterIntent = $windowsManagedAppProtectionAssignment.target.deviceAndAppManagementAssignmentFilterType 914 | FilterID = $windowsManagedAppProtectionAssignment.target.deviceAndAppManagementAssignmentFilterId 915 | } 916 | if ($windowsManagedAppProtectionAssignment.target.groupId) { 917 | Add-NewObjectToGroupInResults @params 918 | } else { 919 | Add-NewObjectToGroupInResults @params -OdataType $windowsManagedAppProtectionAssignment.target.'@odata.type' 920 | } 921 | } 922 | } 923 | $mdmwipProtectionPolicies = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/mdmWindowsInformationProtectionPolicies/?`$select=displayname,id&`$expand=assignments" 924 | $mdmwipProtectionPolicies = Get-nextLinkData -OriginalObject $mdmwipProtectionPolicies 925 | foreach ($mdmwipProtectionPolicy in $mdmwipProtectionPolicies.value) { 926 | if (-not($mdmwipProtectionPolicy.assignments)) { 927 | continue 928 | } 929 | foreach ($mdmwipProtectionPolicyAssignment in $mdmwipProtectionPolicy.assignments) { 930 | $params = @{ 931 | GroupID = $mdmwipProtectionPolicyAssignment.target.groupId 932 | ObjectName = $mdmwipProtectionPolicy.displayName 933 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/intune-mam-targetedmanagedapppolicyassignment-get?view=graph-rest-1.0&tabs=http 934 | ObjectType = '#microsoft.graph.mdmWindowsInformationProtectionPolicy' 935 | ObjectID = $mdmwipProtectionPolicy.id 936 | GroupModeOData = $mdmwipProtectionPolicyAssignment.target.'@odata.type' 937 | FilterIntent = $mdmwipProtectionPolicyAssignment.target.deviceAndAppManagementAssignmentFilterType 938 | FilterID = $mdmwipProtectionPolicyAssignment.target.deviceAndAppManagementAssignmentFilterId 939 | } 940 | if ($mdmwipProtectionPolicyAssignment.target.groupId) { 941 | Add-NewObjectToGroupInResults @params 942 | } else { 943 | Add-NewObjectToGroupInResults @params -OdataType $mdmwipProtectionPolicyAssignment.target.'@odata.type' 944 | } 945 | } 946 | } 947 | } 948 | function Get-AppConfigurationPolicyRelations { 949 | $AllAppConfigurationDeviceConfigurations = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/mobileAppConfigurations/?`$select=displayname,id&`$expand=assignments" 950 | $AllAppConfigurationDeviceConfigurations = Get-nextLinkData -OriginalObject $AllAppConfigurationDeviceConfigurations 951 | foreach ($AppConfigurationDeviceConfiguration in $AllAppConfigurationDeviceConfigurations.value) { 952 | if (-not($AppConfigurationDeviceConfiguration.assignments)) { 953 | continue 954 | } 955 | foreach ($AppConfigurationDeviceConfigurationAssignment in $AppConfigurationDeviceConfiguration.assignments) { 956 | $params = @{ 957 | GroupID = $AppConfigurationDeviceConfigurationAssignment.target.groupId 958 | ObjectName = $AppConfigurationDeviceConfiguration.displayName 959 | ObjectType = $AppConfigurationDeviceConfiguration.'@odata.type' 960 | ObjectID = $AppConfigurationDeviceConfiguration.id 961 | GroupModeOData = $AppConfigurationDeviceConfigurationAssignment.target.'@odata.type' 962 | FilterIntent = $AppConfigurationDeviceConfigurationAssignment.target.deviceAndAppManagementAssignmentFilterType 963 | FilterID = $AppConfigurationDeviceConfigurationAssignment.target.deviceAndAppManagementAssignmentFilterId 964 | } 965 | if ($AppConfigurationDeviceConfigurationAssignment.target.groupId) { 966 | Add-NewObjectToGroupInResults @params 967 | } else { 968 | Add-NewObjectToGroupInResults @params -OdataType $AppConfigurationDeviceConfigurationAssignment.target.'@odata.type' 969 | } 970 | } 971 | } 972 | $AllAppConfigurationConfigurations = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com$TenantAPIToUse/deviceAppManagement/targetedManagedAppConfigurations/?`$select=displayname,id&`$expand=assignments" 973 | $AllAppConfigurationConfigurations = Get-nextLinkData -OriginalObject $AllAppConfigurationConfigurations 974 | foreach ($AllAppConfigurationConfiguration in $AllAppConfigurationConfigurations.value) { 975 | if (-not($AllAppConfigurationConfiguration.assignments)) { 976 | continue 977 | } 978 | foreach ($AllAppConfigurationConfigurationAssignment in $AllAppConfigurationConfiguration.assignments) { 979 | $params = @{ 980 | GroupID = $AllAppConfigurationConfigurationAssignment.target.groupId 981 | ObjectName = $AllAppConfigurationConfiguration.displayName 982 | #No @odata.type is returned - but it should. See https://learn.microsoft.com/en-us/graph/api/resources/intune-mam-targetedmanagedappconfiguration?view=graph-rest-1.0 983 | ObjectType = '#microsoft.graph.targetedManagedAppConfiguration' 984 | ObjectID = $AllAppConfigurationConfiguration.id 985 | GroupModeOData = $AllAppConfigurationConfigurationAssignment.target.'@odata.type' 986 | FilterIntent = $AllAppConfigurationConfigurationAssignment.target.deviceAndAppManagementAssignmentFilterType 987 | FilterID = $AllAppConfigurationConfigurationAssignment.target.deviceAndAppManagementAssignmentFilterId 988 | } 989 | if ($AllAppConfigurationConfigurationAssignment.target.groupId) { 990 | Add-NewObjectToGroupInResults @params 991 | } else { 992 | Add-NewObjectToGroupInResults @params -OdataType $AllAppConfigurationConfigurationAssignment.target.'@odata.type' 993 | } 994 | } 995 | } 996 | #https://graph.microsoft.com/beta/deviceAppManagement/mobileAppConfigurations 997 | #https://graph.microsoft.com/beta/deviceAppManagement/targetedManagedAppConfigurations 998 | } 999 | function Get-eSIMCellularProfileRelations { 1000 | <# 1001 | .NOTES 1002 | This is a stub, because I don't have an eSim to create such a profile - therefore don't know how such an object would look 1003 | #> 1004 | #GET https://graph.microsoft.com/beta/deviceManagement/embeddedSIMActivationCodePools?$select=id,displayName,activationCodeCount&$expand=assignments 1005 | return 1006 | } 1007 | function Get-iOSAppProvisioningProfilesRelations { 1008 | <# 1009 | .NOTES 1010 | This is a stub, because I don't have a LoB iOS app to test this with 1011 | #> 1012 | #https://graph.microsoft.com/beta/deviceAppManagement/iosLobAppProvisioningConfigurations 1013 | } 1014 | function Get-SModePoliciesRelations { 1015 | <# 1016 | .NOTES 1017 | This is a stub, because I don't have a Windows device that has S-Mode enabled 1018 | #> 1019 | #https://graph.microsoft.com/beta/deviceAppManagement/wdacSupplementalPolicies 1020 | } 1021 | function Get-ManagedEBookRelations { 1022 | <# 1023 | .NOTES 1024 | This is a stub, because I don't have ABM set up to deploy an EBook 1025 | #> 1026 | #https://graph.microsoft.com/beta/deviceAppManagement/managedEBooks 1027 | } 1028 | # End of content gathering functions 1029 | function Get-ConfigurationRelations { 1030 | <# 1031 | .NOTES 1032 | Wrap up function. This function wraps up related functions into one call, that could be used elsewhere 1033 | #> 1034 | Get-DeviceEnrollmentConfigurationRelations 1035 | Get-DeviceConfigurationRelations 1036 | Get-ConfigurationPoliciesRelations 1037 | Get-IntentRelations 1038 | } 1039 | function Start-GatherInformation { 1040 | Get-AppRelations 1041 | Get-ConfigurationRelations 1042 | Get-ScriptRelations 1043 | Get-Remediations 1044 | Get-DeviceComplianceRelations 1045 | Get-ComplianceRelations 1046 | Get-DriverUpdateRelations 1047 | Get-FeatureUpdateRelations 1048 | Get-AppProtectionPolicyRelations 1049 | Get-AppConfigurationPolicyRelations 1050 | } 1051 | 1052 | #Start Coding! 1053 | try { 1054 | $ErrorActionPreference = 'stop' 1055 | $MgContext = Get-MgContext 1056 | if ($null -eq $($MgContext)) { 1057 | 1058 | switch -Exact ($PSCmdlet.ParameterSetName) { 1059 | 'SignInAuth' { 1060 | $Splat = @{} 1061 | if (-not([string]::IsNullOrEmpty($TenantID))) { 1062 | $Splat['TenantId'] = $TenantID 1063 | } 1064 | $Splat['Scopes'] = $RequiredGraphScopes 1065 | } 1066 | 'SignInAuthCustom' { 1067 | $Splat = @{} 1068 | $Splat['TenantId'] = $TenantID 1069 | $Splat['ClientId'] = $ClientID 1070 | } 1071 | 'CertificateAuth' { 1072 | $Splat = @{} 1073 | $Splat['TenantId'] = $TenantID 1074 | $Splat['ClientId'] = $ClientID 1075 | $Splat['CertificateThumbPrint'] = $CertificateThumbprint 1076 | 1077 | } 1078 | 1079 | 'AccessTokenAuth' { 1080 | $Splat = @{} 1081 | $Splat['AccessToken'] = $AccessToken 1082 | 1083 | } 1084 | 1085 | } 1086 | Connect-MgGraph @Splat -NoWelcome 1087 | $MgContext = Get-MgContext 1088 | $Splat = $Null 1089 | if ($Null -ne ($RequiredGraphScopes | Where-Object { $_ -notin $MgContext.Scopes })) { 1090 | throw [System.Security.Authentication.AuthenticationException]::New('The required Microsoft Graph scopes are not present in the authentication context. Please use Disconnect-MgGraph and try again') 1091 | } 1092 | } else { 1093 | if ($Null -ne ($RequiredGraphScopes | Where-Object { $_ -notin $MgContext.Scopes })) { 1094 | throw [System.Security.Authentication.AuthenticationException]::New('The required Microsoft Graph scopes are not present in the authentication context. Please use Disconnect-MgGraph and try again') 1095 | } 1096 | } 1097 | #Prepare some data that is expected in every environment 1098 | Initialize-Data 1099 | #Getting Information 1100 | $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() 1101 | Start-GatherInformation 1102 | $Stopwatch.Stop() 1103 | $Stopwatch.Elapsed 1104 | #Export data to Json 1105 | if ($MultiFileResult) { 1106 | foreach ($Group in $Script:ResultArray) { 1107 | #Sanitize Group name 1108 | $GroupName = $Group.DisplayName 1109 | $GroupNameSanitized = $GroupName -replace "[$([RegEx]::Escape([string][IO.Path]::GetInvalidFileNameChars()))]+", "_" 1110 | $TargetPath = "$WorkingDirectory$GroupNameSanitized.json" 1111 | $Group | ConvertTo-Json -Depth 5 | Out-File -LiteralPath $TargetPath -Force 1112 | } 1113 | } else { 1114 | $TargetPath = "$WorkingDirectory`GCD_AllGroups.json" 1115 | $Script:ResultArray | ConvertTo-Json -Depth 5 | Out-File -LiteralPath $TargetPath -Force 1116 | } 1117 | 1118 | 1119 | if ($ConvertToMermaid) { 1120 | <#Mindmap Template Mermaid 1121 | mindmap 1122 | root((mindmap)) 1123 | Origins 1124 | Long history 1125 | ::icon(fa fa-book) 1126 | Popularisation 1127 | British popular psychology author Tony Buzan 1128 | Research 1129 | On effectiveness
and features 1130 | On Automatic creation 1131 | Uses 1132 | Creative techniques 1133 | Strategic planning 1134 | Argument mapping 1135 | Tools 1136 | Pen and paper 1137 | Mermaid 1138 | #> 1139 | foreach ($Group in $Script:ResultArray) { 1140 | $ObjectTypes = Convert-ObjectTypesMermaid 1141 | $Diskpart = @" 1142 | mindmap 1143 | root(($($Group.DisplayName))) 1144 | Origins 1145 | Long history 1146 | ::icon(fa fa-book) 1147 | Popularisation 1148 | British popular psychology author Tony Buzan 1149 | Research 1150 | On effectiveness
and features 1151 | On Automatic creation 1152 | Uses 1153 | Creative techniques 1154 | Strategic planning 1155 | Argument mapping 1156 | Tools 1157 | Pen and paper 1158 | Mermaid 1159 | "@ 1160 | } 1161 | } 1162 | 1163 | } catch { 1164 | Write-Log -Message $_ -Component 'GFDCore' -Type 3 1165 | } finally { 1166 | Remove-Variable RequiredGraphScopes -Force # This shouldn't actually be needed but it makes debugging less painful 1167 | Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null 1168 | Set-Location $CurrentLocation 1169 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Martin Himken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mermaid-Mindmap-Example.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | mindmap 3 | root((GroupName)) 4 | Apps 5 | Install Lockscreen Background; Assignment: Install 6 | zScaler Client Connector; Assignment: Install 7 | Filtered: Yes Intent: Exclude 8 | Solitaire; Assignment: Uninstall 9 | Settings Catalog 10 | Set Lockscreen Background 11 | Skip user ESP 12 | Templates 13 | WiFi Profile 14 | SCEP Profile 15 | VPN Profile 16 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Group Centric Documentation 2 | 3 | In this 'small' project, I was fed up with the fact that there's no easy way to visualize what's assigned to a specific group. The main goal here is to make mistakes, that are often made with assignments, more visible. Planning and monitoring are more manageable on a human readable level. It also helps to document current assignments for later comparison. This does not import/export any data, it's a visualization tool. 4 | 5 | This is the very first version of this solution and will be expanded over time. More information can be found here 6 | 7 | ## Output formats 8 | 9 | * JSON 10 | * In development: Mermaid (Try out now by using [Mermaid](https://mermaid.live/) and the provided [Example File](Mermaid-Mindmap-Example.md)) 11 | * Eventually: Powerpoint 12 | 13 | ## Setup 14 | 15 | You need a way to connect to the Graph API. I used a custom enterprise application with a self-signed certificate to connect, so parameters for that are available if required. I also involved the [Microsoft.Graph module](https://learn.microsoft.com/en-us/powershell/microsoftgraph/installation?view=graph-powershell-1.0). The minimum scope for this script to work are: 16 | 17 | * DeviceManagementApps.Read.All 18 | * DeviceManagementConfiguration.Read.All 19 | * DeviceManagementServiceConfig.Read.All 20 | * Group.Read.All 21 | 22 | It is possible to connect to Graph first using `Connect-MgGraph -Scopes DeviceManagementApps.Read.All,DeviceManagementConfiguration.Read.All,DeviceManagementServiceConfig.Read.All,Group.Read.All`. 23 | 24 | ## Considerations 25 | 26 | * This script was written to be as fast as possible. In a medium sized environment, it's not uncommon for it to run in 30-60 seconds. Larger environments should expect longer. 27 | * This script currently lacks any error handling. This includes the famous `429 Too Many Requests` error. General Graph throttling limits can be [found here](https://learn.microsoft.com/en-us/graph/throttling-limits). However, even with extensive testing, I haven't been able to hit any of these limits (especially using only `GET` this seems unlikely unless you have a _lot_ of groups). 28 | * I strongly recommend that you build your own application using certificates instead of secrets. **Never use secrets in your scripts!** 29 | 30 | ## Closing words 31 | 32 | This project has taken me more time than anything I've ever written (without a customer paying for it). I write these solutions in my spare time because I enjoy it! If you find any kind of problem, bug or have a feature request, please don't hesitate to contact me. I'd appreciate bug reports on GitHub, contact me using the methods provided on my blog (see the top of this readme), or via the WinAdmins Discord . 33 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you find a security issue in my script, please report it to me directly. You may use the following to contact me: 12 | 13 | - DMs on 14 | - 15 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 2024 4 | 5 | * Suggestion from Cody: Show content that are not assigned 6 | * Suggestion from Cody: Members in a group (using a switch, but its slow) 7 | * Suggestion from Cody: Add a note field for "intents" because it has weird effects on the GUI 8 | * Create a GUI 9 | 10 | ## October 2023 11 | 12 | * Initial Release 13 | --------------------------------------------------------------------------------