shiftavenue.GraphAutomation.psm1
<# .SYNOPSIS Disables or enables a service principal or user account in Azure AD. .DESCRIPTION Disables or enables a service principal or user account in Azure AD. .PARAMETER PrincipalId The principal id to disable or enable. User can be guid or SPN .PARAMETER AccountType The type of account to disable or enable. Valid values are 'servicePrincipal' or 'user'. .PARAMETER Enabled Whether to enable or disable the account. .EXAMPLE Set-SagaAccountStatus -ServicePrincipalAppId '00000000-0000-0000-0000-000000000000' -AccountType 'servicePrincipal' -Enabled $false Disables the service principal with the app id '00000000-0000-0000-0000-000000000000'. #> function Set-SagaAccountStatus { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $PrincipalId, [Parameter()] [ValidateSet('servicePrincipal', 'user')] [string] $AccountType = 'servicePrincipal', [Parameter(Mandatory = $true)] [bool] $Enabled ) $accountEnabledCounter = 1 $idToSp = @{} $disableRequest = foreach ($sp in $PrincipalId) { @{ url = "$($AccountType)s/$($sp)" method = "PATCH" id = $accountEnabledCounter body = @{"accountEnabled" = $Enabled } headers = @{ "Content-Type" = "application/json" } } $idToSp[$accountEnabledCounter] = $sp $accountEnabledCounter++ } $responses = Invoke-GraphRequestBatch -Request $disableRequest foreach ($response in $responses) { $sp = $idToSp[$response.id] if ($response.status -in 200 - 299) { Write-PSFMessage -Message "Disabled '$($sp)'" } else { Write-PSFMessage -Message "Error disabling '$($sp)': $($response.body.error.message)" -Level Error } } } <# .SYNOPSIS Add permissions to a user, group, or service principal. .DESCRIPTION Add permissions to a user, group, or service principal. .PARAMETER ServicePrincipalDisplayName The display name of the service principals to add permissions to. .PARAMETER GroupDisplayName The display name of the groups to add permissions to. .PARAMETER UserPrincipalName The user principal name of the users to add permissions to. .PARAMETER ApplicationId The application id of the resource to add permissions to. Default is Graph. .PARAMETER AppRoleId The app role id to add. .EXAMPLE Add-SagaAppPermission -AppRoleId bf7b1a76-6e77-406b-b258-bf5c7720e98f -ServicePrincipalDisplayName 'MyServicePrincipal' Add Group.Create permissions on the app with display name MyServicePrincipal. #> function Add-SagaAppPermission { param ( [string[]] $ServicePrincipalDisplayName, [string[]] $GroupDisplayName, [string[]] $UserPrincipalName, [string] $ApplicationId = "00000003-0000-0000-c000-000000000000", # Graph [Parameter(Mandatory = $true)] [string[]] $AppRoleId ) Connect-SagaGraph if ($ServicePrincipalDisplayName.Count -eq 0 -and $GroupDisplayName.Count -eq 0 -and $UserPrincipalName.Count -eq 0) { Write-PSFMessage -Level Error -Message "What do you want to add permissions to? Specify at least one of ServicePrincipalName, GroupName, or UserName." return } # Get Resource App ID $resource = MiniGraph\Invoke-GraphRequest -Query "servicePrincipals(appId='$ApplicationId')" $counter = 1 $requests = [System.Collections.Generic.List[hashtable]]::new() foreach ($group in $GroupDisplayName) { $sp = MiniGraph\Invoke-GraphRequest -Query "groups?`$filter=displayName eq '$group'" foreach ($role in $AppRoleId) { $requests.Add(@{ url = "groups/$($sp.id)/appRoleAssignments" method = "POST" id = $counter body = @{ principalId = $sp.id resourceId = $resource.id appRoleId = $role } headers = @{ "Content-Type" = "application/json" } }) $counter++ } } foreach ($user in $UserPrincipalName) { $sp = MiniGraph\Invoke-GraphRequest -Query "users?`$filter=userPrincipalName eq '$user'" foreach ($role in $AppRoleId) { $requests.Add(@{ url = "users/$($sp.id)/appRoleAssignments" method = "POST" id = $counter body = @{ principalId = $sp.id resourceId = $resource.id appRoleId = $role } headers = @{ "Content-Type" = "application/json" } }) $counter++ } } foreach ($principal in $ServicePrincipalDisplayName) { $sp = MiniGraph\Invoke-GraphRequest -Query "servicePrincipals?`$filter=displayName eq '$principal'" foreach ($role in $AppRoleId) { $requests.Add(@{ url = "servicePrincipals/$($sp.id)/appRoleAssignments" method = "POST" id = $counter body = @{ principalId = $sp.id resourceId = $resource.id appRoleId = $role } headers = @{ "Content-Type" = "application/json" } }) $counter++ } } MiniGraph\Invoke-GraphRequestBatch -Request $requests } <# .SYNOPSIS Connects to the Microsoft Graph API. .DESCRIPTION Connects to the Microsoft Graph API with the configured settings, Get-PSFConfig -Module shiftavenue.GraphAutomation. If the connection is already established, this function does nothing. .EXAMPLE Connect-SagaGraph Connects to the Microsoft Graph API with the configured settings. #> function Connect-SagaGraph { [CmdletBinding()] param ( ) $clientId = Get-PSFConfigValue -FullName shiftavenue.GraphAutomation.GraphClientId $tenantId = Get-PSFConfigValue -FullName shiftavenue.GraphAutomation.GraphTenantId $graphMethod = Get-PSFConfigValue -FullName shiftavenue.GraphAutomation.GraphConnectionMode if ($graphMethod -ne 'Azure' -and (-not $clientId -or -not $tenantId)) { $msg = "Please configure GraphClientId and GraphTenantId, e.g. using'`nSet-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphClientId -Value '' -PassThru | Register-PSFConfig`nSet-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphTenantId -Value '' -PassThru | Register-PSFConfig" Stop-PSFFunction -Message $msg -EnableException $true } $mg = Get-Module -Name MiniGraph $alreadyConnected = & $mg { $null -ne $script:token } if ($alreadyConnected) { return } switch ($graphMethod) { 'DeviceCode' { Write-PSFMessage -Message 'Using DeviceCode Authentication' Connect-GraphDeviceCode -ClientID $ClientId -TenantID $TenantId } 'Certificate' { Write-PSFMessage -Message 'Using Certificate Authentication' $certificate = Get-PSFConfigValue -FullName shiftavenue.GraphAutomation.GraphCertificate if (-not $certificate) { Stop-PSFFunction -Message 'Please configure GraphCertificate or switch to DeviceCode/Browser auth' -EnableException } Connect-GraphCertificate -Certificate $certificate -ClientID $clientId -TenantID $tenantId } 'Browser' { Write-PSFMessage -Message 'Using Browser Authentication' Connect-GraphBrowser -ClientID $ClientId -TenantID $TenantId } 'ClientSecret' { Write-PSFMessage -Message 'Using ClientSecret Authentication' $clientSecret = Get-PSFConfigValue -FullName shiftavenue.GraphAutomation.GraphClientSecret if (-not $clientSecret) { Stop-PSFFunction -Message 'Please configure GraphClientSecret or switch to DeviceCode/Browser auth' -EnableException } Connect-GraphClientSecret -ClientID $ClientId -ClientSecret $ClientSecret -TenantID $TenantId } 'Azure' { Write-PSFMessage -Message 'Using Azure Authentication' Connect-GraphAzure } } Set-GraphEndpoint -Type beta } <# .SYNOPSIS Disables a service principal or user account in Entra ID. .DESCRIPTION Disables a service principal or user account in Entra ID. .PARAMETER PrincipalId The principal id to enable. User can be guid or SPN .PARAMETER AccountType The type of account to disable. Valid values are 'servicePrincipal' or 'user'. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. .EXAMPLE Disable-SagaPrincipal -ServicePrincipalAppId '00000000-0000-0000-0000-000000000000' -AccountType 'servicePrincipal' Disables the service principal with the app id '00000000-0000-0000-0000-000000000000'. #> function Disable-SagaPrincipal { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] [string[]] $PrincipalId, [Parameter()] [ValidateSet('servicePrincipal', 'user')] [string] $AccountType = 'servicePrincipal' ) Connect-SagaGraph if ($PSCmdlet.ShouldProcess("$($PrincipalId.Count) accounts", "Disable")) { Set-SagaAccountStatus -PrincipalId $PrincipalId -AccountType $AccountType -Enabled $false } } <# .SYNOPSIS Enables a service principal or user account in Entra ID. .DESCRIPTION Enables a service principal or user account in Entra ID. .PARAMETER PrincipalId The principal id to enable. User can be guid or SPN .PARAMETER AccountType The type of account to enable. Valid values are 'servicePrincipal' or 'user'. .EXAMPLE Enable-SagaPrincipal -ServicePrincipalAppId '00000000-0000-0000-0000-000000000000' -AccountType 'servicePrincipal' Enables the service principal with the app id '00000000-0000-0000-0000-000000000000'. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. #> function Enable-SagaPrincipal { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] [string[]] $PrincipalId, [Parameter()] [ValidateSet('servicePrincipal', 'user')] [string] $AccountType = 'servicePrincipal' ) Connect-SagaGraph if ($PSCmdlet.ShouldProcess("$($PrincipalId.Count) accounts", "Disable")) { Set-SagaAccountStatus -ServicePrincipalAppId $PrincipalId -AccountType $AccountType -Enabled $true } } <# .SYNOPSIS Exports all service principals and their permissions to an Excel file. .DESCRIPTION Exports all service principals and their permissions to an Excel file. .PARAMETER ServicePrincipal The service principal to export. .PARAMETER SingleReportPath The path to the Excel file to export the data to. .PARAMETER SummaryReportPath The path to the Excel file to export the summary data to. .EXAMPLE Export-SagaAppPermission -SingleReportPath 'C:\temp\GraphAppInventory.xlsx' -SummaryReportPath 'C:\temp\GraphAppInventorySummary.xlsx' Exports all service principals and their permissions to an Excel file. #> function Export-SagaAppPermission { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]] $ServicePrincipal, [Parameter()] [string] $SingleReportPath = "$(Get-Date -Format 'yyyy-MM-dd')_GraphAppInventory.xlsx", [Parameter()] $SummaryReportPath = "Report.xlsx" ) begin { $preparedOutput = [System.Collections.ArrayList]::new() } process { foreach ($sp in $ServicePrincipal) { $null = $preparedOutput.Add([PSCustomObject][ordered]@{ "Service Principal Name" = $SP.displayName "Application Name" = $SP.appDisplayName "Publisher" = if ($SP.PublisherName) { $SP.PublisherName } else { $null } "Verified" = if ($SP.verifiedPublisher.verifiedPublisherId) { $SP.verifiedPublisher.displayName } else { "Not verified" } "Homepage" = if ($SP.Homepage) { $SP.Homepage } else { $null } "Created on" = if ($SP.createdDateTime) { (Get-Date($SP.createdDateTime) -format g) } else { $null } "ApplicationId" = $SP.AppId "ObjectId" = $SP.id "AccountEnabled" = $SP.AccountEnabled "AccountEnabledDesiredState" = if ($SP.AccountEnabledDesiredState) { $SP.AccountEnabledDesiredState } else { $false } "Last modified" = $sp.lastModified "Permissions (application)" = $sp.permissionsByApplication -join '|' "Authorized By (application)" = $sp.authorizedByApplication "Permissions (delegate)" = $sp.delegatePermissions -join '|' "Valid until (delegate)" = $sp.delegateValidUntil "Authorized By (delegate)" = $sp.delegateAuthorizedBy -join '|' "SignIns last $TimeFrameInDays days" = $sp.signInsTimePeriod "Active Users $TimeFrameInDays days" = $sp.activeUsersTimePeriod "Detailed SignIns" = $sp.signInsTimePeriodDetail -join '|' "SignInAudience" = $sp.signInAudience } ) } } end { #Export the result to Excel file $preparedOutput | Export-Excel -Path $SingleReportPath $preparedOutput | Export-Excel -Path $SummaryReportPath -WorksheetName "$((Get-Date).ToString('yyyy-MM-dd_HH-mm-ss'))_GraphAppInv" -TableName "GraphAppInv_$((Get-Date).ToString('yyyy_MM_dd_HH_mm_ss'))" # Prep table Copy-Item -Path $SummaryReportPath -Destination "$SummaryReportPath.bak" -Force Remove-Worksheet -WorksheetName Reporting -Path $SummaryReportPath -ErrorAction SilentlyContinue $package = Open-ExcelPackage -Path $SummaryReportPath -KillExcel $summaryData = foreach ($worksheet in $package.Workbook.Worksheets.Where({ $_.Name -ne 'Reporting' })) { $doc = Import-Excel -Path $SummaryReportPath -WorksheetName $worksheet.Name [pscustomobject]@{ Date = $worksheet.Name 'NumberSPs' = $doc.Count 'NumberDisabled' = $doc.Where({ $_.PSObject.Properties.Name -contains 'Enabled' -and -not $_.Enabled }).Count 'NumberEnabled' = $doc.Where({ $_.PSObject.Properties.Name -contains 'Enabled' -and $_.Enabled }).Count } } $chart = New-ExcelChartDefinition -Title "Service principals over time" -ChartType Area -XRange 'Date' -YRange "NumberSPs", "NumberDisabled" -SeriesHeader "Number SPs", "Number disabled" $summaryData | Export-Excel -AutoNameRange -WorksheetName Reporting -MoveToStart -Path $SummaryReportPath -ExcelChartDefinition $chart } } <# .SYNOPSIS Gets the permissions of all service principals in the tenant. .DESCRIPTION Gets the permissions, OAuth scopes, Signins and delegations of all service principals in the tenant. .PARAMETER TimeFrameInDays The number of days to look back for signins. .PARAMETER ExcludeBuiltInServicePrincipals Whether to exclude built-in service principals. .PARAMETER ExcludeDisabledApps Whether to exclude disabled apps. .EXAMPLE Get-SagaAppPermission -TimeFrameInDays 30 -ExcludeBuiltInServicePrincipals -ExcludeDisabledApps Gets the permissions, OAuth scopes, Signins and delegations of all service principals in the tenant for the last 30 days. #> function Get-SagaAppPermission { [CmdletBinding()] [OutputType([System.Collections.ArrayList])] param ( [uint16] $TimeFrameInDays = 30, [bool] $ExcludeBuiltInServicePrincipals = $true, [bool] $ExcludeDisabledApps = $true ) $TimeFrameDate = (Get-Date -format u ((Get-Date).AddDays(-$TimeFrameInDays)).Date).Replace(' ', 'T') Connect-SagaGraph $servicePrincipals = [System.Collections.ArrayList]::new() if ($ExcludeBuiltInServicePrincipals) { $query = "servicePrincipals?&`$filter=tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp')" } $query = if (-not $ExcludeDisabledApps) { "servicePrincipals?&`$filter=accountEnabled eq true" } else { "servicePrincipals" } $servicePrincipals.AddRange([array](MiniGraph\Invoke-GraphRequest -Query $query)) $araCounter = 1 $idToSp = @{} $appRoleAssignmentsRequest = foreach ($sp in $servicePrincipals) { @{ url = "/servicePrincipals/$($sp.id)/appRoleAssignments" method = "GET" id = $araCounter } $idToSp[$araCounter] = $sp $araCounter++ } $responses = Invoke-GraphRequestBatch -Request $appRoleAssignmentsRequest foreach ($response in $responses) { if ($null -eq $response.id) { continue } $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'appRoleAssignments' -NotePropertyValue $response.body.value -Force } $assignedPrincipals = ($servicePrincipals | Where-Object { $_.appRoleAssignments.Count -gt 0 }).appRoleAssignments.resourceId | Select-Object -Unique foreach ($principal in $assignedPrincipals) { if ($servicePrincipals.id -notcontains $principal) { $null = $servicePrincipals.Add((MiniGraph\Invoke-GraphRequest -Query "servicePrincipals/$principal")) } } foreach ($sp in ($servicePrincipals | Where-Object { $_.appRoleAssignments.Count -gt 0 })) { $date = ($sp.appRoleAssignments.CreationTimestamp | Select-Object -Unique | Sort-Object -Descending | Select-Object -First 1) -as [datetime] if (-not $date) { $date = [DateTime]::MinValue } $sp | Add-Member -NotePropertyName 'lastModified' -NotePropertyValue $date.ToString('g') -Force $permissionsByApplication = foreach ($appRoleAssignment in $sp.appRoleAssignments) { $roleId = (($servicePrincipals | Where-Object id -eq $appRoleAssignment.resourceId).appRoles | Where-Object { $_.id -eq $appRoleAssignment.appRoleId }).Value | Select-Object -Unique if (!$roleID) { $roleId = "Orphaned ($($appRoleAssignment.appRoleId))" } "[$($appRoleAssignment.ResourceDisplayName)]:$($roleId -join ',')" } $sp | Add-Member -NotePropertyName permissionsByApplication -NotePropertyValue $permissionsByApplication -Force $sp | Add-Member -NotePropertyName authorizedByApplication -NotePropertyValue 'An administrator (application permissions)' -Force } $oauthCounter = 1 $idToSp = @{} $oauth2PermissionGrantsRequest = foreach ($sp in $servicePrincipals) { @{ url = "/servicePrincipals/$($sp.id)/oauth2PermissionGrants" method = "GET" id = $oauthCounter } $idToSp[$oauthCounter] = $sp $oauthCounter++ } $responses = Invoke-GraphRequestBatch -Request $oauth2PermissionGrantsRequest foreach ($response in $responses) { if ($null -eq $response.id) { continue } $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'oauth2PermissionGrants' -NotePropertyValue $response.body.value -Force } $users = [System.Collections.ArrayList]::new() $grantedPrincipals = $servicePrincipals | Where-Object { $_.oauth2PermissionGrants.Count -gt 0 } | ForEach-Object { $_.oauth2PermissionGrants.principalId } | Select-Object -Unique $userCounter = 1 $idToSp = @{} $grantedUsersRequest = foreach ($principal in $grantedPrincipals) { if ($users.id -notcontains $principal) { @{ url = "/users/$($principal)?`$select=UserPrincipalName" method = "GET" id = $userCounter } $idToSp[$userCounter] = $principal $userCounter++ } } if ($grantedUsersRequest) { $responses = Invoke-GraphRequestBatch -Request $grantedUsersRequest $users = @{} foreach ($response in $responses) { if ($null -eq $response.id) { continue } $users[$idToSp[[int]$response.id]] = $response.body.UserPrincipalName } } foreach ($sp in ($ServicePrincipals | Where-Object { $_.oauth2PermissionGrants.Count -gt 0 })) { $perms = foreach ($oauth2PermissionGrant in $sp.oauth2PermissionGrants) { $resID = ($servicePrincipals | Where-Object id -eq $appRoleAssignment.resourceId).appDisplayName if ($null -ne $oauth2PermissionGrant.PrincipalId) { $userId = "($($users[$oauth2PermissionGrant.principalId]))" } else { $userId = $null } "[$($resID)$($userId)]:$($oauth2PermissionGrant.Scope.TrimStart().Split(' ') -join ',')" } $validUntil = ($sp.oauth2PermissionGrants.ExpiryTime | Sort-Object -Descending | Select-Object -Unique -First 1) -replace 'Z$' $sp | Add-Member -NotePropertyName delegatePermissions -NotePropertyValue $perms -Force $sp | Add-Member -NotePropertyName delegateValidUntil -NotePropertyValue $validUntil -Force $assignedTo = [System.Collections.Generic.List[string]]::new() if (($sp.oauth2PermissionGrants.ConsentType | Select-Object -Unique) -eq "AllPrincipals") { $assignedto.Add("All users (admin consent)") } if ($null -ne [string[]]($perms | ForEach-Object { if ($_ -match "\((.*@.*)\)") { $Matches[1] } })) { $assignedto.AddRange([string[]]($perms | ForEach-Object { if ($_ -match "\((.*@.*)\)") { $Matches[1] } })) } $sp | Add-Member -NotePropertyName delegateAuthorizedBy -NotePropertyValue ($assignedto | Select-Object -Unique) -Force } $accountEnabledCounter = 1 $idToSp = @{} $signInRequest = foreach ($sp in ($servicePrincipals | Where-Object AccountEnabled)) { @{ url = "/auditLogs/signIns?`$filter=(CreatedDateTime ge $TimeFrameDate) and (appid eq '$($sp.appId)') and signInEventTypes/any(t: t eq 'interactiveUser' or t eq 'nonInteractiveUser' or t eq 'managedIdentity' or t eq 'servicePrincipal')" method = "GET" id = $accountEnabledCounter } $idToSp[$accountEnabledCounter] = $sp $accountEnabledCounter++ } $responses = Invoke-GraphRequestBatch -Request $signInRequest foreach ($response in $responses) { if ($null -eq $response.id) { continue } $signinsCurrent = $response.body.value $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'signInsTimePeriod' -NotePropertyValue $signinsCurrent.Count -Force $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'activeUsersTimePeriod' -NotePropertyValue ($signinsCurrent | Group-Object userPrincipalName).Name.Count -Force if (($signinsCurrent | Group-Object userPrincipalName).Name.Count -gt 0) { $detailed = $signinsCurrent | Group-Object userPrincipalName | Sort-Object Count -Descending | ForEach-Object { "$($_.Name) - $($_.Count)" } $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'signInsTimePeriodDetail' -NotePropertyValue $detailed -Force $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'AccountEnabledDesiredState' -NotePropertyValue $true -Force } else { $idToSp[[int]$response.id] | Add-Member -NotePropertyName 'AccountEnabledDesiredState' -NotePropertyValue $false -Force } } $servicePrincipals } <# .SYNOPSIS Gets the last signin time for all users in the Saga tenant. .DESCRIPTION Gets the last signin time for all users in the Saga tenant. .EXAMPLE Get-SagaUserLastSignIn Gets the last signin time for all users in the Saga tenant. #> function Get-SagaUserLastSignIn { [CmdletBinding()] param ( ) Connect-SagaGraph Invoke-GraphRequest -Query "users?`$filter=accountEnabled eq true&`$select=UserPrincipalName,id,signInActivity" } <# .SYNOPSIS Get the signin methods and license status for all enabled users in the tenant. .DESCRIPTION Get the signin methods and license status for all enabled users in the tenant. .EXAMPLE Get-SagaUserSigninAndLicenseStatus Get the signin methods and license status for all users in the tenant. #> function Get-SagaUserSigninAndLicenseStatus { [CmdletBinding()] param ( ) Connect-SagaGraph [System.Collections.Arraylist]$users = MiniGraph\Invoke-GraphRequest -Query "users?`$filter=accountEnabled eq true&`$select=UserPrincipalName,id" $batchCounter = 1 $idToUser = @{} $requests = foreach ($user in $users) { @{ url = "/users/$($user.id)/authentication/methods" method = "GET" id = $batchCounter } $idToUser[$batchCounter] = $user $batchCounter++ } $methods = Invoke-GraphRequestBatch -Request $requests foreach ($method in $methods) { [string[]] $formatedMethod = foreach ($authMethod in $method.body.value) { if ($authMethod.psobject.properties.name -contains 'createdDateTime' -and -not $authMethod.createdDateTime) { $authMethod.createdDateTime = [datetime]::MinValue } switch -Regex ($authMethod.'@odata.type') { 'phoneAuthentication' { '{0}_{1}_{2}_SMSSigninEnabled_{3}' -f 'PhoneAuthentication', $authMethod.phoneNumber, $authMethod.phoneType, $authMethod.smsSignInState } 'microsoftAuthenticatorAuthentication' { '{0}_{1}_{2}_{3}' -f 'AuthenticatorApp', $authMethod.displayName, $authMethod.deviceTag, $authMethod.phoneAppVersion } 'fido2Authentication' { '{0}_{1:yyyyMMdd}_{2}_{3}' -f 'FIDO2', $authMethod.createdDateTime, $authMethod.attestationLevel, $authMethod.model } 'windowsHelloForBusinessAuthentication' { '{0}_{1:yyyyMMdd}_{2}_strength_{3}' -f 'Hello', $authMethod.createdDateTime, $authMethod.displayName, $authMethod.keyStrength } 'passwordAuthentication' { '{0}_{1:yyyyMMdd-HHmmss}' -f 'Password', $_.createdDateTime } 'emailAuthentication' { $authMethod.emailAddress } } } $idToUser[[int]$method.id] | Add-Member -MemberType NoteProperty -Name AuthenticationMethods -Value ($formatedMethod -join '#') -Force } $batchCounter = 1 $idToUser = @{} $requests = foreach ($user in $users) { @{ url = "/users/$($user.id)/authentication/signInPreferences" method = "GET" id = $batchCounter } $idToUser[$batchCounter] = $user $batchCounter++ } $preferences = Invoke-GraphRequestBatch -Request $requests foreach ($preference in $preferences) { $idToUser[[int]$preference.id] | Add-Member -MemberType NoteProperty -Name SystemPreferredAuthenticationMethodEnabled -Value $preference.body.isSystemPreferredAuthenticationMethodEnabled $idToUser[[int]$preference.id] | Add-Member -MemberType NoteProperty -Name UserPreferredMethodForSecondaryAuthentication -Value $preference.body.userPreferredMethodForSecondaryAuthentication $idToUser[[int]$preference.id] | Add-Member -MemberType NoteProperty -Name SystemPreferredAuthenticationMethod -Value $preference.body.systemPreferredAuthenticationMethod } $batchCounter = 1 $idToUser = @{} $requests = foreach ($user in $users) { @{ url = "/users/$($user.id)/licenseDetails?`$select=skuPartNumber,servicePlans" method = "GET" id = $batchCounter } $idToUser[$batchCounter] = $user $batchCounter++ } $licenses = Invoke-GraphRequestBatch -Request $requests foreach ($license in $licenses) { $idToUser[[int]$license.id] | Add-Member -MemberType NoteProperty -Name License -Value ($license.body.value.skuPartNumber) $idToUser[[int]$license.id] | Add-Member -MemberType NoteProperty -Name ServicePlans -Value (($license.body.value.servicePlans | Where-Object { $_.provisioningStatus -eq 'Success' -and $_.AppliesTo -eq 'User' }).servicePlanName) } $idToUser.Values } <# .SYNOPSIS Import attributes to users in Entra ID .DESCRIPTION Import attributes to users in Entra ID. Bulk import can be done using CSV or JSON. All objects must have the property UserPrincipalName as either the actual UPN or the object GUID of each user. All other properties will be used as attributes which are updated. There is no schema validation against available Entra attributes! .EXAMPLE Import-SagaEntraAttribute -CsvPath 'C:\temp\users.csv' Import attributes to users in Entra ID from a CSV file. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. #> function Import-SagaEntraAttribute { [CmdletBinding(DefaultParameterSetName = 'Json', SupportsShouldProcess, ConfirmImpact = 'High')] param ( # Column UserPrincipalName, Attribute1Name, Attribute2Name, ... [Parameter(ParameterSetName = 'Csv')] [string] $CsvPath, <# Array of objects with UserPrincipalName, Attribute1Name, Attribute2Name, ... [ { "UserPrincipalName" : "john@contoso.com", "Attribute1Name" : "Attribute1Value", "Attribute2Name" : "Attribute2Value" }, { "UserPrincipalName" : "sally@contoso.com", "Attribute1Name" : "Attribute1Value", "Attribute2Name" : "Attribute2Value" } ] #> [Parameter(ParameterSetName = 'Json')] [string] $JsonPath ) Connect-SagaGraph $bulkData = if ($PSCmdlet.ParameterSetName -eq 'Json') { Get-Content -Path $JsonPath | ConvertFrom-Json } else { Import-Csv -Path $CsvPath } if (-not $PSCmdlet.ShouldProcess("$($bulkData.Count) users", "Update")) { return } $batchCounter = 1 $idToUser = @{} [hashtable[]] $requests = foreach ($item in $bulkData) { $body = @{} foreach ($property in $item.PsObject.Properties.Where({ $_.Name -ne 'UserPrincipalName' })) { $body[$property.Name] = $property.Value } @{ url = "/users/$($item.UserPrincipalName)" method = "PATCH" id = $batchCounter body = $body headers = @{ "Content-Type" = "application/json" } } $idToUser[$batchCounter] = $item $batchCounter++ } $userUpdate = Invoke-GraphRequestBatch -Request $requests foreach ($userUpdate in ($userUpdate | Where-Object { $_.status -in 200..299 })) { $user = $idToUser[[int]$userUpdate.Id] Write-PSFMessage -Message "Updated $($user.UserPrincipalName)" } foreach ($userUpdate in ($userUpdate | Where-Object { $_.status -notin 200..299 })) { $user = $idToUser[[int]$userUpdate.Id] Write-PSFMessage -Message "Failed to update $($user.UserPrincipalName) with $($userUpdate.status)" } } Register-PSFConfigValidation -Name "x509certificate" -ScriptBlock { Param ( $Value ) $Result = [PSCustomObject]@{ Success = $True Value = $null Message = "" } try { [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate = $Value } catch { $Result.Message = "Not an X509Certificate2: $Value" $Result.Success = $False return $Result } $Result.Value = $certificate return $Result } Register-PSFConfigValidation -Name "guid" -ScriptBlock { Param ( $Value ) $Result = [PSCustomObject]@{ Success = $True Value = $null Message = "" } try { [guid]$guid = $Value } catch { $Result.Message = "Not a GUID: $Value" $Result.Success = $False return $Result } $Result.Value = $guid return $Result } Set-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphConnectionMode -Value DeviceCode -Validation string -Description 'Azure, DeviceCode, Browser, Certificate, or ClientSecret' -Default Set-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphClientId -Value '' -Validation guid -Description 'Client ID for Graph API' -Default Set-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphTenantId -Value '' -Validation guid -Description 'Tenant ID for Graph API' -Default Set-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphClientSecret -Value '' -Validation secret -Description 'Client Secret for Graph API' -Default Set-PSFConfig -Module shiftavenue.GraphAutomation -Name GraphCertificate -Value '' -Validation x509certificate -Description 'Certificate for Graph API' -Default Set-PSFConfig -Module shiftavenue.GraphAutomation -Name Logpath -Value (Join-Path -Path $HOME -ChildPath '.saga/logs') -Validation string -Default -Description 'Path to CMtrace log files' $paramSetPSFLoggingProvider = @{ Name = 'logfile' InstanceName = '<saga>' FilePath = Join-Path -Path (Get-PSFConfigValue -FullName shiftavenue.GraphAutomation.LogPath -Fallback "$home") -ChildPath 'Saga-%Date%.log' FileType = 'CMTrace' Enabled = $true } Set-PSFLoggingProvider @paramSetPSFLoggingProvider |