From 3feeab415415c0b5ae680c80fded4d6ba8ca0eb9 Mon Sep 17 00:00:00 2001 From: Merill Fernando Date: Wed, 31 Jan 2024 00:54:34 +1100 Subject: [PATCH] Added Export-MsIdAppConsentGraphReport --- assets/aadconsentgrantpermissiontable.csv | 42 ++ report5.xlsx | Bin 0 -> 46678 bytes src/Export-MsIdAppConsentGrantReport.ps1 | 462 ++++++++++++++++++++++ src/MSIdentityTools.psd1 | 2 + 4 files changed, 506 insertions(+) create mode 100644 assets/aadconsentgrantpermissiontable.csv create mode 100644 report5.xlsx create mode 100644 src/Export-MsIdAppConsentGrantReport.ps1 diff --git a/assets/aadconsentgrantpermissiontable.csv b/assets/aadconsentgrantpermissiontable.csv new file mode 100644 index 0000000..6952d95 --- /dev/null +++ b/assets/aadconsentgrantpermissiontable.csv @@ -0,0 +1,42 @@ +Type,Permission,Privilege,Reason +Delegated,Mail.ReadBasic,Medium,DataExfiltration +Delegated,Mail,High,Phishing +Delegated,Contacts,High,Phishing +Delegated,MailboxSettings,High,Phishing +Delegated,People,High,Phishing +Delegated,Files,High,Phishing +Delegated,Notes,High,Phishing +Delegated,Directory.AccessAsUser.All,High,Phishing +Delegated,user_impersonation,High,Phishing +Delegated,Application.ReadWrite.All,High,BroadImpact +Delegated,Directory.ReadWrite.All,High,BroadImpact +Delegated,Domain.ReadWrite.All,High,BroadImpact +Delegated,EduRoster.ReadWrite.All,High,BroadImpact +Delegated,Group.ReadWrite.All,High,BroadImpact +Delegated,Member.Read.Hidden,High,BroadImpact +Delegated,RoleManagement.ReadWrite.Directory,High,BroadImpact +Delegated,User.ReadWrite.All,High,BroadImpact +Delegated,User.ManageCreds.All,High,BroadImpact +Application,Mail,High,Phishing +Application,Contacts,High,Phishing +Application,MailboxSettings,High,Phishing +Application,People,High,Phishing +Application,Files,High,Phishing +Application,Notes,High,Phishing +Application,Directory.AccessAsUser.All,High,Phishing +Application,user_impersonation,High,Phishing +Application,Application.ReadWrite.All,High,BroadImpact +Application,Directory.ReadWrite.All,High,BroadImpact +Application,Domain.ReadWrite.All,High,BroadImpact +Application,EduRoster.ReadWrite.All,High,BroadImpact +Application,Group.ReadWrite.All,High,BroadImpact +Application,Member.Read.Hidden,High,BroadImpact +Application,RoleManagement.ReadWrite.Directory,High,BroadImpact +Application,User.ReadWrite.All,High,BroadImpact +Application,User.ManageCreds.All,High,BroadImpact +Delegated,User.Read,Low,Common pattern +Delegated,User.ReadBasic.All,Low,Common pattern +Delegated,open_id,Low,Common pattern +Delegated,email,Low,Common pattern +Delegated,profile,Low,Common pattern +Delegated,offline_access,Low,Common pattern when used with other low permissions diff --git a/report5.xlsx b/report5.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d6e7f11fc64436013f944ecadf2e284673c46a17 GIT binary patch literal 46678 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 literal 0 HcmV?d00001 diff --git a/src/Export-MsIdAppConsentGrantReport.ps1 b/src/Export-MsIdAppConsentGrantReport.ps1 new file mode 100644 index 0000000..f8dc0ca --- /dev/null +++ b/src/Export-MsIdAppConsentGrantReport.ps1 @@ -0,0 +1,462 @@ +<# +.SYNOPSIS + Lists and categorizes privilege for delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). + +.DESCRIPTION + This cmdlet requires the ImportExcel module to be installed. + This module can be installed from the PowerShell Gallery with the following command: + + Install-Module ImportExcel + +.EXAMPLE + PS > Install-Module ImportExcel + + PS > Connect-MgGragh -Scopes Application.Read.All + + PS > Export-MsIdAppConsentGrantReport -ReportOutputType ExcelWorkbook -ExcelWorkbookPath .\report.xlsx + + Output a report in Excel format + +.EXAMPLE + PS > Export-MsIdAppConsentGrantReport -ReportOutputType ExcelWorkbook -ExcelWorkbookPath .\report.xlsx -PermissionsTableCsvPath .\table.csv + + Output a report in Excel format and specify a local path for a customized CSV containing consent privilege categorizations + +#> +function Export-MsIdAppConsentGrantReport { + [CmdletBinding(DefaultParameterSetName = 'Download Permissions Table Data', + SupportsShouldProcess = $true, + PositionalBinding = $false, + HelpUri = 'http://www.microsoft.com/', + ConfirmImpact = 'Medium')] + [Alias()] + [OutputType([String])] + Param ( + + # Output type for the report. + [ValidateSet("ExcelWorkbook", "PowerShellObjects")] + [string] + $ReportOutputType = "ExcelWorkbook", + + # Output file location for Excel Workbook + [Parameter(ParameterSetName = 'Excel Workbook Output')] + [Parameter(Mandatory = $false)] + [string] + $ExcelWorkbookPath, + + # Path to CSV file for Permissions Table + # If not provided the default table will be downloaded from GitHub https://raw.githubusercontent.com/AzureAD/MSIdentityTools/main/assets/aadconsentgrantpermissiontable.csv + [string] + $PermissionsTableCsvPath + ) + + begin { + + Set-StrictMode -Off + + function GenerateExcelReport { + param ( + $evaluatedData, + $Path + ) + + # Delete the existing output file if it already exists + $OutputFileExists = Test-Path $Path + if ($OutputFileExists -eq $true) { + Get-ChildItem $Path | Remove-Item -Force + } + + $count = 0 + $highprivilegeobjects = $evaluatedData | Where-Object { $_.Privilege -eq "High" } + $highprivilegeobjects | ForEach-Object { + $userAssignmentRequired = @() + $userAssignments = @() + $userAssignmentsCount = @() + $userAssignmentRequired = Get-MgServicePrincipal -ServicePrincipalId $_.ClientObjectId + + if ($userAssignmentRequired.AppRoleAssignmentRequired -eq $true) { + $userAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $_.ClientObjectId -All:$true + $userAssignmentsCount = $userAssignments.count + Add-Member -InputObject $_ -MemberType NoteProperty -Name UsersAssignedCount -Value $userAssignmentsCount + } + elseif ($userAssignmentRequired.AppRoleAssignmentRequired -eq $false) { + $userAssignmentsCount = "AllUsers" + Add-Member -InputObject $_ -MemberType NoteProperty -Name UsersAssignedCount -Value $userAssignmentsCount + } + + $count++ + Write-Progress -Activity "Counting users assigned to high privilege apps . . ." -Status "Apps Counted: $count of $($highprivilegeobjects.Count)" -PercentComplete (($count / $highprivilegeobjects.Count) * 100) + } + $highprivilegeusers = $highprivilegeobjects | Where-Object { $null -ne $_.PrincipalObjectId } | Select-Object PrincipalDisplayName, Privilege | Sort-Object PrincipalDisplayName -Unique + $highprivilegeapps = $highprivilegeobjects | Select-Object ClientDisplayName, Privilege, UsersAssignedCount, MicrosoftRegisteredClientApp | Sort-Object ClientDisplayName -Unique | Sort-Object UsersAssignedCount -Descending + + # Pivot table by user + $pt = New-PivotTableDefinition -SourceWorkSheet ConsentGrantData ` + -PivotTableName "PermissionsByUser" ` + -PivotFilter PrivilegeFilter, PermissionFilter, ResourceDisplayNameFilter, ConsentTypeFilter, ClientDisplayName, MicrosoftRegisteredClientApp ` + -PivotRows PrincipalDisplayName ` + -PivotColumns Privilege, PermissionType ` + -PivotData @{Permission = 'Count' } ` + -IncludePivotChart ` + -ChartType ColumnStacked ` + -ChartHeight 800 ` + -ChartWidth 1200 ` + -ChartRow 4 ` + -ChartColumn 14 + + # Pivot table by resource + $pt += New-PivotTableDefinition -SourceWorkSheet ConsentGrantData ` + -PivotTableName "PermissionsByResource" ` + -PivotFilter PrivilegeFilter, ResourceDisplayNameFilter, ConsentTypeFilter, PrincipalDisplayName, MicrosoftRegisteredClientApp ` + -PivotRows ResourceDisplayName, PermissionFilter ` + -PivotColumns Privilege, PermissionType ` + -PivotData @{Permission = 'Count' } ` + -IncludePivotChart ` + -ChartType ColumnStacked ` + -ChartHeight 800 ` + -ChartWidth 1200 ` + -ChartRow 4 ` + -ChartColumn 14 + + # Pivot table by privilege rating + $pt += New-PivotTableDefinition -SourceWorkSheet ConsentGrantData ` + -PivotTableName "PermissionsByPrivilegeRating" ` + -PivotFilter PrivilegeFilter, PermissionFilter, ResourceDisplayNameFilter, ConsentTypeFilter, PrincipalDisplayName, MicrosoftRegisteredClientApp ` + -PivotRows Privilege, ResourceDisplayName ` + -PivotColumns PermissionType ` + -PivotData @{Permission = 'Count' } ` + -IncludePivotChart ` + -ChartType ColumnStacked ` + -ChartHeight 800 ` + -ChartWidth 1200 ` + -ChartRow 4 ` + -ChartColumn 5 + + $excel = $data | Export-Excel -Path $Path -WorksheetName ConsentGrantData ` + -PivotTableDefinition $pt ` + -AutoSize ` + -Activate ` + -HideSheet "None" ` + -UnHideSheet "PermissionsByPrivilegeRating" ` + -PassThru + + # Create temporary Excel file and add High Privilege Users sheet + $xlTempFile = "$env:TEMP\ImportExcelTempFile.xlsx" + Remove-Item $xlTempFile -ErrorAction Ignore + $exceltemp = $highprivilegeusers | Export-Excel $xlTempFile -PassThru + Add-Worksheet -ExcelPackage $excel -WorksheetName HighPrivilegeUsers -CopySource $exceltemp.Workbook.Worksheets["Sheet1"] + + # Create temporary Excel file and add High Privilege Apps sheet + $xlTempFile = "$env:TEMP\ImportExcelTempFile.xlsx" + Remove-Item $xlTempFile -ErrorAction Ignore + $exceltemp = $highprivilegeapps | Export-Excel $xlTempFile -PassThru + Add-Worksheet -ExcelPackage $excel -WorksheetName HighPrivilegeApps -CopySource $exceltemp.Workbook.Worksheets["Sheet1"] -Activate + + $sheet = $excel.Workbook.Worksheets["ConsentGrantData"] + Add-ConditionalFormatting -Worksheet $sheet -Range "A1:N1048576" -RuleType Equal -ConditionValue "High" -ForeGroundColor White -BackgroundColor Red -Bold -Underline + Add-ConditionalFormatting -Worksheet $sheet -Range "A1:N1048576" -RuleType Equal -ConditionValue "Medium" -ForeGroundColor Black -BackgroundColor Orange -Bold -Underline + Add-ConditionalFormatting -Worksheet $sheet -Range "A1:N1048576" -RuleType Equal -ConditionValue "Low" -ForeGroundColor Black -BackgroundColor Yellow -Bold -Underline + + $sheet = $excel.Workbook.Worksheets["HighPrivilegeUsers"] + Add-ConditionalFormatting -Worksheet $sheet -Range "B1:B1048576" -RuleType Equal -ConditionValue "High" -ForeGroundColor White -BackgroundColor Red -Bold -Underline + Set-ExcelRange -Worksheet $sheet -Range A1:C1048576 -AutoSize + + $sheet = $excel.Workbook.Worksheets["HighPrivilegeApps"] + Add-ConditionalFormatting -Worksheet $sheet -Range "B1:B1048576" -RuleType Equal -ConditionValue "High" -ForeGroundColor White -BackgroundColor Red -Bold -Underline + Set-ExcelRange -Worksheet $sheet -Range A1:C1048576 -AutoSize + + Export-Excel -ExcelPackage $excel | Out-Null + Write-Verbose ("Excel workbook {0}" -f $ExcelWorkbookPath) + } + + function Get-MSCloudIdConsentGrantList { + [CmdletBinding()] + param( + [int] $PrecacheSize = 999 + ) + # An in-memory cache of objects by {object ID} andy by {object class, object ID} + $script:ObjectByObjectId = @{} + $script:ObjectByObjectClassId = @{} + $script:KnownMSTenantIds = @("f8cdef31-a31e-4b4a-93e4-5f571e91255a", "72f988bf-86f1-41af-91ab-2d7cd011db47") + + # Get Microsoft Graph SPN, appRoles, appRolesAssignedTo and generate hashtable for quick lookups + $servicePrincipalMsGraph = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'" + [array] $msGraphAppRoles = $servicePrincipalMsGraph.AppRoles + $script:msGraphAppRolesHashTableId = $msGraphAppRoles | Group-Object -Property Id -AsHashTable + + # Get Azure AD Graph SPN, appRoles, appRolesAssignedTo and generate hashtable for quick lookups + $servicePrincipalAadGraph = Get-MgServicePrincipal -Filter "AppId eq '00000002-0000-0000-c000-000000000000'" + [array] $aadGraphAppRoles = $servicePrincipalAadGraph.AppRoles + $script:aadGraphAppRolesHashTableId = $aadGraphAppRoles | Group-Object -Property Id -AsHashTable + + # Function to add an object to the cache + function CacheObject($Object) { + if ($Object) { + if (-not $script:ObjectByObjectClassId.ContainsKey($Object.GetType().name)) { + $script:ObjectByObjectClassId[$Object.GetType().name] = @{} + } + $script:ObjectByObjectClassId[$Object.GetType().name][$Object.Id] = $Object + $script:ObjectByObjectId[$Object.Id] = $Object + } + } + + # Function to retrieve an object from the cache (if it's there), or from Entra ID (if not). + function GetObjectByObjectId($ObjectId) { + if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { + Write-Verbose ("Querying Entra ID for object '{0}'" -f $ObjectId) + try { + $object = (Get-MgDirectoryObjectById -Ids $ObjectId).AdditionalProperties + CacheObject -Object $object + } + catch { + Write-Verbose "Object not found." + } + } + return $script:ObjectByObjectId[$ObjectId] + } + + # Get all ServicePrincipal objects and add to the cache + Write-Verbose "Retrieving ServicePrincipal objects..." + $servicePrincipals = Get-MgServicePrincipal -ExpandProperty "appRoleAssignedTo" -All:$true + $Oauth2PermGrants = @() + + $count = 0 + foreach ($sp in $servicePrincipals) { + CacheObject -Object $sp + $spPermGrants = Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $sp.Id -All:$true + $Oauth2PermGrants += $spPermGrants + $count++ + Write-Progress -Activity "Caching Objects from Entra ID . . ." -Status "Cached: $count of $($servicePrincipals.Count)" -PercentComplete (($count / $servicePrincipals.Count) * 100) + } + + # Get one page of User objects and add to the cache + Write-Verbose "Retrieving User objects..." + Get-MgUser -Top $PrecacheSize | ForEach-Object { CacheObject -Object $_ } + + # Get all existing OAuth2 permission grants, get the client, resource and scope details + Write-Progress -Activity "Processing Delegated Permission Grants..." + foreach ($grant in $Oauth2PermGrants) { + if ($grant.Scope) { + $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { + $scope = $_ + $client = GetObjectByObjectId -ObjectId $grant.ClientId + + # Determine if the object comes from the Microsoft Services tenant, and flag it if true + $MicrosoftRegisteredClientApp = @() + $appOwnerTenantId = Get-ObjectPropertyValue $client 'AppOwnerTenantId' + if ($appOwnerTenantId -in $script:KnownMSTenantIds) { + $MicrosoftRegisteredClientApp = $true + } + else { + $MicrosoftRegisteredClientApp = $false + } + + $resource = GetObjectByObjectId -ObjectId $grant.ResourceId + $principalDisplayName = "" + if ($grant.PrincipalId) { + $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId + $principalDisplayName = $principal.DisplayName + } + + if ($grant.ConsentType -eq "AllPrincipals") { + $simplifiedgranttype = "Delegated-AllPrincipals" + } + elseif ($grant.ConsentType -eq "Principal") { + $simplifiedgranttype = "Delegated-Principal" + } + + New-Object PSObject -Property ([ordered]@{ + "PermissionType" = $simplifiedgranttype + "ConsentTypeFilter" = $simplifiedgranttype + "ClientObjectId" = $grant.ClientId + "ClientDisplayName" = $client.DisplayName + "ResourceObjectId" = $grant.ResourceId + "ResourceObjectIdFilter" = $grant.ResourceId + "ResourceDisplayName" = $resource.DisplayName + "ResourceDisplayNameFilter" = $resource.DisplayName + "Permission" = $scope + "PermissionFilter" = $scope + "PrincipalObjectId" = $grant.PrincipalId + "PrincipalDisplayName" = $principalDisplayName + "MicrosoftRegisteredClientApp" = $MicrosoftRegisteredClientApp + }) + } + } + } + + # Iterate over all ServicePrincipal objects and get app permissions + Write-Progress -Activity "Processing Application Permission Grants..." + $script:ObjectByObjectClassId['MicrosoftGraphServicePrincipal'].GetEnumerator() | ForEach-Object { + $sp = $_.Value + + Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $sp.Id -All:$true ` + | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { + $assignment = $_ + + $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId + + # Determine if the object comes from the Microsoft Services tenant, and flag it if true + $MicrosoftRegisteredClientApp = @() + if ($client.AppOwnerTenantId -in $script:KnownMSTenantIds) { + $MicrosoftRegisteredClientApp = $true + } + else { + $MicrosoftRegisteredClientApp = $false + } + + $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId + #$appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id } + + # Lookup appRole for MS Graph + $appRole = $script:msGraphAppRolesHashTableId["$($assignment.AppRoleId)"] + if ($null -eq $appRole) { + # Lookup appRole for AAD Graph + $appRole = $script:aadGraphAppRolesHashTableId["$($assignment.AppRoleId)"] + } + + New-Object PSObject -Property ([ordered]@{ + "PermissionType" = "Application" + "ClientObjectId" = $assignment.PrincipalId + "ClientDisplayName" = $client.DisplayName + "ResourceObjectId" = $assignment.ResourceId + "ResourceObjectIdFilter" = $grant.ResourceId + "ResourceDisplayName" = $resource.DisplayName + "ResourceDisplayNameFilter" = $resource.DisplayName + "Permission" = $appRole.Value + "PermissionFilter" = $appRole.Value + "ConsentTypeFilter" = "Application" + "MicrosoftRegisteredClientApp" = $MicrosoftRegisteredClientApp + }) + } + } + + + } + + function EvaluateConsentGrants { + param ( + $data + ) + + + # Process Privilege for gathered data + $count = 0 + $data | ForEach-Object { + + try { + + $count++ + Write-Progress -Activity "Processing privilege for each permission . . ." -Status "Processed: $count of $($data.Count)" -PercentComplete (($count / $data.Count) * 100) + + $scope = $_.Permission + if ($_.PermissionType -eq "Delegated-AllPrincipals" -or "Delegated-Principal") { + $type = "Delegated" + } + elseif ($_.PermissionType -eq "Application") { + $type = "Application" + } + + # Check permission table for an exact match + $privilege = $null + $scoperoot = @() + Write-Debug ("Permission Scope: $Scope") + + if ($scope -match '.') { + $scoperoot = $scope.Split(".")[0] + } + else { + $scoperoot = $scope + } + + $test = ($permstable | Where-Object { $_.Permission -eq "$scoperoot" -and $_.Type -eq $type }).Privilege # checking if there is a matching root in the CSV + $privilege = ($permstable | Where-Object { $_.Permission -eq "$scope" -and $_.Type -eq $type }).Privilege # Checking for an exact match + + # Search for matching root level permission if there was no exact match + if (!$privilege -and $test) { + # No exact match, but there is a root match + $privilege = ($permstable | Where-Object { $_.Permission -eq "$scoperoot" -and $_.Type -eq $type }).Privilege + } + elseif (!$privilege -and !$test -and $type -eq "Application" -and $scope -like "*Write*") { + # Application permissions without exact or root matches with write scope + $privilege = "High" + } + elseif (!$privilege -and !$test -and $type -eq "Application" -and $scope -notlike "*Write*") { + # Application permissions without exact or root matches without write scope + $privilege = "Medium" + } + elseif ($privilege) { + + } + else { + # Any permissions without a match, should be primarily Delegated permissions + $privilege = "Unranked" + } + + # Add the privilege to the current object + Add-Member -InputObject $_ -MemberType NoteProperty -Name Privilege -Value $privilege + Add-Member -InputObject $_ -MemberType NoteProperty -Name PrivilegeFilter -Value $privilege + Add-Member -InputObject $_ -MemberType NoteProperty -Name Reason -Value $reason + } + catch { + Write-Error "Error Processing Permission for $_" + } + finally { + Write-Output $_ + } + } + + } + + function loadPermisionsTable { + param ( + $PermissionsTableCsvPath + ) + + if ($null -like $PermissionsTableCsvPath) { + # Create hash table of permissions and permissions privilege + Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/AzureAD/MSIdentityTools/main/assets/aadconsentgrantpermissiontable.csv' -OutFile .\aadconsentgrantpermissiontable.csv + $permstable = Import-Csv .\aadconsentgrantpermissiontable.csv -Delimiter ',' + } + else { + + $permstable = Import-Csv $PermissionsTableCsvPath -Delimiter ',' + } + + Write-Output $permstable + + } + + if ("ExcelWorkbook" -eq $ReportOutputType) { + # Determine if the ImportExcel module is installed since the parameter was included + if ($null -eq (Get-Module -Name ImportExcel -ListAvailable)) { + throw "The ImportExcel module is not installed. This is used to export the results to an Excel worksheet. Please install the ImportExcel Module before using this parameter or run without this parameter." + } + } + + } + process { + + $permstable = loadPermisionsTable -PermissionsTableCsvPath $PermissionsTableCsvPath + + Write-Verbose "Retrieving Permission Grants from Entra ID..." + $data = Get-MSCloudIdConsentGrantList + if ($null -ne $data) { + $evaluatedData = EvaluateConsentGrants -data $data + } + + } + end { + + if ("ExcelWorkbook" -eq $ReportOutputType) { + + Write-Verbose "Generating Excel Workbook at $ExcelWorkbookPath" + GenerateExcelReport -evaluatedData $evaluatedData -Path $ExcelWorkbookPath + + } + else { + Write-Output $evaluatedData + } + + Set-StrictMode -Version Latest + } +} diff --git a/src/MSIdentityTools.psd1 b/src/MSIdentityTools.psd1 index 9968a58..c97aae7 100644 --- a/src/MSIdentityTools.psd1 +++ b/src/MSIdentityTools.psd1 @@ -105,6 +105,7 @@ '.\ConvertFrom-MsIdJwtToken.ps1' '.\ConvertFrom-MsIdSamlMessage.ps1' '.\Expand-MsIdJwtTokenPayload.ps1' + '.\Export-MsIdAppConsentGrantReport.ps1' '.\Find-MsIdUnprotectedUsersWithAdminRoles.ps1' '.\Get-MsIdProvisioningLogStatistics.ps1' '.\Get-MsIdAdfsSamlToken.ps1' @@ -160,6 +161,7 @@ 'ConvertFrom-MsIdJwtToken' 'ConvertFrom-MsIdSamlMessage' 'Expand-MsIdJwtTokenPayload' + 'Export-MsIdAppConsentGrantReport' 'Find-MsIdUnprotectedUsersWithAdminRoles' 'Get-MsIdProvisioningLogStatistics' 'Get-MsIdAdfsSamlToken'