AzureStuff.psm1
function Add-AzureAppUserConsent { <# .SYNOPSIS Function for granting consent on behalf of a user to chosen application over selected resource(s) (enterprise app(s)) and permission(s) and assign the user default app role to be able to see the app in his 'My Apps'. .DESCRIPTION Function for granting consent on behalf of a user to chosen application over selected resource(s) (enterprise app(s)) and permission(s) and assign the user default app role to be able to see the app in his 'My Apps'. Consent can be explicitly specified or copied from some existing one. .PARAMETER clientAppId ID of application you want to grant consent on behalf of a user. .PARAMETER consent Hashtable where: - key is objectId of the resource (enterprise app) you are granting permissions to - value is list of permissions strings (scopes) Both can be found at Permissions tab of the enterprise app in Azure portal, when you select particular permission. For example: $consent = @{ "02ad85cd-02ce-4902-a349-1af61152a021" = "User.Read", "Contacts.ReadWrite", "Calendars.ReadWrite", "Mail.Send", "Mail.ReadWrite", "EWS.AccessAsUser.All" } .PARAMETER copyExistingConsent Switch for getting consent details (resource ObjectId and permissions) from existing user consent. You will be asked for confirmation before proceeding. .PARAMETER userUpnOrId User UPN or ID. .EXAMPLE $consent = @{ "88690023-f9e1-4728-9028-cdcc6bf67d22" = "User.Read" "02ad85cd-02ce-4902-a349-1af61152a021" = "User.Read", "Contacts.ReadWrite", "Calendars.ReadWrite", "Mail.Send", "Mail.ReadWrite", "EWS.AccessAsUser.All" } Add-AzureAppUserConsent -clientAppId "00b263e4-3497-4630-b082-3197csadd7c" -consent $consent -userUpnOrId "dealdesk@contoso.onmicrosoft.com" Grants consent on behalf of the "dealdesk@contoso.onmicrosoft.com" user to application "Salesforce Inbox" (00b263e4-3497-4630-b082-3197csadd7c) and given permissions on resource (ent. application) "Office 365 Exchange Online" (02ad85cd-02ce-4902-a349-1af61152a021) and "Windows Azure Active Directory" (88690023-f9e1-4728-9028-cdcc6bf67d22). .EXAMPLE Add-AzureAppUserConsent -clientAppId "00b263e4-3497-4630-b082-3197csadd7c" -copyExistingConsent -userUpnOrId "dealdesk@contoso.onmicrosoft.com" Grants consent on behalf of the "dealdesk@contoso.onmicrosoft.com" user to application "Salesforce Inbox" (00b263e4-3497-4630-b082-3197csadd7c) based on one of the existing consents. .NOTES https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-consent-single-user #> [CmdletBinding()] param ( # The app for which consent is being granted [Parameter(Mandatory = $true)] [string] $clientAppId, [Parameter(Mandatory = $true, ParameterSetName = "explicit")] [hashtable] $consent, [Parameter(ParameterSetName = "copyConsent")] [switch] $copyExistingConsent, [Parameter(Mandatory = $true)] # The user on behalf of whom access will be granted. The app will be able to access the API on behalf of this user. [string] $userUpnOrId ) $ErrorActionPreference = "Stop" #region connect to Microsoft Graph PowerShell # we need User.ReadBasic.All to get # users' IDs, Application.ReadWrite.All to list and create service principals, # DelegatedPermissionGrant.ReadWrite.All to create delegated permission grants, # and AppRoleAssignment.ReadWrite.All to assign an app role. # WARNING: These are high-privilege permissions! Import-Module Microsoft.Graph.Authentication Import-Module Microsoft.Graph.Applications Import-Module Microsoft.Graph.Users Import-Module Microsoft.Graph.Identity.SignIns $null = Connect-MgGraph -Scopes ("User.ReadBasic.All", "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "AppRoleAssignment.ReadWrite.All") #endregion connect to Microsoft Graph PowerShell $clientSp = Get-MgServicePrincipal -Filter "appId eq '$($clientAppId)'" if (-not $clientSp) { throw "Enterprise application with Application ID $clientAppId doesn't exist" } # prepare consent from the existing one if ($copyExistingConsent) { $consent = @{} Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $clientSp.id -All | group resourceId | select @{n = 'ResourceId'; e = { $_.Name } }, @{n = 'ScopeToGrant'; e = { $_.group | select -First 1 | select -ExpandProperty scope } } | % { $consent.($_.ResourceId) = $_.ScopeToGrant } if (!$consent.Keys) { throw "There is no existing user consent that can be cloned. Use parameter consent instead." } else { "Following consent(s) will be added:" $consent.GetEnumerator() | % { $resourceSp = Get-MgServicePrincipal -Filter "id eq '$($_.key)'" if (!$resourceSp) { throw "Resource with ObjectId $($_.key) doesn't exist" } " - resource '$($resourceSp.DisplayName)' permission: $(($_.value | sort) -join ', ')" } $choice = "" while ($choice -notmatch "^[Y|N]$") { $choice = Read-Host "`nContinue? (Y|N)" } if ($choice -eq "N") { break } } } #region create a delegated permission that grants the client app access to the API, on behalf of the user. $user = Get-MgUser -UserId $userUpnOrId if (!$user) { throw "User $userUpnOrId doesn't exist" } foreach ($item in $consent.GetEnumerator()) { $resourceId = $item.key $scope = $item.value if (!$scope) { throw "You haven't specified any scope for resource $resourceId" } $resourceSp = Get-MgServicePrincipal -Filter "id eq '$resourceId'" if (!$resourceSp) { throw "Resource with ObjectId $resourceId doesn't exist" } # convert scope string (perm1 perm2) i.e. permission joined by empty space (returned by Get-AzureADServicePrincipalOAuth2PermissionGrant) into array if ($scope -match "\s+") { $scope = $scope -split "\s+" | ? { $_ } } $scopeToGrant = $scope # check if user already granted some permissions to this app for such resource # and skip such permissions to avoid errors $scopeAlreadyGranted = Get-MgOauth2PermissionGrant -Filter "principalId eq '$($user.Id)' and clientId eq '$($clientSp.Id)' and resourceId eq '$resourceId'" | select -ExpandProperty Scope if ($scopeAlreadyGranted) { Write-Verbose "Some permission(s) ($($scopeAlreadyGranted.trim())) are already granted to an app '$($clientSp.Id)' and resourceId '$resourceId'" $scopeAlreadyGrantedList = $scopeAlreadyGranted.trim() -split "\s+" $scopeToGrant = $scope | ? { $_ } | % { if ($_ -in $scopeAlreadyGrantedList) { Write-Warning "Permission '$_' is already granted. Skipping" } else { $_ } } if (!$scopeToGrant) { Write-Warning "All permissions for resource $resourceId are already granted. Skipping" continue } } Write-Warning "Grant user consent on behalf of '$userUpnOrId' for application '$($clientSp.DisplayName)' to have following permission(s) '$(($scopeToGrant.trim() | sort) -join ', ')' over API '$($resourceSp.DisplayName)'" $grant = New-MgOauth2PermissionGrant -ResourceId $resourceSp.Id -Scope ($scopeToGrant -join " ") -ClientId $clientSp.Id -ConsentType "Principal" -PrincipalId $user.Id } #endregion create a delegated permission that grants the client app access to the API, on behalf of the user. #region assign the app to the user. # this ensures that the user can sign in if assignment is required, and ensures that the app shows up under the user's My Apps. $userAssignableRole = $clientSp.AppRoles | ? { $_.AllowedMemberTypes -contains "User" } if ($userAssignableRole) { Write-Warning "A default app role assignment cannot be created because the client application exposes user-assignable app roles ($($userAssignableRole.DisplayName -join ', ')). You must assign the user a specific app role for the app to be listed in the user's My Apps access panel." } else { if (Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $clientSp.Id -Property AppRoleId, PrincipalId | ? PrincipalId -EQ $user.Id) { # user already have some app role assigned Write-Verbose "User already have some app role assigned. Skipping default app role assignment." } else { # the app role ID 00000000-0000-0000-0000-000000000000 is the default app role # indicating that the app is assigned to the user, but not for any specific app role. Write-Verbose "Assigning default app role to the user" $assignment = New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $clientSp.Id -ResourceId $clientSp.Id -PrincipalId $user.Id -AppRoleId "00000000-0000-0000-0000-000000000000" } } #endregion assign the app to the user. } function Add-AzureGuest { <# .SYNOPSIS Function for inviting guest user to Azure AD. .DESCRIPTION Function for inviting guest user to Azure AD. .PARAMETER displayName Display name of the user. Suffix (guest) will be added automatically. a.k.a Jan Novak .PARAMETER emailAddress Email address of the user. a.k.a novak@seznam.cz .PARAMETER parentTeamsGroup Optional parameter. Name of Teams group, where the guest should be added as member. (it can take several minutes, before this change propagates!) .EXAMPLE Add-AzureGuest -displayName "Jan Novak" -emailAddress "novak@seznam.cz" #> [CmdletBinding()] [Alias("New-AzureADGuest")] param ( [Parameter(Mandatory = $true)] [ValidateScript( { If ($_ -match "\(guest\)") { throw "$_ (guest) will be added automatically." } else { $true } })] [string] $displayName , [Parameter(Mandatory = $true)] [ValidateScript( { If ($_ -match "@") { $true } else { Throw "$_ isn't email address" } })] [string] $emailAddress , [ValidateScript( { If ($_ -notmatch "^External_") { throw "$_ doesn't allow guest members (doesn't start with External_ prefix, so guests will be automatically removed)" } else { $true } })] [string] $parentTeamsGroup ) $null = Connect-MgGraph # naming conventions (Get-Variable displayName).Attributes.Clear() $displayName = $displayName.trim() + " (guest)" $emailAddress = $emailAddress.trim() "Creating Guest: $displayName EMAIL: $emailaddress" $null = New-MgInvitation -InvitedUserDisplayName $displayName -InvitedUserEmailAddress $emailAddress -InviteRedirectUrl "https://myapps.microsoft.com" -SendInvitationMessage:$true -InvitedUserType Guest if ($parentTeamsGroup) { $groupID = Get-MgGroup -Filter "displayName eq '$parentTeamsGroup'" | select -exp Id if (!$groupID) { throw "Unable to find group $parentTeamsGroup" } $guestId = Get-MgUser -Filter "mail eq '$emailaddress'" | select -exp Id New-MgGroupMember -GroupId $groupID -DirectoryObjectId $guestId } } function Disable-AzureGuest { <# .SYNOPSIS Function for disabling guest user in Azure AD. .DESCRIPTION Function for disabling guest user in Azure AD. Do NOT REMOVE the account, because lot of connected systems use UPN as identifier instead of SID. Therefore if someone in the future add such guest again, he would get access to all stuff, previous guest had access to. .PARAMETER displayName Display name of the user. If not specified, GUI with all guests will popup. .EXAMPLE Disable-AzureGuest -displayName "Jan Novak (guest)" Disables "Jan Novak (guest)" guest Azure AD account. .EXAMPLE Disable-AzureGuest Show GUI with all available guest accounts. The selected one will be disabled. #> [CmdletBinding()] [Alias("Remove-AzureADGuest")] param ( [string[]] $displayName ) $null = Connect-MgGraph -ea Stop $guestId = @() if (!$displayName) { # Get all the Guest Users $guest = Get-MgUser -All -Filter "UserType eq 'Guest' and AccountEnabled eq true" | select DisplayName, Mail, Id | Out-GridView -OutputMode Multiple -Title "Select accounts for disable" $guestId = $guest.id } else { $displayName | % { $guest = Get-MgUser -Filter "DisplayName eq '$_' and UserType eq 'Guest' and AccountEnabled eq true" if ($guest) { $guestId += $guest.Id } else { Write-Warning "$_ wasn't found or it is not guest account or is disabled already" } } } if ($guestId) { $guestId | % { "Blocking guest $_" # block Sign-In Update-MgUser -UserId $_ -AccountEnabled:$false # invalidate Azure AD Tokens $null = Revoke-MgUserSignInSession -UserId $_ -Confirm:$false } } else { Write-Warning "No guest to disable" } } function Get-AzureAccountOccurrence { <# .SYNOPSIS Function for getting AzureAD account occurrences through various parts of Azure. Only Azure based objects are scanned (not dir-synced ones). .DESCRIPTION Function for getting AzureAD account occurrences through various parts of AzureAD. Only Azure based objects are scanned (not dir-synced ones). You can search occurrences of 'user', 'group', 'servicePrincipal', 'device' objects. These Azure parts are searched by default: 'IAM', 'GroupMembership', 'DirectoryRoleMembership', 'UserConsent', 'Manager', 'Owner', 'SharepointSiteOwner', 'Users&GroupsRoleAssignment' .PARAMETER userPrincipalName UPN of the user you want to search occurrences for. .PARAMETER objectId ObjectId of the 'user', 'group', 'servicePrincipal' or 'device' you want to search occurrences for. .PARAMETER data Array of Azure parts you want to search in. By default: 'IAM' - IAM assignments of the root, subscriptions, management groups, resource groups, resources where searched account is assigned 'GroupMembership' - groups where searched account is a member 'DirectoryRoleMembership' - directory roles where searched account is a member 'UserConsent' - user granted consents 'Manager' - accounts where searched user is manager 'Owner' - accounts where searched user is owner 'SharepointSiteOwner' - sharepoint sites where searched account is owner 'Users&GroupsRoleAssignment' - applications Users and groups tab where searched account is assigned 'DevOps' - occurrences in DevOps organizations 'KeyVaultAccessPolicy' - KeyVault access policies grants 'ExchangeRole' - Exchange Admin Roles Based on the object type you are searching occurrences for, this can be automatically trimmed. Because for example device cannot be manager etc. .PARAMETER tenantId Name of the tenant if different then the default one should be used. .EXAMPLE Get-AzureAccountOccurrence -objectId 1234-1234-1234 Search for all occurrences of the account with id 1234-1234-1234. .EXAMPLE Get-AzureAccountOccurrence -objectId 1234-1234-1234 -data UserConsent, Manager Search just for user perm. consents which searched account has given and accounts where searched account is manager of. .EXAMPLE Get-AzureAccountOccurrence -userPrincipalName novak@contoso.com Search for all occurrences of the account with UPN novak@contoso.com. .NOTES In case of 'data' parameter edit, don't forget to modify _getAllowedSearchType and Remove-AzureAccountOccurrence functions too #> [CmdletBinding()] [Alias("Get-AzureADAccountOccurrence")] param ( [ValidateNotNullOrEmpty()] [ValidateScript( { If ($_ -notmatch "@") { throw "Username isn't UPN" } else { $true } })] [string[]] $userPrincipalName, [ValidateScript( { $StringGuid = $_ $ObjectGuid = [System.Guid]::empty if ([System.Guid]::TryParse($StringGuid, [System.Management.Automation.PSReference]$ObjectGuid)) { $true } else { throw "$_ is not a valid GUID" } })] [string[]] $objectId, [ValidateSet('IAM', 'GroupMembership', 'DirectoryRoleMembership', 'UserConsent', 'Manager', 'Owner', 'SharepointSiteOwner', 'Users&GroupsRoleAssignment', 'DevOps', 'KeyVaultAccessPolicy', 'ExchangeRole')] [ValidateNotNullOrEmpty()] [string[]] $data = @('IAM', 'GroupMembership', 'DirectoryRoleMembership', 'UserConsent', 'Manager', 'Owner', 'SharepointSiteOwner', 'Users&GroupsRoleAssignment', 'DevOps', 'KeyVaultAccessPolicy', 'ExchangeRole'), [string] $tenantId ) if (!$userPrincipalName -and !$objectId) { throw "You haven't specified userPrincipalname nor objectId parameter" } if ($tenantId) { $tenantIdParam = @{ tenantId = $tenantId } } else { $tenantIdParam = @{} } #region connect # connect to AzureAD $null = Connect-MgGraph -ea Stop $null = Connect-AzAccount2 @tenantIdParam -ea Stop # create Graph API auth. header Write-Verbose "Creating Graph API auth header" $graphAuthHeader = New-GraphAPIAuthHeader @tenantIdParam -ea Stop # connect sharepoint online if ($data -contains 'SharepointSiteOwner') { Write-Verbose "Connecting to Sharepoint" Connect-PnPOnline2 -asMFAUser -ea Stop } if ($data -contains 'ExchangeRole') { Write-Verbose "Connecting to Exchange" Connect-O365 -service exchange -ea Stop } #endregion connect # translate UPN to ObjectId if ($userPrincipalName) { $userPrincipalName | % { $UPN = $_ $AADUserobj = Get-MgUser -Filter "userPrincipalName eq '$UPN'" if (!$AADUserobj) { Write-Error "Account $UPN was not found in AAD" } else { Write-Verbose "Translating $UPN to $($AADUserobj.Id) Id" $objectId += $AADUserobj.Id } } } #region helper functions # function for deciding what kind of data make sense to search through when you have object of specific kind function _getAllowedSearchType { param ($searchedData) switch ($searchedData) { 'IAM' { $allowedObjType = 'user', 'group', 'servicePrincipal' } 'GroupMembership' { $allowedObjType = 'user', 'group', 'servicePrincipal', 'device' } 'DirectoryRoleMembership' { $allowedObjType = 'user', 'group', 'servicePrincipal' } 'UserConsent' { $allowedObjType = 'user' } 'Manager' { $allowedObjType = 'user' } 'Owner' { $allowedObjType = 'user', 'servicePrincipal' } 'SharepointSiteOwner' { $allowedObjType = 'user' } 'Users&GroupsRoleAssignment' { $allowedObjType = 'user', 'group' } 'DevOps' { $allowedObjType = 'user', 'group' } 'KeyVaultAccessPolicy' { $allowedObjType = 'user', 'group', 'servicePrincipal' } 'ExchangeRole' { $allowedObjType = 'user', 'group' } default { throw "Undefined data to search $searchedData (edit _getAllowedSearchType function)" } } if ($objectType -in $allowedObjType) { return $true } else { Write-Warning "Skipping '$searchedData' data search because object of type $objectType cannot be there" return $false } } # function for translating DevOps membership hrefs to actual groups function _getMembership { param ([string[]] $membershipHref, [string] $organizationName) $membershipHref | % { Invoke-WebRequest -Uri $_ -Method get -ContentType "application/json" -Headers $devOpsAuthHeader | select -exp content | ConvertFrom-Json | select -exp value | select -exp containerDescriptor | % { $groupOrg = $devOpsOrganization | ? { $_.OrganizationName -eq $organizationName } $group = $groupOrg.groups | ? descriptor -EQ $_ if ($group) { $group } else { Write-Error "Group with descriptor $_ wasn't found" [PSCustomObject]@{ ContainerDescriptor = $_ } } } } } #endregion helper functions #region pre-cache data #TODO cache only in case some allowed account type for such data is searched if ('IAM' -in $data) { Write-Warning "Caching AzureAD Role Assignments. This can take several minutes!" $azureADRoleAssignments = Get-AzureRoleAssignments @tenantIdParam } if ('SharepointSiteOwner' -in $data) { Write-Warning "Caching Sharepoint sites ownership. This can take several minutes!" $sharepointSiteOwner = Get-SharepointSiteOwner } if ('DevOps' -in $data) { Write-Warning "Caching DevOps organizations." $devOpsOrganization = Get-AzureDevOpsOrganizationOverview @tenantIdParam #TODO poresit strankovani! Write-Warning "Caching DevOps organizations groups." $devOpsAuthHeader = New-AzureDevOpsAuthHeader $devOpsOrganization | % { $organizationName = $_.OrganizationName Write-Verbose "Getting groups for DevOps organization $organizationName" $groups = $null # in case of error this wouldn't be nulled try { $groups = Invoke-WebRequest -Uri "https://vssps.dev.azure.com/$organizationName/_apis/graph/groups?api-version=7.1-preview.1" -Method get -ContentType "application/json" -Headers $devOpsAuthHeader -ea Stop | select -exp content | ConvertFrom-Json | select -exp value } catch { if ($_ -match "is not authorized to access this resource|UnauthorizedRequestException") { Write-Warning "You don't have rights to get groups data for DevOps organization $organizationName." } else { Write-Error $_ } } $_ | Add-Member -MemberType NoteProperty -Name Groups -Value $groups } #TODO poresit strankovani! Write-Warning "Caching DevOps organizations users." $devOpsAuthHeader = New-AzureDevOpsAuthHeader $devOpsOrganization | % { $organizationName = $_.OrganizationName Write-Verbose "Getting users for DevOps organization $organizationName" $users = $null # in case of error this wouldn't be nulled try { $users = Invoke-WebRequest -Uri "https://vssps.dev.azure.com/$organizationName/_apis/graph/users?api-version=7.1-preview.1" -Method get -ContentType "application/json" -Headers $devOpsAuthHeader -ea Stop | select -exp content | ConvertFrom-Json | select -exp value } catch { if ($_ -match "is not authorized to access this resource|UnauthorizedRequestException") { Write-Warning "You don't have rights to get users data for DevOps organization $organizationName." } else { Write-Error $_ } } $_ | Add-Member -MemberType NoteProperty -Name Users -Value $users } } if ('KeyVaultAccessPolicy' -in $data) { Write-Warning "Caching KeyVault Access Policies. This can take several minutes!" $keyVaultList = @() $CurrentContext = Get-AzContext $Subscriptions = Get-AzSubscription -TenantId $CurrentContext.Tenant.Id foreach ($Subscription in ($Subscriptions | Sort-Object Name)) { Write-Verbose "Changing to Subscription $($Subscription.Name) ($($Subscription.SubscriptionId))" $Context = Set-AzContext -TenantId $Subscription.TenantId -SubscriptionId $Subscription.Id -Force Get-AzKeyVault -WarningAction SilentlyContinue | % { $keyVaultList += Get-AzKeyVault -VaultName $_.VaultName -WarningAction SilentlyContinue } } } if ('ExchangeRole' -in $data) { Write-Warning "Caching Exchange roles." $exchangeRoleAssignments = @() Get-RoleGroup | % { $roleName = $_.name $roleDN = $_.displayname $roleCapabilities = $_.capabilities $exchangeRoleAssignments += Get-RoleGroupMember -Identity $roleName -ResultSize unlimited | select @{n = 'Role'; e = { $roleName } }, @{name = 'RoleDisplayName'; e = { $roleDN } }, @{n = 'RoleCapabilities'; e = { $roleCapabilities } }, * } } #endregion pre-cache data # object types that are allowed for searching $allowedObjectType = 'user', 'group', 'servicePrincipal', 'device' foreach ($id in $objectId) { $AADAccountObj = Get-MgDirectoryObjectById -Ids $id | Expand-MgAdditionalProperties if (!$AADAccountObj) { Write-Error "Account $id was not found in AAD" continue } # progress variables $i = 0 $progressActivity = "Account '$($AADAccountObj.displayName)' ($id) occurrences" $objectType = $AADAccountObj.ObjectType if ($objectType -notin $allowedObjectType) { Write-Warning "Skipping '$($AADAccountObj.displayName)' ($id) because it is disallowed object type ($objectType)" continue } else { Write-Warning "Processing '$($AADAccountObj.displayName)' ($id)" } # define base object $result = [PSCustomObject]@{ UPN = $AADAccountObj.userPrincipalName DisplayName = $AADAccountObj.displayName ObjectType = $objectType ObjectId = $id IAM = @() MemberOfDirectoryRole = @() MemberOfGroup = @() Manager = @() PermissionConsent = @() Owner = @() SharepointSiteOwner = @() AppUsersAndGroupsRoleAssignment = @() DevOpsOrganizationOwner = @() DevOpsMemberOf = @() KeyVaultAccessPolicy = @() ExchangeRole = @() } #region get AAD account occurrences #region Exchange Role assignments if ('ExchangeRole' -in $data -and (_getAllowedSearchType 'ExchangeRole')) { Write-Verbose "Getting Exchange role assignments" Write-Progress -Activity $progressActivity -Status "Getting Exchange role assignments" -PercentComplete (($i++ / $data.Count) * 100) $result.ExchangeRole = @($exchangeRoleAssignments | ? ExternalDirectoryObjectId -EQ $id) } #endregion Exchange Role assignments #region KeyVault Access Policy if ('KeyVaultAccessPolicy' -in $data -and (_getAllowedSearchType 'KeyVaultAccessPolicy')) { Write-Verbose "Getting KeyVault Access Policy assignments" Write-Progress -Activity $progressActivity -Status "Getting KeyVault Access Policy assignments" -PercentComplete (($i++ / $data.Count) * 100) $keyVaultList | % { $keyVault = $_ $accessPolicies = $keyVault.AccessPolicies | ? { $_.objectId -eq $id } if ($accessPolicies) { $result.KeyVaultAccessPolicy += $keyVault | select *, @{n = 'AccessPolicies'; e = { $accessPolicies } } -ExcludeProperty AccessPolicies, AccessPoliciesText } } } #endregion KeyVault Access Policy #region IAM if ('IAM' -in $data -and (_getAllowedSearchType 'IAM')) { Write-Verbose "Getting IAM assignments" Write-Progress -Activity $progressActivity -Status "Getting IAM assignments" -PercentComplete (($i++ / $data.Count) * 100) $azureADRoleAssignments | ? objectId -EQ $id | % { $result.IAM += $_ } } #endregion IAM #region DirectoryRoleMembership if ('DirectoryRoleMembership' -in $data -and (_getAllowedSearchType 'DirectoryRoleMembership')) { Write-Verbose "Getting Directory Role Membership assignments" Write-Progress -Activity $progressActivity -Status "Getting Directory Role Membership assignments" -PercentComplete (($i++ / $data.Count) * 100) Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '$id'" | % { $_ | Add-Member -Name 'RoleName' -MemberType NoteProperty -Value (Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $_.RoleDefinitionId | select -ExpandProperty DisplayName) $result.MemberOfDirectoryRole += $_ } } #endregion DirectoryRoleMembership #region Group membership if ('GroupMembership' -in $data -and (_getAllowedSearchType 'GroupMembership')) { Write-Verbose "Getting Group memberships" Write-Progress -Activity $progressActivity -Status "Getting Group memberships" -PercentComplete (($i++ / $data.Count) * 100) # reauthenticate just in case previous steps took too much time and the token has expired in the meantime if (!$graphAuthHeader -or ($graphAuthHeader -and $graphAuthHeader.ExpiresOn -le [datetime]::Now)) { Write-Verbose "Creating new auth token, just in case it expired" $graphAuthHeader = New-GraphAPIAuthHeader @tenantIdParam -ea Stop } switch ($objectType) { 'user' { $searchLocation = "users" } 'group' { $searchLocation = "groups" } 'device' { $searchLocation = "devices" } 'servicePrincipal' { $searchLocation = "servicePrincipals" } default { throw "Undefined object type '$objectType'" } } Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/v1.0/$searchLocation/$id/memberOf" -header $graphAuthHeader | ? { $_ } | % { if ($_.'@odata.type' -eq '#microsoft.graph.directoryRole') { # directory roles are added in different IF, moreover this query doesn't return custom roles } elseif ($_.'@odata.context') { # not a member } else { $result.MemberOfGroup += $_ } } } #endregion Group membership #region user perm consents if ('UserConsent' -in $data -and (_getAllowedSearchType 'UserConsent')) { Write-Verbose "Getting permission consents" Write-Progress -Activity $progressActivity -Status "Getting permission consents" -PercentComplete (($i++ / $data.Count) * 100) Get-MgUserOauth2PermissionGrant -UserId $id -All | % { $result.PermissionConsent += $_ | select *, @{name = 'AppName'; expression = { (Get-MgServicePrincipal -ServicePrincipalId $_.ClientId).DisplayName } }, @{name = 'ResourceDisplayName'; expression = { (Get-MgServicePrincipal -ServicePrincipalId $_.ResourceId).DisplayName } } } } #endregion user perm consents #region is manager if ('Manager' -in $data -and (_getAllowedSearchType 'Manager')) { Write-Verbose "Getting Direct report" Write-Verbose "Just Cloud based objects are outputted" Write-Progress -Activity $progressActivity -Status "Getting Direct Report (managedBy)" -PercentComplete (($i++ / $data.Count) * 100) # TODO nevraci DirSyncedEnabled Get-MgUserDirectReport -UserId $id -All | Expand-MgAdditionalProperties | % { $result.Manager += $_ } } #endregion is manager #region is owner # group, ent. app, app reg. and device ownership if ('Owner' -in $data -and (_getAllowedSearchType 'Owner')) { Write-Verbose "Getting application, group etc ownership" Write-Progress -Activity $progressActivity -Status "Getting group, app and device ownership" -PercentComplete (($i++ / $data.Count) * 100) switch ($objectType) { 'user' { Get-MgUserOwnedObject -UserId $id -All | Expand-MgAdditionalProperties | % { $result.Owner += $_ } Write-Verbose "Getting device(s) ownership" Get-MgUserOwnedDevice -UserId $id -All | Expand-MgAdditionalProperties | % { $result.Owner += $_ } } 'servicePrincipal' { Get-MgServicePrincipalOwnedObject -ServicePrincipalId $id -All | Expand-MgAdditionalProperties | % { $result.Owner += $_ } } default { throw "Undefined condition for $objectType objectType when searching for 'Owner'" } } } #sharepoint sites owner if ('SharepointSiteOwner' -in $data -and (_getAllowedSearchType 'SharepointSiteOwner')) { Write-Verbose "Getting Sharepoint sites ownership" Write-Progress -Activity $progressActivity -Status "Getting Sharepoint sites ownership" -PercentComplete (($i++ / $data.Count) * 100) $sharepointSiteOwner | ? { ($userPrincipalName -and $_.Owner -contains $userPrincipalName) -or ($AADAccountObj.displayName -and $_.Owner -contains $AADAccountObj.displayName) } | % { $result.SharepointSiteOwner += $_ } } #endregion is owner #region App Users and groups role assignments if ('Users&GroupsRoleAssignment' -in $data -and (_getAllowedSearchType 'Users&GroupsRoleAssignment')) { Write-Verbose "Getting applications 'Users and groups' role assignments" Write-Progress -Activity $progressActivity -Status "Getting applications 'Users and groups' role assignments" -PercentComplete (($i++ / $data.Count) * 100) function GetRoleName { param ($objectId, $roleId) if ($roleId -eq '00000000-0000-0000-0000-000000000000') { return 'default' } else { Get-MgServicePrincipal -ServicePrincipalId $objectId -Property AppRoles | select -ExpandProperty AppRoles | ? id -EQ $roleId | select -ExpandProperty DisplayName } } switch ($objectType) { 'user' { # filter out assignments based on group membership Get-MgUserAppRoleAssignment -UserId $id -All | ? PrincipalDisplayName -EQ $AADAccountObj.displayName | select *, @{name = 'AppRoleDisplayName'; expression = { GetRoleName -objectId $_.ResourceId -roleId $_.AppRoleId } } | % { $result.AppUsersAndGroupsRoleAssignment += $_ } } 'group' { Get-MgGroupAppRoleAssignment -GroupId $id -All | select *, @{name = 'AppRoleDisplayName'; expression = { GetRoleName -objectId $_.ResourceId -roleId $_.AppRoleId } } | % { $result.AppUsersAndGroupsRoleAssignment += $_ } } default { throw "Undefined condition for $objectType objectType when searching for 'Users&GroupsRoleAssignment'" } } } #endregion App Users and groups role assignments #region devops # https://docs.microsoft.com/en-us/rest/api/azure/devops/ if ('DevOps' -in $data -and (_getAllowedSearchType 'DevOps')) { Write-Verbose "Getting DevOps occurrences" Write-Progress -Activity $progressActivity -Status "Getting DevOps occurrences" -PercentComplete (($i++ / $data.Count) * 100) $devOpsAuthHeader = New-AzureDevOpsAuthHeader # auth. token has just minutes lifetime! $devOpsOrganization | % { $organization = $_ $organizationName = $organization.OrganizationName $organizationOwner = $organization.Owner if ($organizationOwner -eq $AADAccountObj.userPrincipalName -or $organizationOwner -eq $AADAccountObj.displayName) { $result.DevOpsOrganizationOwner += $organization } if ($objectType -eq 'user') { $userInOrg = $organization.users | ? originId -EQ $AADAccountObj.Id if ($userInOrg) { # user is used in this DevOps organization $memberOf = _getMembership $userInOrg._links.memberships.href $organizationName $result.DevOpsMemberOf += [PSCustomObject]@{ OrganizationName = $organizationName MemberOf = $memberOf Descriptor = $userInOrg.descriptor } } else { # try to find it as an orphaned guest (has special principalname) $orphanGuestUserInOrg = $organization.users | ? { $_.displayName -EQ $AADAccountObj.displayName -and $_.directoryAlias -Match "#EXT#$" -and $_.principalName -Match "OIDCONFLICT_UpnReuse_" } if ($orphanGuestUserInOrg) { Write-Warning "$($AADAccountObj.displayName) guest user is used in DevOps organization '$organizationName' but it is orphaned record (guest user was assigned to this organization than deleted and than invited again with the same UPN" } } } elseif ($objectType -eq 'group') { $groupInOrg = $organization.groups | ? originId -EQ $AADAccountObj.Id if ($groupInOrg) { # group is used in this DevOps organization $memberOf = _getMembership $groupInOrg._links.memberships.href $organizationName $result.DevOpsMemberOf += [PSCustomObject]@{ OrganizationName = $organizationName MemberOf = $memberOf Descriptor = $groupInOrg.descriptor } } } else { throw "Undefined object type $objectType" } } } #endregion devops #endregion get AAD account occurrences Write-Progress -Completed -Activity $progressActivity $result } } function Get-AzureAppConsentRequest { <# .SYNOPSIS Function for getting AzureAD app consent requests. .DESCRIPTION Function for getting AzureAD app consent requests. .PARAMETER header Graph api authentication header. Can be create via New-GraphAPIAuthHeader. .PARAMETER openAdminConsentPage Switch for opening web page with form for granting admin consent for each not yet review application. .EXAMPLE $header = New-GraphAPIAuthHeader Get-AzureAppConsentRequest -header $header .NOTES Requires at least permission ConsentRequest.Read.All (to get requests), Directory.Read.All (to get service principal publisher) https://docs.microsoft.com/en-us/graph/api/appconsentapprovalroute-list-appconsentrequests?view=graph-rest-1.0&tabs=http https://docs.microsoft.com/en-us/graph/api/resources/consentrequests-overview?view=graph-rest-1.0 #> [CmdletBinding()] param ( $header, [switch] $openAdminConsentPage ) if (!$header) { try { $header = New-GraphAPIAuthHeader -ErrorAction Stop } catch { throw "Unable to retrieve authentication header for graph api. Create it using New-GraphAPIAuthHeader and pass it using header parameter" } } Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/identityGovernance/appConsent/appConsentRequests" -header $Header | % { $userConsentRequestsUri = $_.'userConsentRequests@odata.context' -replace [regex]::escape('$metadata#') Write-Verbose "Getting user consent requests via '$userConsentRequestsUri'" $userConsentRequests = Invoke-GraphAPIRequest -uri $userConsentRequestsUri -header $Header $userConsentRequests = $userConsentRequests | select status, reason, @{name = 'createdBy'; expression = { $_.createdBy.user.userPrincipalName } }, createdDateTime, @{name = 'approval'; expression = { $_.approval.steps | select @{name = 'reviewedBy'; expression = { $_.reviewedBy.userPrincipalName } }, reviewResult, reviewedDateTime, justification } }, @{name = 'RequestId'; expression = { $_.Id } } $appVerifiedPublisher = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=(appId%20eq%20%27$($_.appId)%27)&`$select=verifiedPublisher" -header $Header if ($appVerifiedPublisher | Get-Member | ? Name -EQ 'verifiedPublisher') { $appVerifiedPublisher = $appVerifiedPublisher.verifiedPublisher.DisplayName } else { # service principal wasn't found (new application) $appVerifiedPublisher = "*unknown*" } $_ | select appDisplayName, consentType, @{name = 'verifiedPublisher'; expression = { $appVerifiedPublisher } }, @{name = 'pendingScopes'; e = { $_.pendingScopes.displayName } }, @{name = 'consentRequest'; expression = { $userConsentRequests } } if ($openAdminConsentPage -and $userConsentRequests.status -eq 'InProgress') { Open-AzureAdminConsentPage -appId $_.appId } } } function Get-AzureAppRegistration { <# .SYNOPSIS Function for getting Azure AD App registration(s) as can be seen in Azure web portal. .DESCRIPTION Function for getting Azure AD App registration(s) as can be seen in Azure web portal. App registrations are global app representations with unique ID across all tenants. Enterprise app is then its local representation for specific tenant. .PARAMETER objectId (optional) objectID of app registration. If not specified, all app registrations will be processed. .PARAMETER data Type of extra data you want to get. Possible values: - owner get service principal owner - permission get delegated (OAuth2PermissionGrants) and application (AppRoleAssignments) permissions - users&Groups get explicit Users and Groups roles (omits users and groups listed because they gave permission consent) By default all these possible values are selected (this can take several minutes!). .EXAMPLE Get-AzureAppRegistration Get all data for all AzureAD application registrations. .EXAMPLE Get-AzureAppRegistration -objectId 1234-1234-1234 -data 'owner' Get basic + owner data for selected AzureAD application registration. #> [CmdletBinding()] param ( [string] $objectId, [ValidateSet('owner', 'permission', 'users&Groups')] [string[]] $data = ('owner', 'permission', 'users&Groups') ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } $param = @{} if ($objectId) { $param.ApplicationId = $objectId } else { $param.All = $true } if ($data -contains 'owner') { $param.ExpandProperty = 'Owners' } Get-MgApplication @param | % { $appObj = $_ $appName = $appObj.DisplayName $appID = $appObj.AppId Write-Verbose "Processing $appName" Write-Verbose "Getting corresponding Service Principal" $SPObject = Get-MgServicePrincipal -Filter "AppId eq '$appID'" $SPObjectId = $SPObject.Id if ($SPObjectId) { Write-Verbose " - found service principal (enterprise app) with objectId: $SPObjectId" $appObj | Add-Member -MemberType NoteProperty -Name AppRoleAssignmentRequired -Value $SPObject.AppRoleAssignmentRequired } else { Write-Warning "Registered app '$appName' doesn't have corresponding service principal (enterprise app)" } if ($data -contains 'owner') { $appObj = $appObj | select *, @{n = 'Owners'; e = { $appObj.Owners | Expand-MgAdditionalProperties } } -ExcludeProperty 'Owners' } if ($data -contains 'permission') { Write-Verbose "Getting permission grants" if ($SPObjectId) { $SPPermission = Get-AzureServicePrincipalPermissions -objectId $SPObjectId } else { Write-Verbose "Unable to get permissions because corresponding ent. app is missing" $SPPermission = $null } $appObj | Add-Member -MemberType NoteProperty -Name Permission_AdminConsent -Value ($SPPermission | ? { $_.ConsentType -eq "AllPrincipals" -or $_.PermissionType -eq 'Application' } | select Permission, ResourceDisplayName, PermissionDisplayName, PermissionType) $appObj | Add-Member -MemberType NoteProperty -Name Permission_UserConsent -Value ($SPPermission | ? { $_.PermissionType -eq 'Delegated' -and $_.ConsentType -ne "AllPrincipals" } | select Permission, ResourceDisplayName, PermissionDisplayName, PrincipalObjectId, PrincipalDisplayName, PermissionType) } if ($data -contains 'users&Groups') { Write-Verbose "Getting users&Groups assignments" if ($SPObjectId) { $appObj | Add-Member -MemberType NoteProperty -Name UsersAndGroups -Value (Get-AzureServicePrincipalUsersAndGroups -objectId $SPObjectId | select * -ExcludeProperty AppRoleId, DeletedDateTime, ObjectType, Id, ResourceId, ResourceDisplayName, AdditionalProperties) } else { Write-Verbose "Unable to get role assignments because corresponding ent. app is missing" } } $appObj | Add-Member -MemberType NoteProperty -Name EnterpriseAppId -Value $SPObjectId # expired secret? $expiredPasswordCredentials = $appObj.PasswordCredentials | ? { $_.EndDate -and ($_.EndDate -le (Get-Date) -and !($appObj.PasswordCredentials.EndDate -gt (Get-Date))) } if ($expiredPasswordCredentials) { $expiredPasswordCredentials = $true } else { if ($appObj.PasswordCredentials) { $expiredPasswordCredentials = $false } else { $expiredPasswordCredentials = $null } } $appObj | Add-Member -MemberType NoteProperty -Name ExpiredPasswordCredentials -Value $expiredPasswordCredentials # expired certificate? $expiredKeyCredentials = $appObj.KeyCredentials | ? { $_.EndDate -and ($_.EndDate -le (Get-Date) -and !($appObj.KeyCredentials.EndDate -gt (Get-Date))) } if ($expiredKeyCredentials) { $expiredKeyCredentials = $true } else { if ($appObj.KeyCredentials) { $expiredKeyCredentials = $false } else { $expiredKeyCredentials = $null } } $appObj | Add-Member -MemberType NoteProperty -Name ExpiredKeyCredentials -Value $expiredKeyCredentials #endregion add secret(s) # output $appObj } } function Get-AzureAppVerificationStatus { param ( [Parameter(Mandatory = $false, ParameterSetName = "entApp")] [string] $servicePrincipalObjectId, [Parameter(Mandatory = $false, ParameterSetName = "appReg")] [string] $appRegObjectId, $header ) if (!$header) { try { $header = New-GraphAPIAuthHeader -ErrorAction Stop } catch { throw "Unable to retrieve authentication header for graph api. Create it using New-GraphAPIAuthHeader and pass it using header parameter" } } if ($appRegObjectId) { $URL = "https://graph.microsoft.com/v1.0/applications/$appRegObjectId`?`$select=displayName,verifiedPublisher" } elseif ($servicePrincipalObjectId) { $URL = "https://graph.microsoft.com/v1.0/servicePrincipals/$servicePrincipalObjectId`?`$select=displayName,verifiedPublisher" } else { $URL = "https://graph.microsoft.com/v1.0/servicePrincipals?`$select=displayName,verifiedPublisher" } Invoke-GraphAPIRequest -uri $URL -header $header | select displayName, @{name = 'publisherName'; expression = { $_.verifiedPublisher.displayName } }, @{name = 'publisherId'; expression = { $_.verifiedPublisher.verifiedPublisherId } }, @{name = 'publisherAdded'; expression = { Get-Date $_.verifiedPublisher.addedDateTime } } } function Get-AzureAssessNotificationEmail { <# .SYNOPSIS Function returns email(s) of organization technical contact(s) and privileged roles members. .DESCRIPTION Function returns email(s) of organization technical contact(s) and privileged roles members. .EXAMPLE $authHeader = New-GraphAPIAuthHeader Get-AzureAssessNotificationEmail -authHeader $authHeader .NOTES Stolen from Get-AADAssessNotificationEmailsReport function (module AzureADAssessment) #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $authHeader ) #region get Organization Technical Contacts $OrganizationData = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/organization?`$select=technicalNotificationMails" -header $authHeader if ($OrganizationData) { foreach ($technicalNotificationMail in $OrganizationData.technicalNotificationMails) { $result = [PSCustomObject]@{ notificationType = "Technical Notification" notificationScope = "Tenant" recipientType = "emailAddress" recipientEmail = $technicalNotificationMail recipientEmailAlternate = $null recipientId = $null recipientUserPrincipalName = $null recipientDisplayName = $null } $user = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,userPrincipalName,displayName,mail,otherMails,proxyAddresses&`$filter=proxyAddresses/any(c:c eq 'smtp:$technicalNotificationMail') or otherMails/any(c:c eq 'smtp:$technicalNotificationMail')" -header $authHeader | Select-Object -First 1 } if ($user) { $result.recipientType = 'user' $result.recipientId = $user.id $result.recipientUserPrincipalName = $user.userPrincipalName $result.recipientDisplayName = $user.displayName $result.recipientEmailAlternate = $user.otherMails -join ';' } $group = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=proxyAddresses/any(c:c eq 'smtp:$technicalNotificationMail')" -header $authHeader | Select-Object -First 1 if ($group) { $result.recipientType = 'group' $result.recipientId = $group.id $result.recipientDisplayName = $group.displayName } Write-Output $result } #endregion get Organization Technical Contacts #region get email addresses of all users with privileged roles $DirectoryRoleData = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/directoryRoles?`$select=id,displayName&`$expand=members" -header $authHeader foreach ($role in $DirectoryRoleData) { foreach ($roleMember in $role.members) { $member = $null if ($roleMember.'@odata.type' -eq '#microsoft.graph.user') { $member = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,userPrincipalName,displayName,mail,otherMails,proxyAddresses&`$filter=id eq '$($roleMember.id)'" -header $authHeader | Select-Object -First 1 } elseif ($roleMember.'@odata.type' -eq '#microsoft.graph.group') { $member = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName,mail,proxyAddresses&`$filter=id eq '$($roleMember.id)'" -header $authHeader | Select-Object -First 1 } elseif ($roleMember.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { $member = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=id,displayName&`$filter=id eq '$($roleMember.id)'" -header $authHeader | Select-Object -First 1 } else { Write-Error "Undefined type $($roleMember.'@odata.type')" } [PSCustomObject]@{ notificationType = $role.displayName notificationScope = 'Role' recipientType = ($roleMember.'@odata.type') -replace '#microsoft.graph.', '' recipientEmail = ($member.'mail') recipientEmailAlternate = ($member.'otherMails') -join ';' recipientId = ($member.'id') recipientUserPrincipalName = ($member.'userPrincipalName') recipientDisplayName = ($member.'displayName') } } } #endregion get email addresses of all users with privileged roles } function Get-AzureAuthenticatorLastUsedDate { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]]$upnList ) foreach ($upn in $upnList) { # filter is case sensitive in Get-MgAuditLogSignIn and UPNs seems to be always in lower case $upn = $upn.tolower() Write-Warning "Processing $upn" $mfaMethod = Get-MgBetaUserAuthenticationMethod -UserId $upn | Expand-MgAdditionalProperties $mobileAuthenticatorList = $mfaMethod | ? ObjectType -EQ "microsoftAuthenticatorAuthenticationMethod" if (!$mobileAuthenticatorList) { Write-Warning "$upn doesn't have an authenticator app set" continue } if ($mobileAuthenticatorList.count -lt 2) { Write-Warning "$upn doesn't have more than one authenticator set" continue } $mobileAuthenticatorList = $mobileAuthenticatorList | select *, @{n = 'LastTimeUsedUTC'; e = { $null } }, @{n = 'OperatingSystem'; e = { $null } } -ExcludeProperty '@odata.type', 'ObjectType' # get all successfully completed MFA prompts # 0 = Success # 50140 = "This occurred due to 'Keep me signed in' interrupt when the user was signing in." $successfulMFAPrompt = Get-MgBetaAuditLogSignIn -all -Filter "UserPrincipalName eq '$upn' and AuthenticationRequirement eq 'multiFactorAuthentication' and conditionalAccessStatus eq 'success'" -Property * | ? { $_.Status.ErrorCode -in 0, 50140 -and ($_.AuthenticationDetails.AuthenticationStepResultDetail | % { if ($_ -in 'MFA successfully completed', 'MFA completed in Azure AD', 'User approved', 'MFA required in Azure AD', 'MFA requirement satisfied by strong authentication') { $true } }) } if (!$successfulMFAPrompt) { Write-Warning "No completed MFA prompts found (in last 30 days?)" } else { foreach ($mfaPrompt in $successfulMFAPrompt) { if ($mobileAuthenticatorList.count -eq ($mobileAuthenticatorList.LastTimeUsedUTC | ? { $_ }).count) { # I have last used date for each registered authenticator Write-Verbose "I have LastTimeUsedUTC for each authenticator app" break } # "### $($mfaPrompt.AppDisplayName)" $mobileAuthenticatorId = $mfaPrompt.AuthenticationAppDeviceDetails.DeviceId # je ve skutecnosti ID z Get-MgUserAuthenticationMethod if (!$mobileAuthenticatorId) { Write-Verbose "This isn't event where authenticator was used, skipping" continue } $correspondingAuthenticator = $mobileAuthenticatorList | ? Id -EQ $mobileAuthenticatorId if (!$correspondingAuthenticator) { Write-Verbose "Authenticator with ID $mobileAuthenticatorId doesn't exist anymore" } else { if ($correspondingAuthenticator.LastTimeUsedUTC) { Write-Verbose "$mobileAuthenticatorId was already processed" continue } else { Write-Verbose "$mobileAuthenticatorId setting LastTimeUsedUTC $($mfaPrompt.CreatedDateTime) OperatingSystem $($mfaPrompt.AuthenticationAppDeviceDetails.OperatingSystem)" $correspondingAuthenticator.LastTimeUsedUTC = $mfaPrompt.CreatedDateTime $correspondingAuthenticator.OperatingSystem = $mfaPrompt.AuthenticationAppDeviceDetails.OperatingSystem } } } } #TODO u authenticatoru bez udaju zjistit kdy se zaregistroval, mozna je novy a jeste ho nepouzil [PSCustomObject]@{ UPN = $upn MobileAuthenticator = $mobileAuthenticatorList } } } function Get-AzureCompletedMFAPrompt { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]]$upnList ) foreach ($upn in $upnList) { # filter is case sensitive in Get-MgAuditLogSignIn $upn = $upn.tolower() Write-Warning "Processing $upn" $mfaMethod = Get-MgBetaUserAuthenticationMethod -UserId $upn | Expand-MgAdditionalProperties # get all successfully completed MFA prompts # 0 = Success # 50140 = "This occurred due to 'Keep me signed in' interrupt when the user was signing in." $successfulMFAPrompt = Get-MgBetaAuditLogSignIn -all -Filter "UserPrincipalName eq '$upn' and AuthenticationRequirement eq 'multiFactorAuthentication' and conditionalAccessStatus eq 'success'" -Property * | ? { $_.Status.ErrorCode -in 0, 50140 -and ($_.AuthenticationDetails.AuthenticationStepResultDetail | % { if ($_ -in 'MFA successfully completed', 'MFA completed in Azure AD', 'User approved', 'MFA required in Azure AD', 'MFA requirement satisfied by strong authentication') { $true } }) } if (!$successfulMFAPrompt) { Write-Warning "No completed MFA prompts found" continue } foreach ($mfaPrompt in $successfulMFAPrompt) { $authenticationMethod = $mfaPrompt.AuthenticationDetails | ? { $_.AuthenticationMethod -notin "Previously satisfied", "Password" -and $_.Succeeded -eq $true } if ($authenticationMethod) { $authMethod = $authenticationMethod.AuthenticationMethod if (!$authMethod) { # sometimes AuthenticationMethod is empty, but AuthenticationStepResultDetail contains 'MFA completed in Azure AD' $authMethod = $authenticationMethod.AuthenticationStepResultDetail } $authDetail = $authenticationMethod.AuthenticationMethodDetail if (!$authDetail -and $mfaPrompt.AuthenticationAppDeviceDetails.DeviceId) { $authDetail = $mfaPrompt.AuthenticationAppDeviceDetails } } else { $authMethod = $mfaPrompt.MfaDetail.AuthMethod $authDetail = $mfaPrompt.MfaDetail.AuthDetail } [PSCustomObject]@{ UPN = $upn CreatedDateTimeUTC = $mfaPrompt.CreatedDateTime AuthMethod = $authMethod AuthDetail = $authDetail AuthDeviceId = $mfaPrompt.AuthenticationAppDeviceDetails.DeviceId AuditEvent = $mfaPrompt } } } } function Get-AzureDeviceWithoutBitlockerKey { [CmdletBinding()] param () Get-BitlockerEscrowStatusForAzureADDevices | ? { $_.BitlockerKeysUploadedToAzureAD -eq $false -and $_.userPrincipalName -and $_.lastSyncDateTime -and $_.isEncrypted } } function Get-AzureEnterpriseApplication { <# .SYNOPSIS Function for getting Azure AD Service Principal(s) \ Enterprise Application(s) as can be seen in Azure web portal. .DESCRIPTION Function for getting Azure AD Service Principal(s) \ Enterprise Application(s) as can be seen in Azure web portal. .PARAMETER objectId (optional) objectID(s) of Service Principal(s) \ Enterprise Application(s). If not specified, all enterprise applications will be processed. .PARAMETER data Type of extra data you want to get to the ones returned by Get-AzureServicePrincipal. Possible values: - owner get service principal owner - permission get delegated (OAuth2PermissionGrants) and application (AppRoleAssignments) permissions - users&Groups get explicit Users and Groups roles (omits users and groups listed because they gave permission consent) By default all these possible values are selected (this can take several minutes!). .PARAMETER includeBuiltInApp Switch for including also builtin Azure apps. .PARAMETER excludeAppWithAppRegistration Switch for excluding enterprise app(s) for which exists corresponding app registration. .EXAMPLE Get-AzureEnterpriseApplication Get all data for all AzureAD enterprise applications. Builtin apps are excluded. .EXAMPLE Get-AzureEnterpriseApplication -excludeAppWithAppRegistration Get all data for all AzureAD enterprise applications. Builtin apps and apps for which app registration exists are excluded. .EXAMPLE Get-AzureEnterpriseApplication -objectId 1234-1234-1234 -data 'owner' Get basic + owner data for selected AzureAD enterprise application. .NOTES TO be able to retrieve security custom attributes, you need to be member of the "Attribute Assignment Reader" group! #> [CmdletBinding()] param ( [string[]] $objectId, [ValidateSet('owner', 'permission', 'users&Groups')] [string[]] $data = ('owner', 'permission', 'users&Groups'), [switch] $includeBuiltInApp, [switch] $excludeAppWithAppRegistration ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } # to get custom security attributes $servicePrincipalList = $null if ($data -contains 'permission' -and !$objectId -and $includeBuiltInApp) { # it is much faster to get all SP permissions at once instead of one-by-one processing in foreach (thanks to caching) Write-Verbose "Getting granted permission(s)" $SPPermission = Get-AzureServicePrincipalPermissions } if (!$objectId) { $param = @{ Filter = "servicePrincipalType eq 'Application'" All = $true } if ($data -contains 'owner') { $param.ExpandProperty = 'owners' } $enterpriseApp = Get-MgServicePrincipal @param if ($excludeAppWithAppRegistration) { $appRegistrationObj = Get-MgApplication -All $enterpriseApp = $enterpriseApp | ? AppId -NotIn $appRegistrationObj.AppId } if (!$includeBuiltInApp) { # https://learn.microsoft.com/en-us/troubleshoot/azure/active-directory/verify-first-party-apps-sign-in # f8cdef31-a31e-4b4a-93e4-5f571e91255a is the Microsoft Service's Azure AD tenant ID # $enterpriseApp = $enterpriseApp | ? AppOwnerOrganizationId -NE "f8cdef31-a31e-4b4a-93e4-5f571e91255a" $enterpriseApp = $enterpriseApp | ? tags -Contains 'WindowsAzureActiveDirectoryIntegratedApp' } $servicePrincipalList = $enterpriseApp } else { $objectId | % { $param = @{ ServicePrincipalId = $_ } if ($data -contains 'owner') { $param.ExpandProperty = 'owners' } $servicePrincipalList += Get-MgServicePrincipal @param } } $servicePrincipalList | ? { $_ } | % { $SPObj = $_ Write-Verbose "Processing '$($SPObj.DisplayName)' ($($SPObj.Id))" # fill CustomSecurityAttributes attribute (easier this way then explicitly specifying SELECT) # membership in role "Attribute Assignment Reader" is needed! $SPObj.CustomSecurityAttributes = Get-MgBetaServicePrincipal -ServicePrincipalId $SPObj.Id -Select CustomSecurityAttributes | select -ExpandProperty CustomSecurityAttributes #| Expand-MgAdditionalProperties if ($data -contains 'owner') { $SPObj = $SPObj | select *, @{n = 'Owners'; e = { $SPObj.Owners | Expand-MgAdditionalProperties } } -ExcludeProperty 'Owners' } if ($data -contains 'permission') { Write-Verbose "Getting permission grants" if ($SPPermission) { $permission = $SPPermission | ? ClientObjectId -EQ $SPObj.Id } else { $permission = Get-AzureServicePrincipalPermissions -objectId $SPObj.Id } $SPObj | Add-Member -MemberType NoteProperty -Name Permission_AdminConsent -Value ($permission | ? { $_.ConsentType -eq "AllPrincipals" -or $_.PermissionType -eq 'Application' } | select Permission, ResourceDisplayName, PermissionDisplayName, PermissionType) $SPObj | Add-Member -MemberType NoteProperty -Name Permission_UserConsent -Value ($permission | ? { $_.PermissionType -eq 'Delegated' -and $_.ConsentType -ne "AllPrincipals" } | select Permission, ResourceDisplayName, PermissionDisplayName, PrincipalObjectId, PrincipalDisplayName, PermissionType) } if ($data -contains 'users&Groups') { Write-Verbose "Getting users&Groups assignments" $SPObj | Add-Member -MemberType NoteProperty UsersAndGroups -Value (Get-AzureServicePrincipalUsersAndGroups -objectId $SPObj.Id | select * -ExcludeProperty AppRoleId, DeletedDateTime, ObjectType, Id, ResourceId, ResourceDisplayName, AdditionalProperties) } # expired secret? $expiredCertificate = $SPObj.PasswordCredentials | ? { $_.EndDate -and ($_.EndDate -le (Get-Date) -and !($SPObj.PasswordCredentials.EndDate -gt (Get-Date))) } if ($expiredSecret) { $expiredSecret = $true } else { if ($SPObj.PasswordCredentials) { $expiredSecret = $false } else { $expiredSecret = $null } } $SPObj | Add-Member -MemberType NoteProperty ExpiredSecret -Value $expiredSecret # expired certificate? $expiredCertificate = $SPObj.KeyCredentials | ? { $_.EndDate -and ($_.EndDate -le (Get-Date) -and !($SPObj.KeyCredentials.EndDate -gt (Get-Date))) } if ($expiredCertificate) { $expiredCertificate = $true } else { if ($SPObj.KeyCredentials) { $expiredCertificate = $false } else { $expiredCertificate = $null } } $SPObj | Add-Member -MemberType NoteProperty expiredCertificate -Value $expiredCertificate # output $SPObj } } function Get-AzureGroupMemberRecursive { <# .SYNOPSIS Function for getting Azure group members recursively. .DESCRIPTION Function for getting Azure group members recursively. Some advanced filtering options are available. .PARAMETER id Id of the group whose members you want to retrieve. .PARAMETER excludeDisabled Switch for excluding disabled members from the output. .PARAMETER includeNestedGroup Switch for including nested groups in the output (otherwise just their members will be included). .PARAMETER allowedMemberType What type of members should be outputted. Available options: 'User', 'Device', 'All'. By default 'All'. .EXAMPLE Get-AzureGroupMemberRecursive -groupId 330a6343-da12-4999-bf87-a0ae60a68bbc .NOTES Requires following graph modules: Microsoft.Graph.Groups, Microsoft.Graph.Authentication, Microsoft.Graph.DirectoryObjects #> [Alias("Get-MgGroupMemberRecursive")] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [Alias("GroupId")] [guid] $id, [switch] $excludeDisabled, [switch] $includeNestedGroup, [ValidateSet('User', 'Device', 'All')] [string] $allowedMemberType = 'All' ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } foreach ($member in (Get-MgGroupMember -GroupId $id -All)) { $memberType = $member.AdditionalProperties["@odata.type"].split('.')[-1] $memberId = $member.Id if ($memberType -eq "group") { if ($includeNestedGroup) { $member | Expand-MgAdditionalProperties } $param = @{ allowedMemberType = $allowedMemberType } if ($includeDisabled) { $param.includeDisabled = $true } Write-Verbose "Expanding members of group $memberId" Get-AzureGroupMemberRecursive -Id $memberId @param } else { if ($allowedMemberType -ne 'All' -and $memberType -ne $allowedMemberType) { Write-Verbose "Skipping $memberType member $memberId, because not of $allowedMemberType type." continue } if ($excludeDisabled) { $accountEnabled = (Get-MgDirectoryObject -DirectoryObjectId $memberId -Property accountEnabled).AdditionalProperties.accountEnabled if (!$accountEnabled) { Write-Verbose "Skipping $memberType member $memberId, because not enabled." continue } } $member | Expand-MgAdditionalProperties } } } function Get-AzureGroupSettings { <# .SYNOPSIS Function for getting group settings. Official Get-MgGroup -Property Settings doesn't return anything for some reason. .DESCRIPTION Function for getting group settings. Official Get-MgGroup -Property Settings doesn't return anything for some reason. .PARAMETER groupId Group ID. .EXAMPLE Get-AzureGroupSettings -groupId 01c19ec3-e1bb-44f3-ab36-86071b745375 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $groupId ) Invoke-MgGraphRequest -Uri "v1.0/groups/$groupId/settings" -OutputType PSObject | select -exp value | select *, @{n = 'ValuesAsObject'; e = { # return settings values as proper hashtable $hash = @{} $_.Values | % { $hash.($_.name) = $_.value } $hash } } #-ExcludeProperty Values } function Get-AzureManagedIdentity { <# .SYNOPSIS Function for getting Azure AD Managed Identity(ies). .DESCRIPTION Function for getting Azure AD Managed Identity(ies). .PARAMETER objectId (optional) objectID of Managed Identity(ies). If not specified, all app registrations will be processed. .EXAMPLE Get-AzureManagedIdentity Get all Managed Identities. .EXAMPLE Get-AzureManagedIdentity -objectId 1234-1234-1234 Get selected Managed Identity. #> [CmdletBinding()] param ( [string[]] $objectId ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } $servicePrincipalList = @() if (!$objectId) { $servicePrincipalList = Get-MgServicePrincipal -Filter "servicePrincipalType eq 'ManagedIdentity'" -All } else { $objectId | % { $servicePrincipalList += Get-MgServicePrincipal -ServicePrincipalId $_ } } $azureSubscriptions = Get-AzSubscription $servicePrincipalList | % { $SPObj = $_ # output $SPObj | select *, @{n = 'SubscriptionId'; e = { $_.alternativeNames | ? { $_ -Match "/subscriptions/([^/]+)/" } | % { ([regex]"/subscriptions/([^/]+)/").Matches($_).captures.groups[1].value } } }, @{name = 'SubscriptionName'; expression = { $alternativeNames = $_.alternativeNames; $azureSubscriptions | ? { $_.Id -eq ($alternativeNames | ? { $_ -Match "/subscriptions/([^/]+)/" } | % { ([regex]"/subscriptions/([^/]+)/").Matches($_).captures.groups[1].value }) } | select -exp Name } }, @{n = 'ResourceGroup'; e = { $_.alternativeNames | ? { $_ -Match "/resourcegroups/([^/]+)/" } | % { ([regex]"/resourcegroups/([^/]+)/").Matches($_).captures.groups[1].value } } }, @{n = 'Type'; e = { if ($_.alternativeNames -match "/Microsoft.ManagedIdentity/userAssignedIdentities/") { 'UserManagedIdentity' } else { 'SystemManagedIdentity' } } } } } function Get-AzureResource { <# .SYNOPSIS Returns resources for all or just selected Azure subscription(s). .DESCRIPTION Returns resources for all or just selected Azure subscription(s). .PARAMETER subscriptionId ID of subscription you want to get resources for. .PARAMETER selectCurrentSubscription Switch for getting data just for currently set subscription. .EXAMPLE Get-AzureResource Returns resources for all subscriptions. .EXAMPLE Get-AzureResource -subscriptionId 1234-1234-1234-1234 Returns resources for subscription with ID 1234-1234-1234-1234. .EXAMPLE Get-AzureResource -selectCurrentSubscription Returns resources just for current subscription. #> [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(ParameterSetName = "subscriptionId")] [string] $subscriptionId, [Parameter(ParameterSetName = "currentSubscription")] [switch] $selectCurrentSubscription ) # get Current Context $currentContext = Get-AzContext # get Azure Subscriptions if ($selectCurrentSubscription) { Write-Verbose "Only running for current subscription $($currentContext.Subscription.Name)" $subscriptions = Get-AzSubscription -SubscriptionId $currentContext.Subscription.Id -TenantId $currentContext.Tenant.Id } elseif ($subscriptionId) { Write-Verbose "Only running for selected subscription $subscriptionId" $subscriptions = Get-AzSubscription -SubscriptionId $subscriptionId -TenantId $currentContext.Tenant.Id } else { Write-Verbose "Running for all subscriptions in tenant" $subscriptions = Get-AzSubscription -TenantId $currentContext.Tenant.Id } Write-Verbose "Getting information about Role Definitions..." $allRoleDefinition = Get-AzRoleDefinition foreach ($subscription in $subscriptions) { Write-Verbose "Changing to Subscription $($subscription.Name)" $Context = Set-AzContext -TenantId $subscription.TenantId -SubscriptionId $subscription.Id -Force # getting information about Role Assignments for chosen subscription Write-Verbose "Getting information about Role Assignments..." $allRoleAssignment = Get-AzRoleAssignment Write-Verbose "Getting information about Resources..." Get-AzResource | % { $resourceId = $_.ResourceId Write-Verbose "Processing $resourceId" $roleAssignment = $allRoleAssignment | ? { $resourceId -match [regex]::escape($_.scope) -or $_.scope -like "/providers/Microsoft.Authorization/roleAssignments/*" -or $_.scope -like "/providers/Microsoft.Management/managementGroups/*" } | select RoleDefinitionName, DisplayName, Scope, SignInName, ObjectType, ObjectId, @{n = 'CustomRole'; e = { ($allRoleDefinition | ? Name -EQ $_.RoleDefinitionName).IsCustom } }, @{n = 'Inherited'; e = { if ($_.scope -eq $resourceId) { $false } else { $true } } } $_ | select *, @{n = "SubscriptionName"; e = { $subscription.Name } }, @{n = "SubscriptionId"; e = { $subscription.SubscriptionId } }, @{n = 'IAM'; e = { $roleAssignment } } -ExcludeProperty SubscriptionId, ResourceId, ResourceType } } } function Get-AzureRoleAssignments { <# .SYNOPSIS Returns RBAC role assignments (IAM tab for root, subscriptions, management groups, resource groups, resources) from all or just selected Azure subscription(s). It is possible to filter just roles assigned to user, group or service principal. .DESCRIPTION Returns RBAC role assignments (IAM tab for root, subscriptions, management groups, resource groups, resources) from all or just selected Azure subscription(s). It is possible to filter just roles assigned to user, group or service principal. From security perspective these roles are important: Owner Contributor User Access Administrator Virtual Machine Contributor Virtual Machine Administrator Avere Contributor When given to managed identity and scope is whole resource group or subscription (because of lateral movement)! .PARAMETER subscriptionId ID of subscription you want to get role assignments for. .PARAMETER selectCurrentSubscription Switch for getting data just for currently set subscription. .PARAMETER userPrincipalName UPN of the User whose assignments you want to get. .PARAMETER objectId ObjectId of the User, Group or Service Principal whose assignments you want to get. .PARAMETER tenantId Tenant ID if different then the default one should be used. .EXAMPLE Get-AzureRoleAssignments Returns RBAC role assignments for all subscriptions. .EXAMPLE Get-AzureRoleAssignments -subscriptionId 1234-1234-1234-1234 Returns RBAC role assignments for subscription with ID 1234-1234-1234-1234. .EXAMPLE Get-AzureRoleAssignments -selectCurrentSubscription Returns RBAC role assignments just for current subscription. .EXAMPLE Get-AzureRoleAssignments -selectCurrentSubscription -userPrincipalName john@contoso.com Returns RBAC role assignments of the user john@contoso.com just for current subscription. .NOTES Required Azure permissions: - Global reader - Security Reader assigned at 'Tenant Root Group' https://m365internals.com/2021/11/30/lateral-movement-with-managed-identities-of-azure-virtual-machines/?s=09 https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles #> [CmdletBinding(DefaultParameterSetName = 'Default')] [Alias("Get-AzureRBACRoleAssignments", "Get-AzureIAMRoleAssignments")] param ( [Parameter(ParameterSetName = "subscriptionId")] [string] $subscriptionId, [Parameter(ParameterSetName = "currentSubscription")] [Switch] $selectCurrentSubscription, [string] $userPrincipalName, [string] $objectId, [string] $tenantId ) if ($objectId -and $userPrincipalName) { throw "You cannot use parameters objectId and userPrincipalName at the same time" } if ($tenantId) { $null = Connect-AzAccount2 -tenantId $tenantId -ErrorAction Stop } else { $null = Connect-AzAccount2 -ErrorAction Stop } # get Current Context $CurrentContext = Get-AzContext # get Azure Subscriptions if ($selectCurrentSubscription) { Write-Verbose "Only running for current subscription $($CurrentContext.Subscription.Name)" $Subscriptions = Get-AzSubscription -SubscriptionId $CurrentContext.Subscription.Id -TenantId $CurrentContext.Tenant.Id } elseif ($subscriptionId) { Write-Verbose "Only running for selected subscription $subscriptionId" $Subscriptions = Get-AzSubscription -SubscriptionId $subscriptionId -TenantId $CurrentContext.Tenant.Id } else { Write-Verbose "Running for all subscriptions in tenant" $Subscriptions = Get-AzSubscription -TenantId $CurrentContext.Tenant.Id } function _scopeType { param ([string] $scope) if ($scope -match "^/$") { return 'root' } elseif ($scope -match "^/subscriptions/[^/]+$") { return 'subscription' } elseif ($scope -match "^/subscriptions/[^/]+/resourceGroups/[^/]+$") { return "resourceGroup" } elseif ($scope -match "^/subscriptions/[^/]+/resourceGroups/[^/]+/.+$") { return 'resource' } elseif ($scope -match "^/providers/Microsoft.Management/managementGroups/.+") { return 'managementGroup' } else { throw 'undefined type' } } Write-Verbose "Getting Role Definitions..." $roleDefinition = Get-AzRoleDefinition foreach ($Subscription in ($Subscriptions | Sort-Object Name)) { Write-Verbose "Changing to Subscription $($Subscription.Name) ($($Subscription.SubscriptionId))" $Context = Set-AzContext -TenantId $Subscription.TenantId -SubscriptionId $Subscription.Id -Force # getting information about Role Assignments for chosen subscription Write-Verbose "Getting information about Role Assignments..." try { $param = @{ ErrorAction = 'Stop' } if ($objectId) { $param.objectId = $objectId } elseif ($userPrincipalName) { # -ExpandPrincipalGroups for also assignments based on group membership $param.SignInName = $userPrincipalName } Get-AzRoleAssignment @param | Select-Object RoleDefinitionName, DisplayName, SignInName, ObjectType, ObjectId, @{n = 'AssignmentScope'; e = { $_.Scope } }, @{n = "SubscriptionId"; e = { $Subscription.SubscriptionId } }, @{n = 'ScopeType'; e = { _scopeType $_.scope } }, @{n = 'CustomRole'; e = { ($roleDefinition | ? { $_.Name -eq $_.RoleDefinitionName }).IsCustom } }, @{n = "SubscriptionName"; e = { $Subscription.Name } } } catch { if ($_ -match "The current subscription type is not permitted to perform operations on any provider namespace. Please use a different subscription") { Write-Warning "At subscription '$($Subscription.Name)' there is no resource provider registered" } elseif ($_ -match "Operation returned an invalid status code 'BadRequest'") { Write-Warning "You don't have permissions at '$($Subscription.Name)' subscription" } else { Write-Error $_ } } } } function Get-AzureServiceAccount { <# .SYNOPSIS Function for getting information about Azure user service account. As a hack for storing user manager and description, we use helper ACL group 'ACL_Owner_<svcAccID>'. .DESCRIPTION Function for getting information about Azure user service account. As a hack for storing user manager and description, we use helper ACL group 'ACL_Owner_<svcAccID>'. .PARAMETER UPN UPN of the service account. For exmaple: svc_test@contoso.onmicrosoft.com .EXAMPLE Get-AzureServiceAccount -UPN svc_test@contoso.onmicrosoft.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidatePattern('.+@.+$')] [string] $UPN ) $ErrorActionPreference = "Stop" $null = Connect-MgGraph -Scopes User.Read.All, Group.Read.All # check that such user does exist if (!($svcUser = Get-MgUser -Filter "userPrincipalName eq '$UPN'")) { Write-Warning "User $UPN doesn't exists" } $groupName = "ACL_Owner_" + $svcUser.Id if (!($svcGroup = Get-MgGroup -Filter "displayName eq '$groupName'")) { Write-Warning "Group $groupName doesn't exists. This shouldn't happen!" } if ($svcGroup) { $managedBy = Get-MgGroupMember -GroupId $svcGroup.Id if ($managedBy.count -gt 1) { Write-Warning "There is more than one manager. This shouldn't happen!" } } $object = [PSCustomObject]@{ userPrincipalName = $UPN Description = $svcGroup.Description ManagedByObjectId = $managedBy.Id ManagedBy = $managedBy.AdditionalProperties.displayName } return $object } function Get-AzureServicePrincipalBySecurityAttribute { <# .SYNOPSIS Function returns service principals with given security attribute set. .DESCRIPTION Function returns service principals with given security attribute set. .PARAMETER attributeSetName Name of the security attribute set. .PARAMETER attributeName Name of the security attribute. .PARAMETER attributeValue Value of the security attribute. .EXAMPLE Get-AzureServicePrincipalBySecurityAttribute -attributeSetName Security -attributeName SecurityLevel -attributeValue 5 .NOTES To be able to read security attributes you need to be member of 'Attribute Assignment Reader' or 'Attribute Assignment Administrator' or have following Graph API permissions. For SP 'CustomSecAttributeAssignment.Read.All' and 'Application.Read.All', for Users 'CustomSecAttributeAssignment.Read.All' and 'User.Read.All' https://learn.microsoft.com/en-us/graph/custom-security-attributes-examples?tabs=powershell #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $attributeSetName, [Parameter(Mandatory = $true)] [string] $attributeName, [Parameter(Mandatory = $true)] [string[]] $attributeValue ) Write-Warning "To be able to read security attributes you need to be member of 'Attribute Assignment Reader' or 'Attribute Assignment Administrator' or have following Graph API permissions. For SP 'CustomSecAttributeAssignment.Read.All' and 'Application.Read.All', for Users 'CustomSecAttributeAssignment.Read.All' and 'User.Read.All'" # beta api is needed to get custom security attributes $filter = @() $attributeValue | % { $filter += "customSecurityAttributes/$attributeSetName/$attributeName eq '$_'" } $filter = $filter -join " or " Get-MgBetaServicePrincipal -All -Filter $filter -Property AppId, Id, AppDisplayName, AccountEnabled, DisplayName, CustomSecurityAttributes -ConsistencyLevel eventual -CountVariable CountVar -ErrorAction Stop } function Get-AzureServicePrincipalOverview { <# .SYNOPSIS Function for getting overall information for AzureAD Service principal(s). .DESCRIPTION Function for getting overall information for AzureAD Service principal(s). Basic information gathered using Get-MgServicePrincipal command will be enriched with new properties partly by based on values in 'data' parameter. .PARAMETER objectId (optional) objectId of the service principal you want information for. .PARAMETER data Type of extra data you want to get. Possible values: - owner get service principal owner - output is saved in property: Owner - permission get delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments) - output is saved in properties: Permission_AdminConsent, Permission_UserConsent - users&Groups get explicit Users and Groups roles (omits users and groups listed because they gave permission consent) - output is saved in property: UsersAndGroups - lastUsed get last date this service principal was used according the audit logs - output is saved in property: lastUsed By default all these possible values are selected (this can take dozens of minutes!). .PARAMETER credential Credentials for AzureAD authentication. .PARAMETER header Header for authentication of graph calls. Use if calling Get-AzureServicePrincipalOverview several times in short time period. Otherwise you will end with error: We couldn't sign you in. Header object can be created via New-GraphAPIAuthHeader function. .EXAMPLE Get-AzureServicePrincipalOverview Get all data for all service principals. .EXAMPLE Get-AzureServicePrincipalOverview -objectId 1234-1234-1234 -data 'owner', 'permission' Get basic service principal data plus owner and permissions for SP with given objectId. .NOTES Nice similar solution https://github.com/michevnew/PowerShell/blob/master/app_Permissions_inventory_GraphAPI.ps1 #> [CmdletBinding()] param ( [string] $objectId, [ValidateSet('owner', 'permission', 'users&Groups', 'lastUsed')] [string[]] $data = ('owner', 'permission', 'users&Groups', 'lastUsed'), [System.Management.Automation.PSCredential] $credential, $header ) #region authenticate if ($credential) { Connect-AzAccount2 -credential $credential -ErrorAction Stop Connect-MgGraphViaCred -credential $credential -ErrorAction Stop } else { Connect-AzAccount2 -ErrorAction Stop $null = Connect-MgGraph -ErrorAction Stop } if (!$header) { $header = New-GraphAPIAuthHeader -ErrorAction Stop } #endregion authenticate if ($data -contains 'permission') { # it is much faster to get all SP permissions at once instead of one-by-one processing in foreach (thanks to caching) Write-Verbose "Getting granted permission(s)" $param = @{ ErrorAction = 'Continue' } if ($objectId) { $param.objectId = $objectId } $SPPermission = Get-AzureServicePrincipalPermissions @param } $param = @{} if ($objectId) { $param.ServicePrincipalId = $objectId } else { $param.All = $true } Get-MgServicePrincipal @param | % { $SP = $_ $SPName = $SP.AppDisplayName if (!$SPName) { $SPName = $SP.DisplayName } Write-Warning "Processing '$SPName' ($($SP.AppId))" if ($data -contains 'owner') { Write-Verbose "Getting owner" $SP = $SP | select *, @{n = 'Owner'; e = { Get-MgServicePrincipalOwner -ServicePrincipalId $_.Id -All | Expand-MgAdditionalProperties } } } if ($data -contains 'permission') { $permission = $SPPermission | ? ClientObjectId -EQ $SP.Id $SP = $SP | select *, @{n = 'Permission_AdminConsent'; e = { $permission | ? { $_.ConsentType -eq "AllPrincipals" -or $_.PermissionType -eq 'Application' } | select Permission, ResourceDisplayName, PermissionDisplayName, PermissionType } } $SP = $SP | select *, @{n = 'Permission_UserConsent'; e = { $permission | ? { $_.PermissionType -eq 'Delegated' -and $_.ConsentType -ne "AllPrincipals" } | select Permission, ResourceDisplayName, PermissionDisplayName, PrincipalObjectId, PrincipalDisplayName, PermissionType } } } if ($data -contains 'users&Groups') { Write-Verbose "Getting explicitly assigned users and groups" # show just explicitly added members, not added via granting consent $consentPrincipalId = @($SP.Permission_AdminConsent.PrincipalObjectId) + @($SP.Permission_UserConsent.PrincipalObjectId) $SP = $SP | select *, @{n = 'UsersAndGroups'; e = { Get-AzureAppUsersAndGroups -objectId $SP.Id | select CreatedDateTime, PrincipalDisplayName, PrincipalId, PrincipalType | ? PrincipalId -NotIn $consentPrincipalId } } } #region check secrets $sResult = @() $cResult = @() #region process secret(s) $secret = $SP.PasswordCredentials $cert = $SP.KeyCredentials foreach ($s in $secret) { $startDate = $s.StartDate $endDate = $s.EndDate $sResult += [PSCustomObject]@{ StartDate = $startDate EndDate = $endDate } } foreach ($c in $cert) { $startDate = $c.StartDate $endDate = $c.EndDate $cResult += [PSCustomObject]@{ StartDate = $startDate EndDate = $endDate } } #endregion process secret(s) # expired secret $expiredSecret = $sResult | ? { $_.EndDate -and ($_.EndDate -le (Get-Date) -and !($_.EndDate -gt (Get-Date))) } if ($expiredSecret) { $expiredSecret = $true } else { if ($sResult) { $expiredSecret = $false } else { $expiredSecret = $null } } # $SP = $SP | Add-Member -MemberType NoteProperty -Name ExpiredSecret -Value $expiredSecret $SP = $SP | select *, @{n = 'ExpiredSecret'; e = { $expiredSecret } } # expired certificate $expiredCertificate = $cResult | ? { $_.EndDate -and ($_.EndDate -le (Get-Date) -and !($_.EndDate -gt (Get-Date))) } if ($expiredCertificate) { $expiredCertificate = $true } else { if ($cResult) { $expiredCertificate = $false } else { $expiredCertificate = $null } } # $SP = $SP | Add-Member -MemberType NoteProperty -Name ExpiredCertificate -Value $expiredCertificate $SP = $SP | select *, @{n = 'ExpiredCertificate'; e = { $expiredCertificate } } #endregion check secrets if ($data -contains 'lastUsed') { Write-Verbose "Getting last used date" # Get-AzureADAuditSignInLogs has problems with throttling 'Too Many Requests', Invoke-GraphAPIRequest has builtin fix for that $signInResult = Invoke-GraphAPIRequest -uri "https://graph.microsoft.com/beta/auditLogs/signIns?api-version=beta&`$filter=(appId eq '$($SP.AppId)')&`$top=1&`$orderby=createdDateTime desc" -header $header if ($signInResult.count -ge 1) { $SP = $SP | select *, @{n = 'LastUsed'; e = { $signInResult.CreatedDateTime } } } else { $SP = $SP | select *, @{n = 'LastUsed'; e = { $null } } } } #output $SP } } function Get-AzureServicePrincipalPermissions { <# .SYNOPSIS Lists granted delegated (OAuth2PermissionGrants) and application (AppRoleAssignments) permissions of the service principal (ent. app). .PARAMETER objectId Service principal objectId. If not specified, all service principals will be processed. .PARAMETER DelegatedPermissions If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions switch is set, both application and delegated permissions will be returned. .PARAMETER ApplicationPermissions If set, will return application permissions. If neither this switch nor the DelegatedPermissions switch is set, both application and delegated permissions will be returned. .PARAMETER UserProperties The list of properties of user objects to include in the output. Defaults to DisplayName only. .PARAMETER ServicePrincipalProperties The list of properties of service principals (i.e. apps) to include in the output. Defaults to DisplayName only. .PARAMETER ShowProgress Whether or not to display a progress bar when retrieving application permissions (which could take some time). .PARAMETER PrecacheSize The number of users to pre-load into a cache. For tenants with over a thousand users, increasing this may improve performance of the script. .EXAMPLE PS C:\> Get-AzureServicePrincipalPermissions -objectId f1c5b03c-6605-46ac-8ddb-453b953af1fc Generates report of all permissions granted to app f1c5b03c-6605-46ac-8ddb-453b953af1fc. .EXAMPLE PS C:\> Get-AzureServicePrincipalPermissions | Export-Csv -Path "permissions.csv" -NoTypeInformation Generates a CSV report of all permissions granted to all apps. .EXAMPLE PS C:\> Get-AzureServicePrincipalPermissions -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } Get all apps which have application permissions for Directory.Read.All. .EXAMPLE PS C:\> Get-AzureServicePrincipalPermissions -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") Gets all permissions granted to all apps and includes additional properties for users and service principals. .NOTES https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants?view=o365-worldwide #> [CmdletBinding()] [Alias("Get-AzureSPPermissions")] param( [string] $objectId, [switch] $DelegatedPermissions, [switch] $ApplicationPermissions, [string[]] $UserProperties = @("DisplayName"), [string[]] $ServicePrincipalProperties = @("DisplayName"), [switch] $ShowProgress, [int] $PrecacheSize = 999 ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } # An in-memory cache of objects by {object ID} and by {object class, object ID} $script:ObjectByObjectId = @{} $script:ObjectByObjectClassId = @{} #region helper functions # Function to add an object to the cache function CacheObject ($Object, $ObjectType) { if ($Object) { if (-not $script:ObjectByObjectClassId.ContainsKey($ObjectType)) { $script:ObjectByObjectClassId[$ObjectType] = @{} } $script:ObjectByObjectClassId[$ObjectType][$Object.Id] = $Object $script:ObjectByObjectId[$Object.Id] = $Object } } # Function to retrieve an object from the cache (if it's there), or from Azure AD (if not). function GetObjectByObjectId ($ObjectId) { if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) { Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId) try { $object = Get-MgDirectoryObjectById -Ids $ObjectId | Expand-MgAdditionalProperties CacheObject -Object $object -ObjectType $object.ObjectType } catch { Write-Verbose "Object not found." $_ } } return $script:ObjectByObjectId[$ObjectId] } # Function to retrieve OAuth2PermissionGrants function GetOAuth2PermissionGrants { if ($objectId) { Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $objectId -All } else { Get-MgOauth2PermissionGrant -All } } #endregion helper functions $empty = @{} # Used later to avoid null checks # Get ServicePrincipal object(s) and add to the cache if ($objectId) { Write-Verbose "Retrieving $objectId ServicePrincipal object..." Get-MgServicePrincipal -ServicePrincipalId $objectId | ForEach-Object { CacheObject -Object $_ -ObjectType "servicePrincipal" } } else { Write-Verbose "Retrieving all ServicePrincipal objects..." Get-MgServicePrincipal -All | ForEach-Object { CacheObject -Object $_ -ObjectType "servicePrincipal" } } $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count if ($DelegatedPermissions -or (!$DelegatedPermissions -and !$ApplicationPermissions)) { # Get one page of User objects and add to the cache if (!$objectId) { Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize) Get-MgUser -Top $PrecacheSize | Where-Object { CacheObject -Object $_ -ObjectType "user" } } # Get all existing OAuth2 permission grants, get the client, resource and scope details Write-Verbose "Retrieving OAuth2PermissionGrants..." GetOAuth2PermissionGrants | ForEach-Object { $grant = $_ if ($grant.Scope) { $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { $scope = $_ $resource = GetObjectByObjectId -ObjectId $grant.ResourceId $permission = $resource.oauth2PermissionScopes | Where-Object { $_.value -eq $scope } $grantDetails = [ordered]@{ "PermissionType" = "Delegated" "ClientObjectId" = $grant.ClientId "ResourceObjectId" = $grant.ResourceId "GrantId" = $grant.Id "Permission" = $scope # "PermissionId" = $permission.Id "PermissionDisplayName" = $permission.adminConsentDisplayName "PermissionDescription" = $permission.adminConsentDescription "ConsentType" = $grant.ConsentType "PrincipalObjectId" = $grant.PrincipalId } # Add properties for client and resource service principals if ($ServicePrincipalProperties.Count -gt 0) { $client = GetObjectByObjectId -ObjectId $grant.ClientId $resource = GetObjectByObjectId -ObjectId $grant.ResourceId $insertAtClient = 2 $insertAtResource = 3 foreach ($propertyName in $ServicePrincipalProperties) { $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) $insertAtResource++ $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) $insertAtResource ++ } } # Add properties for principal (will all be null if there's no principal) if ($UserProperties.Count -gt 0) { $principal = $empty if ($grant.PrincipalId) { $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId } foreach ($propertyName in $UserProperties) { $grantDetails["Principal$propertyName"] = $principal.$propertyName } } New-Object PSObject -Property $grantDetails } } } } if ($ApplicationPermissions -or (!$DelegatedPermissions -and !$ApplicationPermissions)) { # Iterate over all ServicePrincipal objects and get app permissions Write-Verbose "Retrieving AppRoleAssignments..." if ($objectId) { $spObjectId = $objectId } else { $spObjectId = $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | % { $_.Value.Id } } $spObjectId | ForEach-Object { $i = 0 } { Write-Progress "Processing $_ service principal" if ($ShowProgress) { Write-Progress -Activity "Retrieving application permissions..." ` -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` -PercentComplete (($i / $servicePrincipalCount) * 100) } $serviceAppRoleAssignedTo = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $_ -All $serviceAppRoleAssignedTo | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { $assignment = $_ $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId $appRole = $resource.AppRoles | Where-Object { $_.id -eq $assignment.AppRoleId } $grantDetails = [ordered]@{ "PermissionType" = "Application" "ClientObjectId" = $assignment.PrincipalId "ResourceObjectId" = $assignment.ResourceId "Permission" = $appRole.value # "PermissionId" = $assignment.appRoleId "PermissionDisplayName" = $appRole.displayName "PermissionDescription" = $appRole.description } # Add properties for client and resource service principals if ($ServicePrincipalProperties.Count -gt 0) { $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId $insertAtClient = 2 $insertAtResource = 3 foreach ($propertyName in $ServicePrincipalProperties) { $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName) $insertAtResource++ $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName) $insertAtResource ++ } } New-Object PSObject -Property $grantDetails } } } } function Get-AzureServicePrincipalUsersAndGroups { <# .SYNOPSIS Get users and groups roles of (selected) service principal. .DESCRIPTION Get users and groups roles of (selected) service principal. .PARAMETER objectId ObjectId of service principal. If not provided all service principals will be processed. .EXAMPLE Get-AzureServicePrincipalUsersAndGroups Returns all service principals and their users and groups roles assignments. .EXAMPLE Get-AzureServicePrincipalUsersAndGroups -objectId 123123 Returns service principal with objectId 123123 and its users and groups roles assignments. .NOTES https://github.com/MicrosoftDocs/azure-docs/issues/48159 #> [CmdletBinding()] param ( [string] $objectId ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } $param = @{} if ($objectId) { Write-Verbose "Get $objectId service principal" $param.ServicePrincipalId = $objectId } else { Write-Verbose "Get all service principals" $param.all = $true } Get-MgServicePrincipal @param | % { # Build a hash table of the service principal's app roles. The 0-Guid is # used in an app role assignment to indicate that the principal is assigned # to the default app role (or rather, no app role). $appRoles = @{ [Guid]::Empty.ToString() = "(default)" } $_.AppRoles | % { $appRoles[$_.Id] = $_.DisplayName } Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $_.Id -All | % { $_ | Add-Member -Name "AppRoleDisplayName" -Value $appRoles[$_.AppRoleId] -MemberType NoteProperty -PassThru } } } function Get-AzureSkuAssignment { <# .SYNOPSIS Function returns users with selected Sku license. .DESCRIPTION Function returns users with selected Sku license. .PARAMETER sku SkuId or SkuPartNumber of the O365 license Sku. If not provided, all users and their Skus will be outputted. SkuId/SkuPartNumber can be found via: Get-MgSubscribedSku -All .PARAMETER assignmentType Limit what kind of license assignment the user needs to have. Possible values are: 'direct', 'inherited' By default users with both types are displayed. .EXAMPLE Get-AzureSkuAssignment -sku "f8a1db68-be16-40ed-86d5-cb42ce701560" Get all users with selected sku (defined by id). .EXAMPLE Get-AzureSkuAssignment -sku "POWER_BI_PRO" Get all users with selected sku. .EXAMPLE Get-AzureSkuAssignment Get all users and their skus. .EXAMPLE Get-AzureSkuAssignment -assignmentType direct Get all users which have some sku assigned directly. .EXAMPLE Get-AzureSkuAssignment -sku "POWER_BI_PRO" -assignmentType inherited Get all users with selected sku if it is inherited. #> [CmdletBinding()] param ( [ArgumentCompleter( { param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) Get-MgSubscribedSku -Property SkuPartNumber, SkuId -All | ? SkuPartNumber -Like "*$WordToComplete*" | select -ExpandProperty SkuPartNumber })] [string] $sku, [ValidateSet('direct', 'inherited')] [string[]] $assignmentType = ('direct', 'inherited'), [string[]] $userProperty = ('id', 'userprincipalname', 'assignedLicenses', 'LicenseAssignmentStates') ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } # add mandatory property if ($userProperty -notcontains 'assignedLicenses') { $userProperty += 'assignedLicenses' } if ($userProperty -notcontains 'LicenseAssignmentStates') { $userProperty += 'LicenseAssignmentStates' } $param = @{ Select = $userProperty All = $true } if ($sku) { $skuId = Get-MgSubscribedSku -Property SkuPartNumber, SkuId -All | ? { $_.SkuId -eq $sku -or $_.SkuPartNumber -eq $sku } | select -ExpandProperty SkuId if (!$skuId) { throw "Sku with id $skuId doesn't exist" } $param.Filter = "assignedLicenses/any(u:u/skuId eq $skuId)" } if ($assignmentType.count -eq 2) { # has some license $whereFilter = { $_.assignedLicenses } } elseif ($assignmentType -contains 'direct') { # direct assignment if ($sku) { $whereFilter = { $_.assignedLicenses -and ($_.LicenseAssignmentStates | ? { $_.SkuId -eq $skuId -and $_.AssignedByGroup -eq $null }) } } else { $whereFilter = { $_.assignedLicenses -and ($_.LicenseAssignmentStates.AssignedByGroup -eq $null).count -ge 1 } } } else { # inherited assignment if ($sku) { $whereFilter = { $_.assignedLicenses -and ($_.LicenseAssignmentStates | ? { $_.SkuId -eq $skuId -and $_.AssignedByGroup -ne $null }) } } else { $whereFilter = { $_.assignedLicenses -and $_.LicenseAssignmentStates.AssignedByGroup -ne $null } } } Get-MgUser @param | select $userProperty | ? $whereFilter } function Get-AzureSkuAssignmentError { <# .SYNOPSIS Function returns users that have problems with licenses assignment. .DESCRIPTION Function returns users that have problems with licenses assignment. #> if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): The context is invalid. Please login using Connect-MgGraph." } $userWithLicenseProblem = Get-MgUser -Property UserPrincipalName, Id, LicenseAssignmentStates -All | ? { $_.LicenseAssignmentStates.state -eq 'error' } foreach ($user in $userWithLicenseProblem) { $errorLicense = $user.LicenseAssignmentStates | ? State -EQ "Error" foreach ($license in $errorLicense) { [PSCustomObject]@{ UserPrincipalName = $user.UserPrincipalName UserId = $user.Id LicError = $license.Error AssignedByGroup = $license.AssignedByGroup AssignedByGroupName = (if ($license.AssignedByGroup) { (Get-MgGroup -GroupId $license.AssignedByGroup -Property DisplayName).DisplayName }) LastUpdatedDateTime = $license.LastUpdatedDateTime SkuId = $license.SkuId SkuName = (Get-MgSubscribedSku -Property SkuPartNumber, SkuId -All | ? { $_.SkuId -eq $license.SkuId } | select -ExpandProperty SkuPartNumber) } } } <# logictejsi by bylo jit shora dolu (group > user), ale tam je problem s vracenim potrebnych dat Get-MgGroup -Property Id, DisplayName, AssignedLicenses, LicenseProcessingState, MembersWithLicenseErrors -Filter "HasMembersWithLicenseErrors eq true" | % { $groupId = $_.Id # kvuli bugu je potreba delat primy api call namisto pouziti property MembersWithLicenseErrors (je prazdna) Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/groups/$groupId/membersWithLicenseErrors" -OutputType PSObject | select -ExpandProperty value } #> } function Get-AzureUserAuthMethodChanges { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]]$upnList, [ValidateSet('StrongAuthenticationMethod', 'StrongAuthenticationPhoneAppDetail')] # SearchableDeviceKey == FIDO, 'passwordless phone sign-in' # StrongAuthenticationPhoneAppDetail == mobile authenticator app [string[]] $methodType = ('SearchableDeviceKey', 'StrongAuthenticationPhoneAppDetail') ) # unfortunately I don't know how to directly get events just for specific user :( $allMFAMethodRegistration = Get-MgBetaAuditLogDirectoryAudit -All -Property * -Filter "Category eq 'UserManagement' and ActivityDisplayName eq 'Update user' and LoggedByService eq 'Core Directory' and InitiatedBy/App/DisplayName eq 'Azure MFA StrongAuthenticationService'" $allFIDOMFAMethodRegistration = Get-MgBetaAuditLogDirectoryAudit -All -Property * -Filter "Category eq 'UserManagement' and ActivityDisplayName eq 'Update user' and LoggedByService eq 'Core Directory' and InitiatedBy/App/DisplayName eq 'Device Registration Service'" #FIXME to vypada ze jde brat primo ty user eventy pres LoggedByService eq 'Device Registration Service' ?? (jen bych ignoroval 'Register device') ale asi neobsahuje ciste phone app?? $userMFAMethodRegistration = Get-MgBetaAuditLogDirectoryAudit -All -Property * -Filter "Category eq 'UserManagement' and LoggedByService eq 'Device Registration Service' and InitiatedBy/User/Id eq '$userId'" foreach ($upn in $upnList) { $userId = Get-MgBetaUser -UserId $upn -Property Id -ErrorAction Stop | select -ExpandProperty Id # rozsekat dle typu pridavane metody, $allMFAMethodRegistration ted NEOBSAHUJE vsechny # keyIdentifier u FIDO je vlastne ID $userMFAMethodRegistration = $allMFAMethodRegistration | ? { $_.TargetResources.Id -eq $userId } $userMFAMethodRegistration | % { $event = $_ $userAuthenticatorMFAMethodRegistrationOldValue = $event.TargetResources.ModifiedProperties | ? { $_.DisplayName -in $methodType -and $_.OldValue } | select -ExpandProperty OldValue | ConvertFrom-Json $userAuthenticatorMFAMethodRegistrationNewValue = $event.TargetResources.ModifiedProperties | ? { $_.DisplayName -in $methodType -and $_.NewValue } | select -ExpandProperty NewValue | ConvertFrom-Json $addedMethod = $userAuthenticatorMFAMethodRegistrationNewValue | ? { $_.Id -notin $userAuthenticatorMFAMethodRegistrationOldValue.Id } if ($addedMethod) { $addedMethod | select @{n = 'UPN'; e = { $upn } }, @{n = 'Action'; e = { 'Added' } }, @{n = 'DateTimeUTC'; e = { $event.ActivityDateTime } }, * } $removedMethod = $userAuthenticatorMFAMethodRegistrationOldValue | ? { $_.Id -notin $userAuthenticatorMFAMethodRegistrationNewValue.Id } if ($removedMethod) { $removedMethod | select @{n = 'UPN'; e = { $upn } }, @{n = 'Action'; e = { 'Removed' } }, @{n = 'DateTimeUTC'; e = { $event.ActivityDateTime } }, * } } $userMFAMethodRegistration = $allFIDOMFAMethodRegistration | ? { $_.TargetResources.Id -eq $userId } $userMFAMethodRegistration | % { $event = $_ $userAuthenticatorMFAMethodRegistrationOldValue = $event.TargetResources.ModifiedProperties | ? { $_.DisplayName -in $methodType -and $_.OldValue } | select -ExpandProperty OldValue | ConvertFrom-Json $userAuthenticatorMFAMethodRegistrationNewValue = $event.TargetResources.ModifiedProperties | ? { $_.DisplayName -in $methodType -and $_.NewValue } | select -ExpandProperty NewValue | ConvertFrom-Json $addedMethod = $userAuthenticatorMFAMethodRegistrationNewValue | ? { $_.Id -notin $userAuthenticatorMFAMethodRegistrationOldValue.Id } if ($addedMethod) { $addedMethod | select @{n = 'UPN'; e = { $upn } }, @{n = 'Action'; e = { 'Added' } }, @{n = 'DateTimeUTC'; e = { $event.ActivityDateTime } }, * } $removedMethod = $userAuthenticatorMFAMethodRegistrationOldValue | ? { $_.Id -notin $userAuthenticatorMFAMethodRegistrationNewValue.Id } if ($removedMethod) { $removedMethod | select @{n = 'UPN'; e = { $upn } }, @{n = 'Action'; e = { 'Removed' } }, @{n = 'DateTimeUTC'; e = { $event.ActivityDateTime } }, * } } } } function Grant-AzureServicePrincipalPermission { <# .SYNOPSIS Function for granting application/delegated permission(s) for selected resource to selected account. .DESCRIPTION Function for granting application/delegated permission(s) for selected resource to selected account. By default grants permission to Graph Api resource. .PARAMETER servicePrincipalName Name of the service principal you want to grant permission(s) to. .PARAMETER servicePrincipalId ObjectId of the service principal you want to grant permissions(s) to. .PARAMETER resourceAppId ObjectId of the resource you want to grant permission(s) to. By default ObjectId of the Graph API resource a.k.a. GraphAggregatorService service principal. .PARAMETER permissionList List of permissions you want to grant. If not defined, Out-GridView table with all available permissions (of type defined in permissionType) will be interactively outputted, so the user can pick some. .PARAMETER permissionType Type of permission you want to add. Possible values are application, delegated. By default application is selected. .EXAMPLE Grant-AzureServicePrincipalPermission -servicePrincipalName "Merge EU Integration" -permissionList user.read.all ,GroupMember.Read.All, Group.Read.All, offline_access Grant selected 'application' type Graph Api permissions to application "Merge EU Integration". .EXAMPLE Grant-AzureServicePrincipalPermission -servicePrincipalName "Merge EU Integration" Shows table with all available 'application' type permissions for Graph Api, let the user pick some and grant them to application "Merge EU Integration". .EXAMPLE Grant-AzureServicePrincipalPermission -servicePrincipalId e9af2b82-335f-4160-9da6-0ad647affd7e -permissionList offline_access -permissionType delegated Grant selected 'delegated' type Graph Api permissions to application with selected ObjectId. #> [CmdletBinding(DefaultParameterSetName = 'name')] param ( [Parameter(Mandatory = $true, ParameterSetName = "name")] [string] $servicePrincipalName, [Parameter(Mandatory = $true, ParameterSetName = "id")] [string] $servicePrincipalId, [string] $resourceAppId = '00000003-0000-0000-c000-000000000000', # graph api [ArgumentCompleter( { param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) $resourceAppId = $FakeBoundParams.resourceAppId if (!$resourceAppId) { $resourceAppId = '00000003-0000-0000-c000-000000000000' } if (!$FakeBoundParams.permissionType -or $FakeBoundParams.permissionType -eq 'application') { (Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'").AppRoles.Value | ? { $_ -like "*$WordToComplete*" } } else { (Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'").Oauth2PermissionScopes.Value | ? { $_ -like "*$WordToComplete*" } } })] [string[]] $permissionList, [ValidateSet('application', 'delegated')] [string] $permissionType = "application" ) # authenticate if ($permissionType -eq "application") { $graphScope = "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" } else { $graphScope = "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All" } $null = Connect-MgGraph -Scopes $graphScope -ea Stop # remove duplicates $permissionList = $permissionList | select -Unique # get account to which permissions will be granted if ($servicePrincipalName) { $servicePrincipal = Get-MgServicePrincipal -Filter "displayName eq '$servicePrincipalName'" if (!$servicePrincipal) { throw "Service principal '$servicePrincipalName' doesn't exist" } } else { $servicePrincipal = (Get-MgServicePrincipal -ServicePrincipalId $servicePrincipalId) if (!$servicePrincipal) { throw "Service principal '$servicePrincipalId' doesn't exist" } } # get application whose permissions will be granted $resourceServicePrincipal = Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'" -Property Id, AppRoles, Oauth2PermissionScopes if (!$resourceServicePrincipal) { throw "Resource '$resourceAppId' doesn't exist" } # let the user pick permissions to grant interactively if (!$permissionList) { if ($permissionType -eq "application") { $availablePermission = (Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'").AppRoles | select Value, DisplayName, Description } else { $availablePermission = (Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'").Oauth2PermissionScopes | select Value, AdminConsentDisplayName, AdminConsentDescription } $permissionList = $availablePermission | sort Value | Out-GridView -Title "Select $permissionType permission(s) you want to grant" -OutputMode Multiple | select -ExpandProperty Value if (!$permissionList) { throw "You haven't selected any permission" } } Write-Verbose "Permission(s): $(($permissionList | sort) -join ', ') of the resource '$($resourceServicePrincipal.displayName)' will be granted to: $($servicePrincipal.displayName)" # get already assigned permissions if ($permissionType -eq "application") { $appRoleAssignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id | ? ResourceId -EQ $resourceServicePrincipal.Id } else { # if some permissions were already granted, update must be used instead of creation of the new grant $Oauth2PermissionGrant = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($servicePrincipal.Id)' and ResourceId eq '$($resourceServicePrincipal.Id)' and consentType eq 'AllPrincipals'" } $delegatedPermissionList = @() if ($Oauth2PermissionGrant) { $delegatedPermissionList = @($Oauth2PermissionGrant.Scope -split " ") } #region grant requested permissions foreach ($permission in $permissionList) { if ($permissionType -eq "application") { # grant application permission # https://learn.microsoft.com/en-us/powershell/microsoftgraph/tutorial-grant-app-only-api-permissions?view=graph-powershell-1.0 # check whether such permission exists $appRole = $resourceServicePrincipal.AppRoles | Where-Object { $_.Value -eq $permission -and $_.AllowedMemberTypes -contains "Application" } if (!$appRole) { Write-Warning "Application permission '$permission' wasn't found in '$resourceAppId' application. Skipping" continue } elseif ($appRole.Id -in $appRoleAssignment.AppRoleId) { Write-Warning "Application permission '$permission' is already granted. Skipping" continue } $params = @{ PrincipalId = $servicePrincipal.Id ResourceId = $resourceServicePrincipal.Id AppRoleId = $appRole.Id } Write-Warning "Granting application permission '$permission'" $null = New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id -BodyParameter $params } else { # prepare delegated permission to add # https://learn.microsoft.com/en-us/powershell/microsoftgraph/tutorial-grant-delegated-api-permissions?view=graph-powershell-1.0 # check whether such permission exists $Oauth2PermissionScope = $resourceServicePrincipal.Oauth2PermissionScopes | Where-Object { $_.Value -eq $permission } if (!$Oauth2PermissionScope) { Write-Warning "Delegated permission '$permission' wasn't found in '$resourceAppId' application. Skipping" continue } # check whether permission is already added if ($Oauth2PermissionGrant -and ($Oauth2PermissionGrant.Scope -split " " -contains $permission)) { Write-Warning "Delegated permission '$permission' is already granted. Skipping" continue } $delegatedPermissionList += $permission } } # grant delegated permission # delegated permissions have to be set at once, and not one by one if ($delegatedPermissionList) { Write-Warning "Granting delegated permission(s) '$($delegatedPermissionList -join " ")'" if ($Oauth2PermissionGrant) { # there is some permissions grant already, update it $params = @{ "Scope" = ($delegatedPermissionList -join " ") } $null = Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $Oauth2PermissionGrant.Id -BodyParameter $params } else { # there is no existing permissions grant, create it $params = @{ "ClientId" = $servicePrincipal.Id "ConsentType" = "AllPrincipals" "ResourceId" = $resourceServicePrincipal.Id "Scope" = ($delegatedPermissionList -join " ") } $null = New-MgOauth2PermissionGrant -BodyParameter $params } } #endregion grant requested permissions } function New-AzureAutomationModule { <# .SYNOPSIS Function for uploading new (or updating existing) Azure Automation PSH module. Any module dependencies will be installed too. .DESCRIPTION Function for uploading new (or updating existing) Azure Automation PSH module. Any module dependencies will be installed too. If module exists, but in lower version, it will be updated. .PARAMETER moduleName Name of the PSH module. .PARAMETER moduleVersion (optional) version of the PSH module. .PARAMETER resourceGroupName Name of the Azure Resource Group. .PARAMETER automationAccountName Name of the Azure Automation Account. .PARAMETER runtimeVersion PSH runtime version. Possible values: 5.1, 7.1, 7.2. By default 5.1. .PARAMETER overridePSGalleryModuleVersion Hashtable of hashtables where you can specify what module version should be used for given runtime if no specific version is required. This is needed in cases, where module newest available PSGallery version isn't compatible with your runtime because of incorrect manifest. By default: $overridePSGalleryModuleVersion = @{ # 2.x.x PnP.PowerShell versions (2.1.1, 2.2.0) requires PSH 7.2 even though manifest doesn't say it # so the wrong module version would be picked up which would cause an error when trying to import "PnP.PowerShell" = @{ "5.1" = "1.12.0" } } .EXAMPLE Connect-AzAccount -Tenant "contoso.onmicrosoft.com" -SubscriptionName "AutomationSubscription" New-AzureAutomationModule -resourceGroupName test -automationAccountName test -moduleName "Microsoft.Graph.Groups" Imports newest supported version (for given Runtime) of the "Microsoft.Graph.Groups" module including all its dependencies. In case module "Microsoft.Graph.Groups" (with any version) and all its dependencies are already imported, nothing will happens. .EXAMPLE Connect-AzAccount -Tenant "contoso.onmicrosoft.com" -SubscriptionName "AutomationSubscription" New-AzureAutomationModule -resourceGroupName test -automationAccountName test -moduleName "Microsoft.Graph.Groups" -moduleVersion "2.11.1" Imports newest supported version (for given Runtime) of the "Microsoft.Graph.Groups" module including all its dependencies. In case module "Microsoft.Graph.Groups" with version "2.11.1" and all its dependencies are already imported, nothing will happens. Otherwise module will be replaced (including all dependencies that are required for this specific version). #> [CmdletBinding()] [Alias("New-AzAutomationModule2")] param ( [Parameter(Mandatory = $true)] [string] $moduleName, [string] $moduleVersion, [Parameter(Mandatory = $true)] [string] $resourceGroupName, [Parameter(Mandatory = $true)] [string] $automationAccountName, [ValidateSet('5.1', '7.1', '7.2')] [string] $runtimeVersion = '5.1', [int] $indent = 0, [hashtable[]] $overridePSGalleryModuleVersion = @{ # 2.x.x PnP.PowerShell versions (2.1.1, 2.2.0) requires PSH 7.2 even though manifest doesn't say it # so the wrong module version would be picked up which would cause an error when trying to import "PnP.PowerShell" = @{ "5.1" = "1.12.0" } } ) if (!(Get-Command 'Get-AzAccessToken' -ErrorAction silentlycontinue) -or !($azAccessToken = Get-AzAccessToken -ErrorAction SilentlyContinue) -or $azAccessToken.ExpiresOn -lt [datetime]::now) { throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-AzAccount." } $indentString = " " * $indent function _write { param ($string, $color) $param = @{ Object = ($indentString + $string) } if ($color) { $param.ForegroundColor = $color } Write-Host @param } if ($moduleVersion) { $moduleVersionString = "($moduleVersion)" } else { $moduleVersionString = "" } _write "Processing module $moduleName $moduleVersionString" "Magenta" #region get PSGallery module data $param = @{ # IncludeDependencies = $true # cannot be used, because always returns newest usable module version, I want to use existing modules if possible (to minimize the runtime & risk that something will stop working) Name = $moduleName ErrorAction = "Stop" } if ($moduleVersion) { $param.RequiredVersion = $moduleVersion } elseif ($runtimeVersion -eq '5.1') { $param.AllVersions = $true } $moduleGalleryInfo = Find-Module @param #endregion get PSGallery module data # get newest usable module version for given runtime if (!$moduleVersion -and $runtimeVersion -eq '5.1') { # no specific version was selected and older PSH version is used, make sure module that supports it, will be found # for example (currently newest) pnp.powershell 2.3.0 supports only PSH 7.2 $moduleGalleryInfo = $moduleGalleryInfo | ? { $_.AdditionalMetadata.PowerShellVersion -le $runtimeVersion } | select -First 1 } if (!$moduleGalleryInfo) { Write-Error "No supported $moduleName module was found in PSGallery" return } # override module version if (!$moduleVersion -and $moduleName -in $overridePSGalleryModuleVersion.Keys -and $overridePSGalleryModuleVersion.$moduleName.$runtimeVersion) { $overriddenModule = $overridePSGalleryModuleVersion.$moduleName $overriddenModuleVersion = $overriddenModule.$runtimeVersion if ($overriddenModuleVersion) { _write " (no version specified and override for version exists, hence will be used ($overriddenModuleVersion))" $moduleVersion = $overriddenModuleVersion } } if (!$moduleVersion) { $moduleVersion = $moduleGalleryInfo.Version _write " (no version specified, newest supported version from PSGallery will be used ($moduleVersion))" } Write-Verbose "Getting current Automation modules" $currentAutomationModules = Get-AzAutomationModule -AutomationAccountName $automationAccountName -ResourceGroup $resourceGroupName -RuntimeVersion $runtimeVersion -ErrorAction Stop # check whether required module is present # there can be module in Failed state, just because update of such module failed, but if it has SizeInBytes set, it means its in working state $moduleExists = $currentAutomationModules | ? { $_.Name -eq $moduleName -and $_.SizeInBytes } if ($moduleVersion) { $moduleExists = $moduleExists | ? Version -EQ $moduleVersion } if ($moduleExists) { return ($indentString + "Module $moduleName ($($moduleExists.Version)) is already present") } _write " - Getting module $moduleName dependencies" $moduleDependency = $moduleGalleryInfo.Dependencies | Sort-Object { $_.name } # dependency must be installed first if ($moduleDependency) { #TODO znacit si jake moduly jsou required (at uz tam jsou nebo musim doinstalovat) a kontrolovat, ze jeden neni required s ruznymi verzemi == konflikt protoze nainstalovana muze byt jen jedna _write " - Depends on: $($moduleDependency.Name -join ', ')" foreach ($module in $moduleDependency) { $requiredModuleName = $module.Name [version]$requiredModuleMinVersion = $module.MinimumVersion [version]$requiredModuleMaxVersion = $module.MaximumVersion [version]$requiredModuleReqVersion = $module.RequiredVersion $notInCorrectVersion = $false _write " - Checking module $requiredModuleName (minVer: $requiredModuleMinVersion maxVer: $requiredModuleMaxVersion reqVer: $requiredModuleReqVersion)" # there can be module in Failed state, just because update of such module failed, but if it has SizeInBytes set, it means its in working state $existingRequiredModule = $currentAutomationModules | ? { $_.Name -eq $requiredModuleName -and ($_.ProvisioningState -eq "Succeeded" -or $_.SizeInBytes) } [version]$existingRequiredModuleVersion = $existingRequiredModule.Version # check that existing module version fits if ($existingRequiredModule -and ($requiredModuleMinVersion -or $requiredModuleMaxVersion -or $requiredModuleReqVersion)) { #TODO pokud nahrazuji existujici modul, tak bych se mel podivat, jestli jsou vsechny ostatni ok s jeho novou verzi if ($requiredModuleReqVersion -and $requiredModuleReqVersion -ne $existingRequiredModuleVersion) { $notInCorrectVersion = $true _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be: $requiredModuleReqVersion). Will be replaced" "Yellow" } elseif ($requiredModuleMinVersion -and $requiredModuleMaxVersion -and ($existingRequiredModuleVersion -lt $requiredModuleMinVersion -or $existingRequiredModuleVersion -gt $requiredModuleMaxVersion)) { $notInCorrectVersion = $true _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be: $requiredModuleMinVersion .. $requiredModuleMaxVersion). Will be replaced" "Yellow" } elseif ($requiredModuleMinVersion -and $existingRequiredModuleVersion -lt $requiredModuleMinVersion) { $notInCorrectVersion = $true _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be > $requiredModuleMinVersion). Will be replaced" "Yellow" } elseif ($requiredModuleMaxVersion -and $existingRequiredModuleVersion -gt $requiredModuleMaxVersion) { $notInCorrectVersion = $true _write " - module exists, but not in the correct version (has: $existingRequiredModuleVersion, should be < $requiredModuleMaxVersion). Will be replaced" "Yellow" } } if (!$existingRequiredModule -or $notInCorrectVersion) { if (!$existingRequiredModule) { _write " - module is missing" "Yellow" } if ($notInCorrectVersion) { #TODO kontrola, ze jina verze modulu nerozbije zavislost nejakeho jineho existujiciho modulu } #region install required module first $param = @{ moduleName = $requiredModuleName resourceGroupName = $resourceGroupName automationAccountName = $automationAccountName runtimeVersion = $runtimeVersion indent = $indent + 1 } if ($requiredModuleMinVersion) { $param.moduleVersion = $requiredModuleMinVersion } if ($requiredModuleMaxVersion) { $param.moduleVersion = $requiredModuleMaxVersion } if ($requiredModuleReqVersion) { $param.moduleVersion = $requiredModuleReqVersion } New-AzureAutomationModule @param #endregion install required module first } else { if ($existingRequiredModuleVersion) { _write " - module (ver. $existingRequiredModuleVersion) is already present" } else { _write " - module is already present" } } } } else { _write " - No dependency found" } $uri = "https://www.powershellgallery.com/api/v2/package/$moduleName/$moduleVersion" _write " - Uploading module $moduleName ($moduleVersion)" "Yellow" $status = New-AzAutomationModule -AutomationAccountName $automationAccountName -ResourceGroup $resourceGroupName -Name $moduleName -ContentLinkUri $uri -RuntimeVersion $runtimeVersion $i = 0 do { if ($i % 5 -eq 0) { _write " Still working..." } Start-Sleep 5 ++$i } while (!($requiredModule = Get-AzAutomationModule -AutomationAccountName $automationAccountName -ResourceGroup $resourceGroupName -RuntimeVersion $runtimeVersion -ErrorAction Stop | ? { $_.Name -eq $moduleName -and $_.ProvisioningState -in "Succeeded", "Failed" })) if ($requiredModule.ProvisioningState -ne "Succeeded") { Write-Error "Import failed. Check Azure Portal >> Automation Account >> Modules >> $moduleName details to get the reason." } else { _write " - Success" "Green" } } function Open-AzureAdminConsentPage { <# .SYNOPSIS Function for opening web page with admin consent to requested/selected permissions to selected application. .DESCRIPTION Function for opening web page with admin consent to requested/selected permissions to selected application. .PARAMETER appId Application (client) ID. .PARAMETER tenantId Your Azure tenant ID. .EXAMPLE Open-AzureAdminConsentPage -appId 123412341234 -scope openid, profile, email, user.read, Mail.Send Grant admin consent for selected permissions to app with client ID 123412341234. .EXAMPLE Open-AzureAdminConsentPage -appId 123412341234 Grant admin consent for requested permissions to app with client ID 123412341234. .NOTES https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $appId, [string] $tenantId = $_tenantId, [string[]] $scope, [switch] $justURL ) if ($scope) { # grant custom permission $scope = $scope.trim() -join "%20" $URL = "https://login.microsoftonline.com/$tenantId/v2.0/adminconsent?client_id=$appId&scope=$scope" if ($justURL) { return $URL } else { Start-Process $URL } } else { # grant requested permissions $URL = "https://login.microsoftonline.com/$tenantId/adminconsent?client_id=$appId" if ($justURL) { return $URL } else { Start-Process $URL } } } function Remove-AzureAccountOccurrence { <# .SYNOPSIS Function for removal of selected AAD account occurrences in various parts of AAD. .DESCRIPTION Function for removal of selected AAD account occurrences in various parts of AAD. .PARAMETER inputObject PSCustomObject that is outputted by Get-AzureAccountOccurrence function. Contains information about account and its occurrences i.e. is used in this function as information about what to remove and from where. Object (as a output of Get-AzureAccountOccurrence) should have these properties: UPN DisplayName ObjectType ObjectId IAM MemberOfDirectoryRole MemberOfGroup PermissionConsent Owner SharepointSiteOwner AppUsersAndGroupsRoleAssignment KeyVaultAccessPolicy ExchangeRole .PARAMETER replaceByUser (optional) ObjectId or UPN of the AAD user that will replace processed user as a new owner/manager. But if there are other owners, the one being removed won't be replaced, just deleted! Cannot be used with replaceByManager. .PARAMETER replaceByManager Switch for using user's manager as a new owner/manager. Applies ONLY for processed USERS (because only users have managers) and not other object types! If there are other owners, the one being removed won't be replaced, just deleted! Cannot be used with replaceByUser. .PARAMETER whatIf Switch for omitting any changes, just output what would be done. .PARAMETER removeRegisteredDevice Switch for removal of registered devices. Otherwise registered devices stays intact. This doesn't apply to joined device. .PARAMETER informNewManOwn Switch for sending email notification to new owners/managers about what and why was transferred to them. .EXAMPLE Get-AzureAccountOccurrence -userPrincipalName pavel@contoso.com | Remove-AzureAccountOccurrence -whatIf Get all occurrences of specified user and just output what would be done with them. .EXAMPLE Get-AzureAccountOccurrence -userPrincipalName pavel@contoso.com | Remove-AzureAccountOccurrence Get all occurrences of specified user and remove them. In case user has registered some devices, they stay intact. .EXAMPLE Get-AzureAccountOccurrence -userPrincipalName pavel@contoso.com | Remove-AzureAccountOccurrence -removeRegisteredDevice Get all occurrences of specified user and remove them. In case user has registered some devices, they will be deleted. .EXAMPLE Get-AzureAccountOccurrence -userPrincipalName pavel@contoso.com | Remove-AzureAccountOccurrence -replaceByUser 1234-1234-1234-1234 Get all occurrences of specified user and remove them. In case user is owner or manager on some object(s) he will be replaced there by specified user (for ownerships this apply only if removed user is last owner). In case user has registered some devices, they stay intact. .EXAMPLE Get-AzureAccountOccurrence -userPrincipalName pavel@contoso.com | Remove-AzureAccountOccurrence -replaceByManager Get all occurrences of specified user and remove them. In case user is owner or manager on some object(s) he will be replaced there by his manager (for ownerships this apply only if removed user is last owner). In case user has registered some devices, they stay intact. #> [CmdletBinding()] [Alias("Remove-AzureADAccountOccurrence")] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSCustomObject] $inputObject, [string] $replaceByUser, [switch] $replaceByManager, [switch] $whatIf, [switch] $removeRegisteredDevice, [switch] $informNewManOwn ) begin { if ($replaceByUser -and $replaceByManager) { throw "replaceByUser and replaceByManager cannot be used together. Choose one of them." } if ($informNewManOwn -and (!$replaceByUser -and !$replaceByManager)) { Write-Warning "Parameter 'informNewManOwn' will be ignored because no replacements will be made." $informNewManOwn = $false } #region connect # connect to AzureAD Write-Verbose "Connecting to AzureAD" $null = Connect-MgGraph -ea Stop -Scopes Directory.AccessAsUser.All, GroupMember.ReadWrite.All, User.Read.All, GroupMember.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, Application.ReadWrite.All, AppRoleAssignment.ReadWrite.All Write-Verbose "Connecting to AzAccount" $null = Connect-AzAccount2 -ea Stop # connect sharepoint online if ($inputObject.SharepointSiteOwner) { Write-Verbose "Connecting to Sharepoint" Connect-PnPOnline2 -asMFAUser -ea Stop } if ($inputObject.ExchangeRole -or $inputObject.MemberOfGroup.MailEnabled) { Write-Verbose "Connecting to Exchange" Connect-O365 -service exchange -ea Stop } #endregion connect if ($informNewManOwn) { $newManOwnReport = @() } } process { # check replacement user account if ($replaceByUser) { $replacementAADAccountObj = Get-MgUser -UserId $replaceByUser if (!$replacementAADAccountObj) { throw "Replacement account $replaceByUser was not found in AAD" } else { $replacementAADAccountId = $replacementAADAccountObj.Id $replacementAADAccountDisplayName = $replacementAADAccountObj.DisplayName Write-Warning "'$replacementAADAccountDisplayName' will be new manager/owner instead of account that is being removed" } } $inputObject | % { <# Object (as a output of Get-AzureAccountOccurrence) should have these properties: UPN DisplayName ObjectType ObjectId IAM MemberOfDirectoryRole MemberOfGroup PermissionConsent Owner SharepointSiteOwner AppUsersAndGroupsRoleAssignment KeyVaultAccessPolicy ExchangeRole #> $accountId = $_.ObjectId $accountDisplayName = $_.DisplayName "Processing cleanup on account '$accountDisplayName' ($accountId)" $AADAccountObj = Get-MgDirectoryObjectById -Ids $accountId if (!$AADAccountObj) { Write-Error "Account $accountId was not found in AAD" } if ($replaceByManager) { if ($_.ObjectType -eq 'user') { $replacementAADAccountObj = Get-MgUserManager -UserId $accountId | Expand-MgAdditionalProperties # so the $replacementAADAccountObj have user properties at root level therefore looks same as when Get-MgUser is used (because of $replaceByUser) if (!$replacementAADAccountObj) { throw "Account '$accountDisplayName' doesn't have a manager. Specify replacement account via 'replaceByUser' parameter?" } else { $replacementAADAccountId = $replacementAADAccountObj.Id $replacementAADAccountDisplayName = $replacementAADAccountObj.DisplayName Write-Warning "User's manager '$replacementAADAccountDisplayName' will be new manager/owner instead of account that is being removed" } } else { Write-Warning "Account $accountId isn't a user ($($_.ObjectType)). Parameter 'replaceByManager' will be ignored." } } # prepare base object for storing data for later email notification if ($informNewManOwn -and $replacementAADAccountObj) { $newManOwnObj = [PSCustomObject]@{ replacedUserObjectId = $accountId replacedUserName = $accountDisplayName newUserEmail = $replacementAADAccountObj.mail newUserName = $replacementAADAccountDisplayName newUserObjectId = $replacementAADAccountId message = @() } } #region remove AAD account occurrences #region IAM if ($_.IAM) { Write-Verbose "Removing IAM assignments" $tenantId = (Get-AzContext).tenant.id $_.IAM | select ObjectId, AssignmentScope, RoleDefinitionName -Unique | % { # $Context = Set-AzContext -TenantId $tenantId -SubscriptionId $_.SubscriptionId -Force "Removing IAM role '$($_.RoleDefinitionName)' at scope '$($_.AssignmentScope)'" if (!$whatIf) { $lock = Get-AzResourceLock -Scope $_.AssignmentScope if ($lock) { Write-Warning "Unable to delete IAM role, because resource is LOCKED via '$($lock.name)' lock" } else { Remove-AzRoleAssignment -ObjectId $_.ObjectId -Scope $_.AssignmentScope -RoleDefinitionName $_.RoleDefinitionName } } } } #endregion IAM #region group membership if ($_.MemberOfGroup) { $_.MemberOfGroup | % { if ($_.onPremisesSyncEnabled) { Write-Warning "Skipping removal from group '$($_.displayName)' ($($_.id)), because it is synced from on-premises AD" } elseif ($_.membershipRule) { Write-Warning "Skipping removal from group '$($_.displayName)' ($($_.id)), because it has rule-based membership" } else { "Removing from group '$($_.displayName)' ($($_.id))" if (!$whatIf) { if ($_.mailEnabled -and !$_.groupTypes) { # distribution group Remove-DistributionGroupMember -Identity $_.id -Member $accountId -BypassSecurityGroupManagerCheck -Confirm:$false } else { # Microsoft 365 group Remove-MgGroupMemberByRef -GroupId $_.id -DirectoryObjectId $accountId } } } } } #endregion group membership #region membership directory role if ($_.MemberOfDirectoryRole) { $_.MemberOfDirectoryRole | % { "Removing from directory role '$($_.displayName)' ($($_.id))" if (!$whatIf) { Remove-MgDirectoryRoleScopedMember -DirectoryRoleId $_.id -ScopedRoleMembershipId $accountId } } } #endregion membership directory role #region user perm consents if ($_.PermissionConsent) { $_.PermissionConsent | % { "Removing user consent from app '$($_.AppName)', permission '$($_.scope)' to '$($_.ResourceDisplayName)'" if (!$whatIf) { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $_.Id } } } #endregion user perm consents #region manager if ($_.Manager) { $_.Manager | % { $managerOf = $_ $managerOfObjectType = $managerOf.ObjectType $managerOfDisplayName = $managerOf.DisplayName $managerOfObjectId = $managerOf.Id switch ($managerOfObjectType) { User { "Removing as a manager of the $managerOfObjectType '$managerOfDisplayName' ($managerOfObjectId)" if (!$whatIf) { Remove-MgUserManagerByRef -UserId $managerOfObjectId } if ($replacementAADAccountObj) { "Adding '$replacementAADAccountDisplayName' as a manager of the $managerOfObjectType '$managerOfDisplayName' ($managerOfObjectId)" if (!$whatIf) { $newManager = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/users/$replacementAADAccountId" } Set-MgUserManagerByRef -UserId $managerOfObjectId -BodyParameter $newManager if ($informNewManOwn) { $newManOwnObj.message += @("new manager of the $managerOfObjectType '$managerOfDisplayName' ($managerOfObjectId)") } } } } Contact { "Removing as a manager of the $managerOfObjectType '$managerOfDisplayName' ($managerOfObjectId)" if (!$whatIf) { Write-Warning "Remove manager of the $managerOfObjectType '$managerOfDisplayName' ($managerOfObjectId) manually!" } if ($replacementAADAccountObj) { Write-Warning "Add '$replacementAADAccountDisplayName' as a manager of the $managerOfObjectType '$managerOfDisplayName' ($managerOfObjectId) manually!" } } default { Write-Error "Not defined action for object type $managerOfObjectType. User won't be removed as a manager of this object." } } } } #endregion manager #region ownership # application, group, .. owner if ($_.Owner) { $_.Owner | % { $ownerOf = $_ $ownerOfObjectType = $ownerOf.ObjectType $ownerOfDisplayName = $ownerOf.DisplayName $ownerOfObjectId = $ownerOf.Id switch ($ownerOfObjectType) { Application { # app registration "Removing owner from app registration '$ownerOfDisplayName'" if (!$whatIf) { $null = Remove-MgApplicationOwnerByRef -ApplicationId $ownerOfObjectId -DirectoryObjectId $accountId } if ($replacementAADAccountObj) { $recentObjOwner = Get-MgApplicationOwner -ApplicationId $ownerOfObjectId -All | ? Id -NE $accountId if (!$recentObjOwner) { "Adding '$replacementAADAccountDisplayName' as owner of the '$ownerOfDisplayName' application" if (!$whatIf) { $newOwner = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$replacementAADAccountId" } New-MgApplicationOwnerByRef -ApplicationId $ownerOfObjectId -BodyParameter $NewOwner if ($informNewManOwn) { $appId = Get-MgApplication -ApplicationId $ownerOfObjectId | select -ExpandProperty AppId $url = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/$appId" $newManOwnObj.message += @("new owner of the '$ownerOfDisplayName' application ($url)") } } } else { Write-Warning "App registration has some owners left. '$replacementAADAccountDisplayName' won't be added." } } } ServicePrincipal { # enterprise apps owner "Removing owner from service principal '$ownerOfDisplayName'" if (!$whatIf) { Remove-MgServicePrincipalOwnerByRef -ServicePrincipalId $ownerOfObjectId -DirectoryObjectId $accountId } if ($replacementAADAccountObj) { $recentObjOwner = Get-MgServicePrincipalOwner -ServicePrincipalId $ownerOfObjectId -All | ? Id -NE $accountId if (!$recentObjOwner) { "Adding '$replacementAADAccountDisplayName' as owner of the '$ownerOfDisplayName' service principal" if (!$whatIf) { $newOwner = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/{$replacementAADAccountId}" } New-MgServicePrincipalOwnerByRef -ServicePrincipalId $ownerOfObjectId -BodyParameter $newOwner if ($informNewManOwn) { $appId = Get-MgServicePrincipal -ServicePrincipalId $ownerOfObjectId | select -ExpandProperty AppId $url = "https://portal.azure.com/#blade/Microsoft_AAD_IAM/ManagedAppMenuBlade/Overview/objectId/$ownerOfObjectId/appId/$appId" $newManOwnObj.message += @("new owner of the '$ownerOfDisplayName' service principal ($url)") } } } else { Write-Warning "Service principal has some owners left. '$replacementAADAccountDisplayName' won't be added." } } } Group { # adding new owner before removing the old one because group won't let you remove last owner if ($replacementAADAccountObj) { $recentObjOwner = Get-MgGroupOwner -GroupId $ownerOfObjectId -All | ? Id -NE $accountId if (!$recentObjOwner) { "Adding '$replacementAADAccountDisplayName' as owner of the '$ownerOfDisplayName' group" if (!$whatIf) { $newOwner = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/users/{$replacementAADAccountId}" } New-MgGroupOwnerByRef -GroupId $ownerOfObjectId -BodyParameter $newOwner if ($informNewManOwn) { $url = "https://portal.azure.com/#blade/Microsoft_AAD_IAM/GroupDetailsMenuBlade/Overview/groupId/$ownerOfObjectId" $newManOwnObj.message += @("new owner of the '$ownerOfDisplayName' group ($url)") } } } else { Write-Warning "Group has some owners left. '$replacementAADAccountDisplayName' won't be added." } } "Removing owner from group '$ownerOfDisplayName'" if (!$whatIf) { Remove-MgGroupOwnerByRef -GroupId $ownerOfObjectId -DirectoryObjectId $accountId } } Device { if ($ownerOf.DeviceTrustType -eq 'Workplace') { # registered device if ($removeRegisteredDevice) { "Removing registered device '$ownerOfDisplayName' ($ownerOfObjectId)" if (!$whatIf) { Remove-MgDevice -DeviceId $ownerOfObjectId } } else { Write-Warning "Registered device '$ownerOfDisplayName' won't be deleted nor owner of this device will be removed" } } else { # joined device "Removing owner from device '$ownerOfDisplayName' ($ownerOfObjectId)" if (!$whatIf) { Remove-MgDeviceRegisteredOwnerByRef -DeviceId $ownerOfObjectId -DirectoryObjectId $accountId } } if ($replacementAADAccountObj) { Write-Verbose "Device owner won't be replaced by '$replacementAADAccountDisplayName' because I don't want to" } } default { Write-Error "Not defined action for object type $ownerOfObjectType. User won't be removed as a owner of this object." } } } } # sharepoint sites owner if ($_.SharepointSiteOwner) { $_.SharepointSiteOwner | % { if ($_.template -like 'GROUP*') { # it is sharepoint site based on group (owners are group members) "Removing from group '$($_.Title)' that has owner rights on Sharepoint site '$($_.Site)'" if (!$whatIf) { Remove-PnPMicrosoft365GroupOwner -Identity $_.GroupId -Users $userPrincipalName } if ($replacementAADAccountObj) { $recentObjOwner = Get-PnPMicrosoft365GroupOwner -Identity $_.GroupId -All:$true | ? Id -NE $accountId if (!$recentObjOwner) { "Adding '$replacementAADAccountDisplayName' as owner of the '$($_.Title)' group" if (!$whatIf) { Add-PnPMicrosoft365GroupOwner -Identity $_.GroupId -Users $replacementAADAccountObj.UserPrincipalName if ($informNewManOwn) { $url = "https://portal.azure.com/#blade/Microsoft_AAD_IAM/GroupDetailsMenuBlade/Overview/groupId/$($_.GroupId)" $newManOwnObj.message += @("new owner of the '$($_.Title)' group ($url)") } } } else { Write-Warning "Sharepoint site has some owners left. '$replacementAADAccountDisplayName' won't be added." } } } else { # it is common sharepoint site Write-Warning "Remove owner from Sharepoint site '$($_.site)' manually!" # "Removing from sharepoint site '$($_.site)'" # https://www.sharepointdiary.com/2018/02/change-site-owner-in-sharepoint-online-using-powershell.html # https://www.sharepointdiary.com/2020/05/sharepoint-online-grant-site-owner-permission-to-user-with-powershell.html if ($replacementAADAccountObj) { Write-Warning "Add '$($replacementAADAccountObj.UserPrincipalName)' as new owner at Sharepoint site '$($_.site)' manually!" # "Adding '$replacementAADAccountDisplayName' as owner of the '$($_.site)' sharepoint site" # Set-SPOSite https://contoso.sharepoint.com/sites/otest_communitysite_test_smazani -Owner admin@contoso.com # zda se ze funguje, ale vyzaduje Connect-SPOService -Url $_SPOConnectionUri # Set-PnPSite -Identity $_.site -Owners $replacementAADAccountObj.UserPrincipalName # prida jen jako admina..ne primary admina (ownera) # Set-PnPTenantSite -Identity $_.site -Owners $replacementAADAccountObj.UserPrincipalName # prida jen jako admina..ne primary admina (ownera) } } } } #endregion ownership #region app Users and groups role assignments if ($_.AppUsersAndGroupsRoleAssignment) { $_.AppUsersAndGroupsRoleAssignment | % { "Removing $($_.PrincipalType) from app's '$($_.ResourceDisplayName)' role '$($_.AppRoleDisplayName)'" if (!$whatIf) { Remove-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $_.ResourceId -AppRoleAssignmentId $_.Id } } } #endregion app Users and groups role assignments #region devops if ($_.DevOpsOrganizationOwner) { $_.DevOpsOrganizationOwner | % { Write-Warning "Remove owner of DevOps organization '$($_.OrganizationName))' manually!" if ($replacementAADAccountObj) { Write-Warning "Add '$($replacementAADAccountObj.UserPrincipalName)' as new owner of the DevOps organization '$($_.OrganizationName))' manually!" } } } if ($_.DevOpsMemberOf) { $header = New-AzureDevOpsAuthHeader $_.DevOpsMemberOf | % { $accountDescriptor = $_.Descriptor $organizationName = $_.OrganizationName $_.memberOf | % { $groupDescriptor = $_.descriptor "Removing from DevOps organization's '$organizationName' group '$($_.principalName)'" if (!$whatIf) { $result = Invoke-WebRequest -Uri "https://vssps.dev.azure.com/$organizationName/_apis/graph/memberships/$accountDescriptor/$($groupDescriptor)?api-version=7.1-preview.1" -Method delete -ContentType "application/json" -Headers $header if ($result.StatusCode -ne 200) { Write-Error "Removal of account '$accountDisplayName' in DevOps organization '$organizationName' from group '$($_.displayName)' wasn't successful. Do it manually!" } } } } } #endregion devops #region keyVaultAccessPolicy if ($_.KeyVaultAccessPolicy) { $_.KeyVaultAccessPolicy | % { $vaultName = $_.VaultName $removedObjectId = $_.AccessPolicies.ObjectId | select -Unique "Removing Access from KeyVault $vaultName for '$removedObjectId'" if (!$whatIf) { Remove-AzKeyVaultAccessPolicy -VaultName $vaultName -ObjectId $removedObjectId -WarningAction SilentlyContinue } } } #endregion keyVaultAccessPolicy #region exchangeRole if ($_.ExchangeRole) { $_.ExchangeRole | % { $roleName = $_.name $roleDN = $_.RoleDisplayName if ($_.capabilities -eq 'Partner_Managed') { Write-Warning "Skipping removal of account '$($_.Identity)' from Exchange role $roleName. Role is not managed by Exchange, but via some external entity" } else { "Removing account '$($_.Identity)' from Exchange role '$roleName' ($roleDN)" if (!$whatIf) { Remove-RoleGroupMember -Confirm:$false -Identity $roleName -Member $_.Identity -BypassSecurityGroupManage rCheck } } } } #endregion exchangeRole #endregion remove AAD account occurrences # save object with made changes for later email notification if ($informNewManOwn -and $replacementAADAccountObj) { $newManOwnReport += $newManOwnObj } } } end { if ($informNewManOwn -and $newManOwnReport.count) { $newManOwnReport | % { if ($_.message) { # there were some changes in ownership if ($_.newUserEmail) { # new owner/manager has email address defined if ($replaceByManager) { $newUserRole = "as his/her manager" } else { $newUserRole = "as chosen successor" } $body = "Hi,`nemployee '$($_.replacedUserName)' left the company and you $newUserRole are now:`n`n$(($_.message | % {" - $_"}) -join "`n")`n`nThese changes are related to Azure environment.`n`n`Sincerely your IT" Write-Warning "Sending email to: $($_.newUserEmail) body:`n`n$body" Send-Email -to $_.newUserEmail -subject "Notification of new Azure assets responsibility" -body $body } else { Write-Warning "Cannot inform new owner/manager '$($_.newUserName)' about transfer of Azure asset from '$($_.replacedUserName)'. Email address is missing.`n`n$($_.message -join "`n")" } } else { Write-Verbose "No asset was transferred to the '$($_.newUserName)' from the '$($_.replacedUserName)'" } } } } } function Remove-AzureAppUserConsent { <# .SYNOPSIS Function for removing permission consents. .DESCRIPTION Function for removing permission consents. For selected OAuth2PermissionGrantId(s) or OGV with filtered grants will be shown (based on servicePrincipalObjectId, principalObjectId, resourceObjectId you specify). .PARAMETER OAuth2PermissionGrantId ID of the OAuth permission grant(s). .PARAMETER servicePrincipalObjectId ObjectId of the enterprise app for which was the consent given. .PARAMETER principalObjectId ObjectId of the user which have given the consent. .PARAMETER resourceObjectId ObjectId of the resource to which the consent have given permission to. .EXAMPLE Remove-AzureAppUserConsent -OAuth2PermissionGrantId L5awNI6RwE-QWiIIWcNMqYIrr-lfQ2BBnaYK1kev_X5Q2a7DBw0rSKTgiBsrZi4z Consent with ID L5awNI6RwE-QWiIIWcNMqYIrr-lfQ2BBnaYK1kev_X5Q2a7DBw0rSKTgiBsrZi4z will be deleted. .EXAMPLE Remove-AzureAppUserConsent OGV with all grants will be shown and just selected consent(s) will be deleted. .EXAMPLE Remove-AzureAppUserConsent -principalObjectId 1234 -servicePrincipalObjectId 5678 OGV with consent(s) related to user with ID 1234 and enterprise application with ID 5678 will be shown and just selected consent(s) will be deleted. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = "id")] [string[]] $OAuth2PermissionGrantId, [Parameter(ParameterSetName = "filter")] [string] $servicePrincipalObjectId, [Parameter(ParameterSetName = "filter")] [string] $principalObjectId, [Parameter(ParameterSetName = "filter")] [string] $resourceObjectId ) $null = Connect-MgGraph -ea Stop $objectByObjectId = @{} function GetObjectByObjectId ($objectId) { if (!$objectByObjectId.ContainsKey($objectId)) { Write-Verbose ("Querying Azure AD for object '{0}'" -f $objectId) try { $object = Get-MgDirectoryObjectById -Ids $objectId -ea stop $objectByObjectId.$objectId = $object return $object } catch { Write-Verbose "Object not found." } } return $objectByObjectId.$objectId } if ($OAuth2PermissionGrantId) { $OAuth2PermissionGrantId | % { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $_ -Confirm:$true } } else { $filter = "" if ($servicePrincipalObjectId) { if ($filter) { $filter = $filter + " and " } $filter = $filter + "clientId eq '$servicePrincipalObjectId'" } if ($principalObjectId) { if ($filter) { $filter = $filter + " and " } $filter = $filter + "principalId eq '$principalObjectId'" } if ($resourceObjectId) { if ($filter) { $filter = $filter + " and " } $filter = $filter + "resourceId eq '$resourceObjectId'" } $param = @{} if ($filter) { $param.filter = $filter } Get-MgOauth2PermissionGrant @param -Property ClientId, ConsentType, PrincipalId, ResourceId, Scope, Id | select @{n = 'App'; e = { (GetObjectByObjectId $_.ClientId).DisplayName } }, ConsentType, @{n = 'Principal'; e = { (GetObjectByObjectId $_.PrincipalId).DisplayName } }, @{n = 'Resource'; e = { (GetObjectByObjectId $_.ResourceId).DisplayName } }, Scope, Id | Out-GridView -OutputMode Multiple | % { Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $_.Id -Confirm:$true } } } function Remove-AzureUserMemberOfDirectoryRole { <# .SYNOPSIS Function for removing given user from given Directory role. .DESCRIPTION Function for removing given user from given Directory role. .PARAMETER userId ID of the user. Can be retrieved using Get-MgUser. .PARAMETER roleId ID of the Directory role. Can be retrieved using Get-MgUserMemberOf. .EXAMPLE $aadUser = Get-MgUser -Filter "userPrincipalName eq '$UPN'" Get-MgUserMemberOf -UserId $aadUser.id -All | ? { $_.AdditionalProperties.'@odata.type' -eq "#microsoft.graph.directoryRole" } | % { Remove-AzureUserMemberOfDirectoryRole -userId $aadUser.id -roleId $_.id } #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $userId, [Parameter(Mandatory = $true)] [string] $roleId ) # Use this endpoint when using the role Id $uri = "https://graph.microsoft.com/v1.0/directoryRoles/$roleId/members/$userId/`$ref" # Use this endpoint when using the role template ID # $uri = "https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=$roleTemplateId/members/$userId/`$ref" $params = @{ Headers = (New-GraphAPIAuthHeader -ea Stop) Method = "Delete" Uri = $uri } Write-Verbose "Invoking DELETE method against '$uri'" Invoke-RestMethod @params } function Revoke-AzureServicePrincipalPermission { <# .SYNOPSIS Function for revoking granted application/delegated permissions from selected account. .DESCRIPTION Function for revoking granted application/delegated permissions from selected account. .PARAMETER servicePrincipalName Name of the service principal you want to revoke permission(s) from. .PARAMETER servicePrincipalId ObjectId of the service principal you want to revoke permissions(s) from. .PARAMETER resourceAppId ObjectId of the resource you want to revoke permission(s). By default ObjectId of the Graph API resource a.k.a. GraphAggregatorService service principal. .PARAMETER permissionList List of permissions you want to revoke. If not defined, Out-GridView table with all available permissions (of type defined in permissionType) will be interactively outputted, so the user can pick some. .PARAMETER permissionType Type of permission you want to revoke. Possible values are application, delegated. By default application is selected. .PARAMETER all Switch to remove all permissions (of type defined in permissionType parameter). .EXAMPLE Revoke-AzureServicePrincipalPermission -servicePrincipalName "otest" -permissionList AgreementAcceptance.Read.All Revoke 'application' permission 'AgreementAcceptance.Read.All' for Graph Api resource from 'otest' ent. app (service principal) .EXAMPLE Revoke-AzureServicePrincipalPermission -servicePrincipalName "otest" Shows table with all assigned 'application' type permissions for Graph Api, let the user pick some and revoke them from application "otest". .EXAMPLE Revoke-AzureServicePrincipalPermission -servicePrincipalName "otest" -permissionList AccessReview.Read.All, AccessReview.ReadWrite.Membership -permissionType delegated Revoke 'delegated' permissions 'AccessReview.Read.All, AccessReview.ReadWrite.Membership' for Graph Api resource from 'otest' ent. app (service principal) .EXAMPLE Revoke-AzureServicePrincipalPermission -servicePrincipalName "otest" -All -permissionType delegated Revoke all 'delegated' permissions for Graph Api resource from 'otest' ent. app (service principal) #> [CmdletBinding(DefaultParameterSetName = 'name')] param ( [Parameter(Mandatory = $true, ParameterSetName = "name")] [string] $servicePrincipalName, [Parameter(Mandatory = $true, ParameterSetName = "id")] [string] $servicePrincipalId, [string] $resourceAppId = '00000003-0000-0000-c000-000000000000', # graph api [ArgumentCompleter( { param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) $resourceAppId = $FakeBoundParams.resourceAppId if (!$resourceAppId) { $resourceAppId = '00000003-0000-0000-c000-000000000000' } $resourceServicePrincipal = Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'" -Property Id, AppRoles, Oauth2PermissionScopes if ($FakeBoundParams.servicePrincipalName) { $servicePrincipal = Get-MgServicePrincipal -Filter "displayName eq '$($FakeBoundParams.servicePrincipalName)'" } else { $servicePrincipal = (Get-MgServicePrincipal -ServicePrincipalId $FakeBoundParams.servicePrincipalId) } if (!$FakeBoundParams.permissionType -or $FakeBoundParams.permissionType -eq 'application') { $appRoleAssignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id | ? ResourceId -EQ $resourceServicePrincipal.Id $availablePermission = (Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'" -Property AppRoles).AppRoles | select Value, Id function _getScope { param ($availablePermission, $appRoleId) $availablePermission | ? Id -EQ $appRoleId | select -ExpandProperty Value } $appRoleAssignment | select @{n = 'scope'; e = { _getScope $availablePermission $_.AppRoleId } } | select -ExpandProperty scope | ? { $_ -like "*$WordToComplete*" } } else { (Get-MgOauth2PermissionGrant -Filter "clientId eq '$($servicePrincipal.Id)' and ResourceId eq '$($resourceServicePrincipal.Id)' and consentType eq 'AllPrincipals'").Scope -split " " | ? { $_ -like "*$WordToComplete*" } } })] [string[]] $permissionList, [ValidateSet('application', 'delegated')] [string] $permissionType = "application", [switch] $all ) if ($all -and $permissionList) { Write-Warning "Because 'All' parameter was used, 'permissionList' parameter will be ignored" } if ($all) { Write-Warning "All permissions of type '$permissionType' will be revoked" } # authenticate if ($permissionType -eq "application") { $graphScope = "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" } else { $graphScope = "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All" } $null = Connect-MgGraph -Scopes $graphScope -ea Stop # remove duplicates $permissionList = $permissionList | select -Unique # get account to which permissions will be revoked if ($servicePrincipalName) { $servicePrincipal = Get-MgServicePrincipal -Filter "displayName eq '$servicePrincipalName'" if (!$servicePrincipal) { throw "Service principal '$servicePrincipalName' doesn't exist" } } else { $servicePrincipal = (Get-MgServicePrincipal -ServicePrincipalId $servicePrincipalId) if (!$servicePrincipal) { throw "Service principal '$servicePrincipalId' doesn't exist" } } # get application whose permissions will be revoked $resourceServicePrincipal = Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'" -Property Id, DisplayName, AppRoles, Oauth2PermissionScopes if (!$resourceServicePrincipal) { throw "Resource '$resourceAppId' doesn't exist" } # get assigned permissions if ($permissionType -eq "application") { $appRoleAssignment = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipal.Id | ? ResourceId -EQ $resourceServicePrincipal.Id } else { $Oauth2PermissionGrant = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($servicePrincipal.Id)' and ResourceId eq '$($resourceServicePrincipal.Id)' and consentType eq 'AllPrincipals'" } if (!$appRoleAssignment -and !$Oauth2PermissionGrant) { Write-Warning "There are no permissions of '$permissionType' type assigned for resource $($resourceServicePrincipal.DisplayName) ($resourceAppId)" return } # get all assignable permissions if ($permissionType -eq "application") { $availablePermission = (Get-MgServicePrincipal -Filter "appId eq '$resourceAppId'" -Property AppRoles).AppRoles | ? Id -In $appRoleAssignment.AppRoleId | select Value, DisplayName, Description, Id } else { $availablePermission = $Oauth2PermissionGrant.Scope -split " " } # let the user pick permissions to remove interactively if (!$all -and !$permissionList) { if ($permissionType -eq "application") { $permissionList = $availablePermission | sort Value | Out-GridView -Title "Select $permissionType permission(s) you want to revoke" -OutputMode Multiple | select -ExpandProperty Value } else { $permissionList = $availablePermission | sort | Out-GridView -Title "Select $permissionType permission(s) you want to revoke" -OutputMode Multiple } if (!$permissionList) { throw "You haven't selected any permission" } } if ($permissionType -eq "application") { if ($all) { # remove all permissions Write-Warning "Removing all application permissions ($((($availablePermission.Value | sort ) -join ", ")))" $appRoleAssignment | % { Remove-MgServicePrincipalAppRoleAssignment -AppRoleAssignmentId $_.Id -ServicePrincipalId $servicePrincipal.Id } } else { # remove just some permissions $appRoleAssignment | ? AppRoleId -In ($availablePermission | ? Value -In $permissionList).Id | % { $permId = $_.Id $permValue = $availablePermission | ? Id -EQ ($appRoleAssignment | ? Id -EQ $permId).AppRoleId | select -ExpandProperty Value Write-Warning "Removing application permission ($permValue)" Remove-MgServicePrincipalAppRoleAssignment -AppRoleAssignmentId $_.Id -ServicePrincipalId $servicePrincipal.Id } } } else { if ($all) { # remove all permissions Write-Warning "Removing all delegated permissions ($(($availablePermission | sort ) -join ", "))" Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $Oauth2PermissionGrant.Id } else { # remove just some permissions $preservePermission = $availablePermission | ? { $_ -notin $permissionList } if ($preservePermission) { $params = @{ Scope = ($preservePermission -join " ") } Write-Warning "Removing selected delegated permissions ($(($permissionList | sort ) -join ", "))" $null = Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId $Oauth2PermissionGrant.Id -BodyParameter $params } else { # remove all permissions Write-Warning "Removing all delegated permissions ($(($availablePermission | sort ) -join ", "))" Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $Oauth2PermissionGrant.Id } } } } function Set-AzureAppCertificate { <# .SYNOPSIS Function for creating (or replacing existing) authentication certificate for selected AzureAD Application. .DESCRIPTION Function for creating (or replacing existing) authentication certificate for selected AzureAD Application. Use this function with cerPath parameter (if you already have existing certificate you want to add) or rest of the parameters (if you want to create it first). If new certificate will be create, it will be named using application ObjectID of the corresponding enterprise app. .PARAMETER appObjectId ObjectId of the Azure application registration, to which you want to assign certificate. .PARAMETER cerPath Path to existing '.cer' certificate which should be added to the application. .PARAMETER StartDate Datetime object defining since when certificate will be valid. Default value is now. .PARAMETER EndDate Datetime object defining to when certificate will be valid. Default value is 2 years from now. .PARAMETER Password Secure string with password that will protect certificate private key. Choose strong one! .PARAMETER directory Path to folder where pfx (cert. with private key) certificate will be exported. .PARAMETER dontRemoveFromCertStore Switch to NOT remove certificate from the local cert. store after it is created&exported to pfx. .EXAMPLE Set-AzureAppCertificate -appObjectId cc210920-4c75-48ad-868b-6aa2dbcd1d51 -cerPath C:\cert\appCert.cer Adds certificate 'appCert' to the Azure application cc210920-4c75-48ad-868b-6aa2dbcd1d51. .EXAMPLE Set-AzureAppCertificate -appObjectId cc210920-4c75-48ad-868b-6aa2dbcd1d51 -password (Read-Host -AsSecureString) Creates new self signed certificate, export it as pfx (cert with private key) into working directory and adds its public counterpart (.cer) to the Azure application cc210920-4c75-48ad-868b-6aa2dbcd1d51. Certificate private key will be protected by entered password and it will be valid 2 years from now. #> [CmdletBinding(DefaultParameterSetName = 'createCert')] param ( [Parameter(Mandatory = $true, ParameterSetName = "cerExists")] [Parameter(Mandatory = $true, ParameterSetName = "createCert")] [string] $appObjectId, [Parameter(Mandatory = $true, ParameterSetName = "cerExists")] [ValidateScript( { if ($_ -match ".cer$" -and (Test-Path -Path $_)) { $true } else { throw "$_ is not a .cer file or doesn't exist" } })] [string] $cerPath, [Parameter(Mandatory = $false, ParameterSetName = "createCert")] [DateTime] $startDate = (Get-Date), [Parameter(Mandatory = $false, ParameterSetName = "createCert")] [ValidateScript( { if ($_ -gt (Get-Date)) { $true } else { throw "$_ has to be in the future" } })] [DateTime] $endDate = (Get-Date).AddYears(2), [Parameter(Mandatory = $true, ParameterSetName = "createCert")] [SecureString]$password, [Parameter(Mandatory = $false, ParameterSetName = "createCert")] [ValidateScript( { if (Test-Path -Path $_ -PathType Container) { $true } else { throw "$_ is not a folder or doesn't exist" } })] [string] $directory = (Get-Location), [switch] $dontRemoveFromCertStore ) $null = Connect-MgGraph -ea Stop # test that app exists try { $application = Get-MgApplication -ApplicationId $appObjectId -ErrorAction Stop } catch { throw "Application registration with ObjectId $appObjectId doesn't exist" } $appCert = $application | select -exp KeyCredentials if ($appCert | ? EndDateTime -GT ([datetime]::Today)) { $choice = "" while ($choice -notmatch "^[Y|N]$") { $choice = Read-Host "There is a valid certificate(s) already. Do you really want to REPLACE it?! (Y|N)" } if ($choice -eq "N") { break } } if ($cerPath) { $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2($cerPath) } else { Write-Warning "Creating self signed certificate named '$appObjectId'" $cert = New-SelfSignedCertificate -CertStoreLocation 'cert:\currentuser\my' -Subject "CN=$appObjectId" -NotBefore $startDate -NotAfter $endDate -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 Write-Warning "Exporting '$appObjectId.pfx' to '$directory'" $pfxFile = Join-Path $directory "$appObjectId.pfx" $path = 'cert:\currentuser\my\' + $cert.Thumbprint $null = Export-PfxCertificate -Cert $path -FilePath $pfxFile -Password $password if (!$dontRemoveFromCertStore) { Write-Verbose "Removing created certificate from cert. store" Get-ChildItem 'cert:\currentuser\my' | ? { $_.thumbprint -eq $cert.Thumbprint } | Remove-Item } } # $keyValue = [System.Convert]::ToBase64String($cert.GetRawCertData()) # $base64Thumbprint = [System.Convert]::ToBase64String($cert.GetCertHash()) # $endDateTime = ($cert.NotAfter).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" ) # $startDateTime = ($cert.NotBefore).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" ) Write-Warning "Adding certificate to the application $($application.DisplayName)" # toto funguje s update-mgaaplication $keyCredentialParams = @{ DisplayName = "certificate" # in reality this sets description field :D Type = "AsymmetricX509Cert" Usage = "Verify" Key = $cert.GetRawCertData() # StartDateTime = ($cert.NotBefore).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" ) # EndDateTime = ($cert.NotAfter).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" ) } Update-MgApplication -ApplicationId $appObjectId -KeyCredential $keyCredentialParams } function Set-AzureDeviceExtensionAttribute { <# .SYNOPSIS Function for setting Azure device ExtensionAttribute. .DESCRIPTION Function for setting Azure device ExtensionAttribute. .PARAMETER deviceName Device name. .PARAMETER deviceId Device ID as returned by Get-MGDevice command. Can be used instead of device name. .PARAMETER extensionId Id number of the extension you want to set. Possible values are 1-15. .PARAMETER extensionValue Value you want to set. If empty, currently set value will be removed. .PARAMETER scope Permissions you want to use for connecting to Graph. Default is 'Directory.AccessAsUser.All' and can be used if you have Global or Intune administrator role. Possible values are: 'Directory.AccessAsUser.All', 'Device.ReadWrite.All', 'Directory.ReadWrite.All' .EXAMPLE Set-AzureDeviceExtensionAttribute -deviceName nn-69-ntb -extensionId 1 -extensionValue 'ntb' On device nn-69-ntb set value 'ntb' into device ExtensionAttribute1. .EXAMPLE Set-AzureDeviceExtensionAttribute -deviceName nn-69-ntb -extensionId 1 On device nn-69-ntb empty current value saved in device ExtensionAttribute1. .NOTES https://blogs.aaddevsup.xyz/2022/05/how-to-use-microsoft-graph-sdk-for-powershell-to-update-a-registered-devices-extension-attribute/?utm_source=rss&utm_medium=rss&utm_campaign=how-to-use-microsoft-graph-sdk-for-powershell-to-update-a-registered-devices-extension-attribute #> [CmdletBinding(DefaultParameterSetName = 'deviceName')] param ( [Parameter(Mandatory = $true, ParameterSetName = "deviceName")] [string] $deviceName, [Parameter(Mandatory = $true, ParameterSetName = "deviceId")] [string] $deviceId, [Parameter(Mandatory = $true)] [ValidateRange(1, 15)] $extensionId, [string] $extensionValue, [ValidateSet('Directory.AccessAsUser.All', 'Device.ReadWrite.All', 'Directory.ReadWrite.All')] [string] $scope = 'Directory.AccessAsUser.All' ) #region checks if (!(Get-Module "Microsoft.Graph.Authentication" -ListAvailable -ea SilentlyContinue)) { throw "Microsoft.Graph.Authentication module is missing" } if (!(Get-Module "Microsoft.Graph.Identity.DirectoryManagement" -ListAvailable -ea SilentlyContinue)) { throw "Microsoft.Graph.Identity.DirectoryManagement module is missing" } #endregion checks # connect to Graph $null = Connect-MgGraph -Scopes $scope # get the device if ($deviceName) { $device = Get-MgDevice -Filter "DisplayName eq '$deviceName'" if (!$device) { throw "Device $deviceName wasn't found" } } else { $device = Get-MgDeviceById -DeviceId $deviceId -ErrorAction SilentlyContinue if (!$device) { throw "Device $deviceId wasn't found" } $deviceName = $device.DisplayName } if ($device.count -gt 1) { throw "There are more than one devices with name $device. Use DeviceId instead." } # get current value saved in attribute $currentExtensionValue = $device.AdditionalProperties.extensionAttributes."extensionAttribute$extensionId" # set attribute if necessary if (($currentExtensionValue -eq $extensionValue) -or ([string]::IsNullOrEmpty($currentExtensionValue) -and [string]::IsNullOrEmpty($extensionValue))) { Write-Warning "New extension value is same as existing one set in extensionAttribute$extensionId on device $deviceName. Skipping" } else { if ($extensionValue) { $verb = "Setting '$extensionValue' to" } else { $verb = "Emptying" } Write-Warning "$verb extensionAttribute$extensionId on device $deviceName (previous value was '$currentExtensionValue')" # prepare value hash $params = @{ "extensionAttributes" = @{ "extensionAttribute$extensionId" = $extensionValue } } Update-MgDevice -DeviceId $device.id -BodyParameter ($params | ConvertTo-Json) } } function Set-AzureRingGroup { <# .SYNOPSIS Function for dynamically setting members of specified "ring" groups based on the provided users list (members of the rootGroup) and the members per group percent ratio (ringGroupConfig). Useful if you want to deploy some feature gradually (ring by ring). "Ring" group concept is inspired by Intune Autopatch deployment rings. .DESCRIPTION Function for dynamically setting members of specified "ring" groups based on the provided users list (members of the rootGroup) and the members per group percent ratio (ringGroupConfig). Useful if you want to deploy some feature gradually (ring by ring). "Ring" group concept is inspired by Intune Autopatch deployment rings. With each function run, members and their ratio is checked and a rebalance of members is made if needed. Ring groups can contain only accounts that are members of the root group too! Ring groups description will be automatically updated with each run of this function. It will contain date of the last update and some generated text about how many percent of the root group this group contains. .PARAMETER rootGroup Id of the Azure group which members should be distributed across all ring groups based on the percent weight specified in the "ringGroupConfig". Members are searched recursively! Only users or devices accounts are used based on 'memberType'. .PARAMETER ringGroupConfig Ordered hashtable where keys are IDs of the Azure "ring" groups and values are integers representing percent of the "rootGroup" group members this "ring" group should contain. Sum of the values must be 100 at total. Example: [ordered]@{ 'bcf239e9-6a5e-4de0-baf4-c14bda4c0571' = 5 # ring_1 '19fe5c4c-7568-43a3-bd21-f95cb5547366' = 15 # ring_2 '0db6da9f-c224-4252-a7dc-c31d55b3acb3' = 80 # ring_3 } .PARAMETER forceRecalculate Use if you want to force members check even though count of the root group members is the same as of all ring groups members (to overwrite manual edits etc) .PARAMETER firstRingGroupMembersSetManually Switch to specify that first group in ringGroupConfig is being manually set a.k.a skipped in re-balancing process. Therefore its value in ringGroupConfig must be set to 0 (because members are added manually). Percent weight (specified in ringGroupConfig) of the rest of the ring groups is used only for re-balancing users that are non-first-ring-group members. .PARAMETER skipUnderscoreInNameCheck Switch for skipping check that all "ring" groups that have dynamically set members have '_' prefix in their name (name convention). .PARAMETER includeDisabled Switch for including also disabled members of the root group, otherwise just enabled will be used to fill the "ring" groups. .PARAMETER skipDescriptionUpdate Switch for not modifying ring groups description. .PARAMETER memberType Type of the "rootGroup" you want to set on "ring" groups. Possible values: User, Device. By default 'User'. .EXAMPLE # group whose members will be distributed between ring groups $rootGroup = "330a6543-da12-4999-bf87-a0ae60g28bbc" # ring groups configuration $ringGroupConfig = [ordered]@{ # manually set members '9e6be2e2-c050-4887-b14c-e612a1b4bb48' = 0 # ring_0 # automatically set members 'bcf239e9-6a5e-4de0-baf4-c14bda4c0a71' = 5 # ring_1 '19fe5c4c-7568-43a3-bd21-f95cb5547766' = 15 # ring_2 '0db6da9f-c224-4252-a7dc-c31d55b9acb3' = 80 # ring_3 } Set-AzureRingGroup -rootGroup $rootGroup -ringGroupConfig $ringGroupConfig -firstRingGroupMembersSetManually Members of the root group (minus members of the first "ring" group) will be distributed across rest of the "ring" groups by percent ratio selected in the $ringGroupConfig. Members of the first "ring" group stay intact. In case current "ring" groups members count doesn't correspond to the percent specified in the $ringGroupConfig, members will be removed/added accordingly. .EXAMPLE # group whose members will be distributed between ring groups $rootGroup = "330a6543-da12-4999-bf87-a0ae60g28bbc" # ring groups configuration $ringGroupConfig = [ordered]@{ 'bcf239e9-6a5e-4de0-baf4-c14bda4c0a71' = 5 # ring_1 '19fe5c4c-7568-43a3-bd21-f95cb5547766' = 15 # ring_2 '0db6da9f-c224-4252-a7dc-c31d55b9acb3' = 80 # ring_3 } Set-AzureRingGroup -rootGroup $rootGroup -ringGroupConfig $ringGroupConfig Members of the root group will be distributed across the "ring" groups by percent ratio selected in the $ringGroupConfig. In case current "ring" groups members count doesn't correspond to the percent specified in the $ringGroupConfig, members will be removed/added accordingly. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [guid] $rootGroup, [Parameter(Mandatory = $true)] [System.Collections.Specialized.OrderedDictionary] $ringGroupConfig, [switch] $forceRecalculate, [switch] $firstRingGroupMembersSetManually, [switch] $skipUnderscoreInNameCheck, [switch] $includeDisabled, [switch] $skipDescriptionUpdate, [ValidateSet('User', 'Device')] [string] $memberType = 'User' ) if (!(Get-Command Get-MgContext -ErrorAction silentlycontinue) -or !(Get-MgContext)) { throw "$($MyInvocation.MyCommand): Authentication needed. Please call Connect-MgGraph." } #region functions function _getGroupName { param ($id) return (Get-MgGroup -GroupId $id -Property displayname).displayname } function _getMemberName { param ($id) return (Get-MgDirectoryObject -DirectoryObjectId $id).AdditionalProperties.displayName } function _setRingGroupsDescription { "Updating ring groups description" $ringGroupConfig.Keys | % { $groupId = $_ $value = $ringGroupConfig.$groupId $ring0GroupId = $($ringGroupConfig.Keys)[0] if ($firstRingGroupMembersSetManually -and $groupId -eq $ring0GroupId) { $description = "Contains selected $($memberType.ToLower()) members of the $(_getGroupName $rootGroup) group. Members are assigned manually. Last processed at $(Get-Date -Format 'yyyy.MM.dd_HH:mm')" } else { $description = "Contains cca $value% $($memberType.ToLower()) members of the $(_getGroupName $rootGroup) group. Members are assigned programmatically. Last processed at $(Get-Date -Format 'yyyy.MM.dd_HH:mm')" } Update-MgGroup -GroupId $groupId -Description $description } } #endregion functions if ($firstRingGroupMembersSetManually) { # first ring group has manually set members # some exceptions in checks etc needs to be made $ring0GroupId = $($ringGroupConfig.Keys)[0] } else { # first ring group has automatically set members (as the rest of the ring groups) # no extra treatment is needed $ring0GroupId = $null } #region checks # all groups exists $allGroupId = @() $allGroupId += $rootGroup $ringGroupConfig.Keys | % { $allGroupId += $_ } $allGroupId | % { $groupId = $_ try { $null = [guid] $groupId } catch { throw "$groupId isn't valid group ID" } try { $null = Get-MgGroup -GroupId $groupId -Property displayname -ErrorAction Stop } catch { throw "Group with ID $groupId that is defined in `$ringGroupConfig doesn't exist" } } # all automatically filled ring groups should have '_' prefix (naming convention) if (!$skipUnderscoreInNameCheck) { $ringGroupConfig.Keys | % { $groupId = $_ if (!$firstRingGroupMembersSetManually -or $groupId -ne $ring0GroupId) { $groupName = _getGroupName $groupId if ($groupName -notlike "_*") { throw "Group $groupName ($groupId) doesn't have prefix '_'. It has dynamically set members therefore it should!" } } } } # beta ring group has 0% set as assigned members count if ($firstRingGroupMembersSetManually -and $ringGroupConfig[0] -ne 0) { throw "First group in `$ringGroupConfig is manually filled a.k.a. value must be set to 0 (now $($ringGroupConfig[0]))" } # sum of all ring groups assigned members percent is 100% at total $ringGroupPercentSum = $ringGroupConfig.Values | Measure-Object -Sum | select -ExpandProperty Sum if ($ringGroupPercentSum -ne 100) { throw "Total sum of groups percent has to be 100 (now $ringGroupPercentSum)" } #endregion checks # make a note that group was processed, by updating its description if (!$skipDescriptionUpdate) { _setRingGroupsDescription } # get all users/devices that should be assigned to the "ring" groups $rootGroupMember = Get-AzureGroupMemberRecursive -Id $rootGroup -excludeDisabled:(!$includeDisabled) -allowedMemberType $memberType #region cleanup of members that are no longer in the root group or are placed in more than one group $memberOccurrence = @{} $ringGroupConfig.Keys | % { $groupId = $_ Get-MgGroupMember -GroupId $groupId -All -Property Id | % { $memberId = $_.Id if ($memberId -notin $rootGroupMember.Id) { Write-Warning "Removing group's $(_getGroupName $groupId) member $(_getMemberName $memberId) (not in the root group)" Remove-MgGroupMemberByRef -GroupId $groupId -DirectoryObjectId $memberId } else { if ($memberOccurrence.$memberId) { Write-Warning "Removing group's $(_getGroupName $groupId) member $(_getMemberName $memberId) (already member of the group $(_getGroupName $memberOccurrence.$memberId))" Remove-MgGroupMemberByRef -GroupId $groupId -DirectoryObjectId $memberId } else { $memberOccurrence.$memberId = $groupId } } } } #endregion cleanup of members that are no longer in the root group or are placed in more than one group $ringGroupsMember = $ringGroupConfig.Keys | % { Get-MgGroupMember -GroupId $_ -All -Property Id } $rootGroupMemberCount = $rootGroupMember.count $ringGroupsMemberCount = $ringGroupsMember.count if ($firstRingGroupMembersSetManually) { # set percent weight is calculated from all available members except the manually set members of the test (ring0) group $ring0GroupMember = Get-MgGroupMember -GroupId $ring0GroupId -All -Property Id $assignableRingGroupsMemberCount = $rootGroupMemberCount - $ring0GroupMember.count } else { $assignableRingGroupsMemberCount = $rootGroupMemberCount } if ($rootGroupMemberCount -eq $ringGroupsMemberCount -and !$forceRecalculate) { return "No change in members count detected. Exiting" } # contains users/devices that are members of the root group, but not of any ring group # plus users/devices that were removed from any ring group for redundancy a.k.a. should be relocate to another ring group $memberToRelocateList = New-Object System.Collections.ArrayList ($rootGroupMember).Id | % { if ($_ -notin $ringGroupsMember.Id) { $null = $memberToRelocateList.Add($_) } } # hashtable with group ids and number of members that should be added $groupWithMissingMember = @{} # remove obsolete/redundancy ring group members if ($assignableRingGroupsMemberCount -ne 0) { foreach ($groupId in $ringGroupConfig.Keys) { if ($firstRingGroupMembersSetManually -and $groupId -eq $ring0GroupId) { # ring0 group is manually filled, hence no checks on members count are needed continue } $groupMember = Get-MgGroupMember -GroupId $groupId -All -Property Id $groupCurrentMemberCount = $groupMember.count if ($groupCurrentMemberCount) { $groupCurrentWeight = [math]::round($groupCurrentMemberCount / $assignableRingGroupsMemberCount * 100) } else { $groupCurrentWeight = 0 } $groupRequiredWeight = $ringGroupConfig.$groupId $groupRequiredMemberCount = [math]::round($assignableRingGroupsMemberCount / 100 * $groupRequiredWeight) if ($groupRequiredMemberCount -eq 0 -and $groupRequiredWeight -gt 0) { # assign at least one member $groupRequiredMemberCount = 1 } if ($groupCurrentMemberCount -ne $groupRequiredMemberCount) { "Group $(_getGroupName $groupId) ($groupCurrentMemberCount member(s)) should contain $groupRequiredWeight% ($groupRequiredMemberCount member(s)) of all assignable ($assignableRingGroupsMemberCount) users/devices, but contains $groupCurrentWeight%" if ($groupCurrentMemberCount -gt $groupRequiredMemberCount) { # remove some random users/devices $memberToRelocate = Get-Random -InputObject $groupMember.Id -Count ($groupCurrentMemberCount - $groupRequiredMemberCount) $memberToRelocate | % { $memberId = $_ Write-Warning "Removing group's $(_getGroupName $groupId) member $(_getMemberName $memberId) (is over the set limit)" Remove-MgGroupMemberByRef -GroupId $groupId -DirectoryObjectId $memberId $null = $memberToRelocateList.Add($memberId) } } else { # make a note about how many members should be added (later, because at first I need to free up/remove them from their current groups) $groupWithMissingMember.$groupId = $groupRequiredMemberCount - $groupCurrentMemberCount } } } } # add new members to ring groups that have less members than required if ($groupWithMissingMember.Keys) { # add some random users/devices from the pool of available users/devices # start with the group with least required members, because of the rounding there might not be enough of them for all groups and you want to have the testing groups filled foreach ($groupId in ($groupWithMissingMember.Keys | Sort-Object -Property { $ringGroupConfig.$_ })) { $memberToRelocateCount = $groupWithMissingMember.$groupId if ($memberToRelocateList.count -eq 0) { Write-Warning "There is not enough members left. Adding no members to the group $(_getGroupName $groupId) instead of $memberToRelocateCount" } else { if ($memberToRelocateList.count -lt $memberToRelocateCount) { Write-Warning "There is not enough members left. Adding $($memberToRelocateList.count) instead of $memberToRelocateCount to the group $(_getGroupName $groupId)" $memberToRelocateCount = $memberToRelocateList.count } $memberToAdd = Get-Random -InputObject $memberToRelocateList -Count $memberToRelocateCount $memberToAdd | % { $memberId = $_ Write-Warning "Adding member $(_getMemberName $memberId) to the group $(_getGroupName $groupId)" $params = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$memberId" } New-MgGroupMemberByRef -GroupId $groupId -BodyParameter $params $null = $memberToRelocateList.Remove($memberId) } } } } if ($memberToRelocateList) { # this shouldn't happen? throw "There are still some unassigned users/devices left?!" } } function Start-AzureSync { <# .SYNOPSIS Invoke Azure AD sync cycle command (Start-ADSyncSyncCycle) on the server where 'Azure AD Connect' is installed. .DESCRIPTION Invoke Azure AD sync cycle command (Start-ADSyncSyncCycle) on the server where 'Azure AD Connect' is installed. .PARAMETER Type Type of sync. Initial (full) or just delta. Delta is default. .PARAMETER ADSynchServer Name of the server where 'Azure AD Connect' is installed .EXAMPLE Start-AzureSync -ADSynchServer ADSYNCSERVER Invokes synchronization between on-premises AD and AzureAD on server ADSYNCSERVER by running command Start-ADSyncSyncCycle there. #> [Alias("Sync-ADtoAzure", "Start-AzureADSync")] [cmdletbinding()] param ( [ValidateSet('delta', 'initial')] [string] $type = 'delta', [ValidateNotNullOrEmpty()] [string] $ADSynchServer = $_ADSynchServer ) $ErrState = $false do { try { Invoke-Command -ScriptBlock { Start-ADSyncSyncCycle -PolicyType $using:type } -ComputerName $ADSynchServer -ErrorAction Stop | Out-Null $ErrState = $false } catch { $ErrState = $true Write-Warning "Start-AzureSync: Error in Sync:`n$_`nRetrying..." Start-Sleep 5 } } while ($ErrState -eq $true) } Export-ModuleMember -function Add-AzureAppUserConsent, Add-AzureGuest, Disable-AzureGuest, Get-AzureAccountOccurrence, Get-AzureAppConsentRequest, Get-AzureAppRegistration, Get-AzureAppVerificationStatus, Get-AzureAssessNotificationEmail, Get-AzureAuthenticatorLastUsedDate, Get-AzureCompletedMFAPrompt, Get-AzureDeviceWithoutBitlockerKey, Get-AzureEnterpriseApplication, Get-AzureGroupMemberRecursive, Get-AzureGroupSettings, Get-AzureManagedIdentity, Get-AzureResource, Get-AzureRoleAssignments, Get-AzureServiceAccount, Get-AzureServicePrincipalBySecurityAttribute, Get-AzureServicePrincipalOverview, Get-AzureServicePrincipalPermissions, Get-AzureServicePrincipalUsersAndGroups, Get-AzureSkuAssignment, Get-AzureSkuAssignmentError, Get-AzureUserAuthMethodChanges, Grant-AzureServicePrincipalPermission, New-AzureAutomationModule, Open-AzureAdminConsentPage, Remove-AzureAccountOccurrence, Remove-AzureAppUserConsent, Remove-AzureUserMemberOfDirectoryRole, Revoke-AzureServicePrincipalPermission, Set-AzureAppCertificate, Set-AzureDeviceExtensionAttribute, Set-AzureRingGroup, Start-AzureSync Export-ModuleMember -alias Get-AzureADAccountOccurrence, Get-AzureIAMRoleAssignments, Get-AzureRBACRoleAssignments, Get-AzureSPPermissions, Get-MgGroupMemberRecursive, New-AzAutomationModule2, New-AzureADGuest, Remove-AzureADAccountOccurrence, Remove-AzureADGuest, Start-AzureADSync, Sync-ADtoAzure |