AzureADIncidentResponse.psm1
############################################################################################################# ############################################################################################################# <# ############################################# AZURE AD INCIDENT RESPONSE POWERSHELL MODULE ############################################# Included functions: 1) CONNECTIVITY * Connect-AzureADIR * Get-AzureADIRApiToken * Get-AzureADIRTenantId * Get-AzureADIRHeader * Invoke-AzureADIRDoWhile * Invoke-AzureADIRWebRequest 2) DOMAINS * Get-AzureADIRDomainRegistrationDetail 3) APPLICATIONS * Get-AzureADIRPermission 4) ACTIVITY * Get-AzureADIRSignInDetail * Get-AzureADIRAuditActivity * Get-AzureADIRDismissedUserRisk * Get-AzureADIRSsprUsageHistory * Get-AzureADIRUserLastSignInActivity 5) PRIVILEGE * Get-AzureADIRPrivilegedRoleAssignment * Get-AzureADIRPrivilegedUserOnPremCorrelation * Get-AzureADIRPimPrivilegedRoleAssignment * Get-AzureADIRPimPrivilegedRoleAssignmentRequest 6) SECURITY CREDENTIALS * Get-AzureADIRMfaAuthMethodAnalysis * Get-AzureADIRMfaPhoneToLocationCheck 7) POLICIES * Get-AzureADIRConditionalAccessPolicy 8) MISC * Get-AzureADIRObjectIdToDisplayName * Get-AzureADIRDisplayNameToObjectId Least privilege: * Run the module as GLOBAL READER in Azure AD roles THIS CODE-SAMPLE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. This sample is not supported under any Microsoft standard support program or service. The script is provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the script be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample or documentation, even if Microsoft has been advised of the possibility of such damages, rising out of the use of or inability to use the sample script, even if Microsoft has been advised of the possibility of such damages. #> ############################################################################################################# #Author: Ian Farr (PoSh Chap) #(c) 2020 Microsoft. All rights reserved. $VerbosePreference = "Continue" ############################################################################################################# ################################# ################################# #region 1) CONNECTIVITY ############################### #FUNCTION: Connect-AzureADIR ############################### function Connect-AzureADIR { ############################################################################ <# .SYNOPSIS Autheticate to the Microsoft Graph API and Azure AD Graph API. Use the obtained tokens to authenticate to the Azure AD PowerShell and the MSOnline modules. .DESCRIPTION Performs the following in order: 1) Obtains tokens for MS Graph API / Azure AD Graph API 2) Connect to the Azure AD PowerShell module 3) Connect to MSOnline PowerShell module. .EXAMPLE Connect-AzureADIR -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f Connect to the tenant ID - b446a536-cb76-4360-a8bb-6593cf4d9c7f for: 1) MS Graph API / Azure AD Graph API 2) Azure AD PowerShell 3) MSOnline PowerShell .EXAMPLE Connect-AzureADIR -TenantId (Get-AzureADIRTenantId -DomainName test.info) Use the Get-AzureADIRTenantId cmdlet to obtain a tenant ID for test.info. Then connects to the supplied tenant ID. .EXAMPLE Connect-AzureADIR -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -UserUpn Bob@contoso.com Connect to the tenant ID - b446a536-cb76-4360-a8bb-6593cf4d9c7f as user Bob@contoso.com for: 1) MS Graph API / Azur AD Graph API 2) Azure AD PowerShell 3) MSOnline PowerShell #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #A login hint [Parameter(Position=1)] [string]$UserUpn ) ############################################################################ ######################## ##Microsoft Graph Token if ($UserUpn) { Write-Verbose -Message "$(Get-Date -f T) - Obtaining MS Graph access token..." $MsGraphResponse = Get-AzureADIRApiToken -TenantId $TenantId -LoginHint $UserUpn -InterActive if ($MsGraphResponse) { Write-Verbose -Message "$(Get-Date -f T) - Obtaining Azure AD Graph access token..." $AadGraphResponse = Get-AzureADIRApiToken -TenantId $TenantId -LoginHint $UserUpn -AadGraph } } else { Write-Verbose -Message "$(Get-Date -f T) - Obtaining MS Graph access token..." $MSGraphResponse = Get-AzureADIRApiToken -TenantId $TenantId -InterActive if ($MsGraphResponse) { Write-Verbose -Message "$(Get-Date -f T) - Obtaining Azure AD Graph access token..." $AadGraphResponse = Get-AzureADIRApiToken -TenantId $TenantId -AadGraph } } ############################ ##Azure AD PowerShell module if ($AadGraphResponse -and $MsGraphResponse) { #Get tenant details to test that Connect-AzureADIR has been called try {$TenantInfo = Get-AzureADTenantDetail -ErrorAction SilentlyContinue} catch {} if ($TenantInfo) { Write-Verbose -Message "$(Get-Date -f T) - A connection for Azure AD Powershell module is already established" $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" if ($TenantInfo.ObjectId -eq $TenantId) { Write-Verbose -Message "$(Get-Date -f T) - Retrieved tenant ($(($TenantInfo).ObjectId)) matches supplied target tenant ID ($TenantId)" $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" } else { Write-Verbose -Message "$(Get-Date -f T) - Retrieved tenant ($(($TenantInfo).ObjectId)) does not match supplied target tenant ID ($TenantId)" Write-Verbose -Message "$(Get-Date -f T) - Disconnecting from $(($TenantInfo).ObjectId) for Azure AD PowerShell module..." try {Disconnect-AzureAD -ErrorAction SilentlyContinue} catch {} #Check if we've disconnected if ($?) { Write-Verbose -Message "$(Get-Date -f T) - $(($TenantInfo).ObjectId) disconnected" Write-Verbose -Message "$(Get-Date -f T) - Connecting to $TenantId for Azure AD PowerShell module..." if ($UserUpn) { #Silently connect try {Connect-AzureAD -TenantId $TenantId -AccountID $UserUpn -AadAccessToken $AadGraphResponse.AccessToken ` -MsAccessToken $MsGraphResponse.AccessToken ` -ErrorAction SilentlyContinue | Out-Null} catch {} } else { #Silently connect try {Connect-AzureAD -TenantId $TenantId -AccountId $MsGraphResponse.Account.UserName -AadAccessToken $AadGraphResponse.AccessToken ` -MsAccessToken $MsGraphResponse.AccessToken ` -ErrorAction SilentlyContinue | Out-Null} catch {} } #Check if if Connect-AzureAD works if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId for Azure AD PowerShell module established" try {$TenantInfo = Get-AzureADTenantDetail -ErrorAction SilentlyContinue} catch {} $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" } else { Write-Warning -Message "$(Get-Date -f T) - Connection to $TenantId for Azure AD PowerShell module could not be established" $TenantInfo = $false } } else { Write-Warning -Message "$(Get-Date -f T) - Could not disconnect from $(($TenantInfo).ObjectId) for Azure AD PowerShell module" $TenantInfo = $false } } } else { Write-Verbose -Message "$(Get-Date -f T) - Connecting to $TenantId for Azure AD PowerShell module..." #Silently connect if ($UserUpn) { #Silently connect try {Connect-AzureAD -TenantId $TenantId -AccountID $UserUpn -AadAccessToken $AadGraphResponse.AccessToken ` -MsAccessToken $MsGraphResponse.AccessToken ` -ErrorAction SilentlyContinue | Out-Null} catch {} } else { #Silently connect try {Connect-AzureAD -TenantId $TenantId -AccountId $MsGraphResponse.Account.UserName -AadAccessToken $AadGraphResponse.AccessToken ` -MsAccessToken $MsGraphResponse.AccessToken ` -ErrorAction SilentlyContinue | Out-Null} catch {} } #Check if if Connect-AzureAD works if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId for Azure AD PowerShell module established" try {$TenantInfo = Get-AzureADTenantDetail -ErrorAction SilentlyContinue} catch {} $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" } else { Write-Warning -Message "$(Get-Date -f T) - Connection to $TenantId for Azure AD PowerShell module could not be established" $TenantInfo = $false } } ############################# ##MSOnline PowerShell module #Try and connect to the MS Online PowerShell module try {$DomainInfo = Get-MsolDomain -TenantId $TenantId -ErrorAction SilentlyContinue} catch {} if ($DomainInfo) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId for MSOnline PowerShell module established" } else { #Present connection pop-up Write-Verbose -Message "$(Get-Date -f T) - Calling Connect-MsolService cmdlet" Connect-MsolService -AdGraphAccesstoken $AadGraphResponse.AccessToken -MsGraphAccessToken $MsGraphResponse.AccessToken -ErrorAction SilentlyContinue #Populate the DomainInfo variable if Connect-MsolService works if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId for MSOnline PowerShell module established" $DomainInfo = $true } else { Write-Verbose "$(Get-Date -f T) - Connection to $TenantId for MSOnline PowerShell module could not be established" } } } } #end function ################################## #FUNCTION: Get-AzureADIRApiToken ################################## function Get-AzureADIRApiToken { ############################################################################ <# .SYNOPSIS Get an access token for use with the API cmdlets. .DESCRIPTION Uses MSAL.ps to obtain an access token. Has an option to refresh an existing token. .EXAMPLE Get-AzureADIRApiToken -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f Gets or refreshes an access token for making API calls for the tenant ID b446a536-cb76-4360-a8bb-6593cf4d9c7f. .EXAMPLE Get-AzureADIRApiToken -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -ForceRefresh Refreshes an access token for making API calls for the tenant ID b446a536-cb76-4360-a8bb-6593cf4d9c7f. .EXAMPLE Get-AzureADIRApiToken -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -LoginHint Bob@Contoso.com Gets or refreshes an access token for making API calls for the tenant ID b446a536-cb76-4360-a8bb-6593cf4d9c7f and user Bob@Contoso.com. .EXAMPLE Get-AzureADIRApiToken -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -InterActive Gets or refreshes an access token for making API calls for the tenant ID b446a536-cb76-4360-a8bb-6593cf4d9c7f. Ensures a pop-up box appears. #> ############################################################################ [CmdletBinding(DefaultParameterSetName="InterActive")] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Force a token refresh [Parameter(Position=1,ParameterSetName="ForceRefresh")] [switch]$ForceRefresh, #The user's upn used for the login hint [Parameter(Position=2,ParameterSetName="InterActive")] [string]$LoginHint, #Force a pop-up box [Parameter(Position=3,ParameterSetName="InterActive")] [switch]$InterActive, #get an Azure AD Graph token [Parameter(Position=4)] [switch]$AadGraph ) ############################################################################ #Get an access token using the PowerShell client ID $ClientId = "1b730954-1685-4b74-9bfd-dac224a7b894" $RedirectUri = "urn:ietf:wg:oauth:2.0:oob" $Authority = "https://login.microsoftonline.com/$TenantId" if ($AadGraph) { $Scopes = "https://graph.windows.net/.default" } else { $Scopes = "https://graph.microsoft.com/.default" } if ($ForceRefresh) { Write-Verbose -Message "$(Get-Date -f T) - Attempting to refresh an existing access token" #Attempt to refresh access token try { $Response = Get-MsalToken -ClientId $ClientId -RedirectUri $RedirectUri -Authority $Authority -Scopes $Scopes -ForceRefresh } catch {} #Error handling for token acquisition if ($Response) { Write-Verbose -Message "$(Get-Date -f T) - API Access Token refreshed - new expiry: $(($Response).ExpiresOn.UtcDateTime)" return $Response } else { Write-Warning -Message "$(Get-Date -f T) - Failed to refresh Access Token - try re-running the cmdlet again" } } elseif ($LoginHint) { Write-Verbose -Message "$(Get-Date -f T) - Checking token cache with -LoginHint for $LoginHint" #Run this to obtain an access token - should prompt on first run to select the account used for future operations try { if ($InterActive) { $Response = Get-MsalToken -ClientId $ClientId -RedirectUri $RedirectUri -Authority $Authority -LoginHint $LoginHint -Scopes $Scopes -Interactive } else { $Response = Get-MsalToken -ClientId $ClientId -RedirectUri $RedirectUri -Authority $Authority -LoginHint $LoginHint -Scopes $Scopes } } catch {} #Error handling for token acquisition if ($Response) { Write-Verbose -Message "$(Get-Date -f T) - API Access Token obtained for: $(($Response).Account.Username) ($(($Response).Account.HomeAccountId.ObjectId))" #Write-Verbose -Message "$(Get-Date -f T) - API Access Token scopes: $(($Response).Scopes)" return $Response } else { Write-Warning -Message "$(Get-Date -f T) - Failed to obtain an Access Token - try re-running the cmdlet again" Write-Warning -Message "$(Get-Date -f T) - If the problem persists, use `$Error[0] for more detail on the error or start a new PowerShell session" } } else { Write-Verbose -Message "$(Get-Date -f T) - Checking token cache with -Prompt" #Run this to obtain an access token - should prompt on first run to select the account used for future operations try { if ($InterActive) { $Response = Get-MsalToken -ClientId $ClientId -RedirectUri $RedirectUri -Authority $Authority -Prompt SelectAccount -Interactive -Scopes $Scopes } else { $Response = Get-MsalToken -ClientId $ClientId -RedirectUri $RedirectUri -Authority $Authority -Prompt SelectAccount -Scopes $Scopes } } catch {} #Error handling for token acquisition if ($Response) { Write-Verbose -Message "$(Get-Date -f T) - API Access Token obtained for: $(($Response).Account.Username) ($(($Response).Account.HomeAccountId.ObjectId))" #Write-Verbose -Message "$(Get-Date -f T) - API Access Token scopes: $(($Response).Scopes)" return $Response } else { Write-Warning -Message "$(Get-Date -f T) - Failed to obtain an Access Token - try re-running the cmdlet again" Write-Warning -Message "$(Get-Date -f T) - If the problem persists, run Connect-AzureADIR with the -UserUpn parameter" } } } #end function ################################### #FUNCTION: Get-AzureADIRTenantId ################################### function Get-AzureADIRTenantId { ############################################################################ <# .SYNOPSIS Retrieves the tenant ID for a supplied domain name. .DESCRIPTION Retrieves the tenant ID for a supplied domain name by querying the \well-known\openid-configuration end point. .EXAMPLE Get-AzureADIRTenantId -DomainName test.info Retrives the tenant ID for the domain name test.info. .NOTES Thanks to Ramiro Calderon! #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [string]$DomainName ) ############################################################################ try { Write-Verbose -Message "$(Get-Date -f T) - Obtaining tenant ID for $DomainName" $RawResult = Invoke-WebRequest "https://login.microsoftonline.com/$DomainName/v2.0/.well-known/openid-configuration" -ErrorAction SilentlyContinue -Verbose:$false $ObjectResult = $RawResult | ConvertFrom-Json $Endpoint = $ObjectResult.authorization_endpoint $EndpointUri = [Uri]$Endpoint Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID - $($EndpointUri.Segments[1].Trim('/'))" Write-Output $EndpointUri.Segments[1].Trim('/') } catch { Write-Warning -Message "$(Get-Date -f T) - Domain not found" } } #end function ################################# #FUNCTION: Get-AzureADIRHeader ################################# function Get-AzureADIRHeader { ############################################################################ <# .SYNOPSIS Uses a supplied Access Token to construct a header for a an API call. .DESCRIPTION Uses a supplied Access Token to construct a header for a an API call with Invoke-WebRequest. Can supply the ConsistencyLevel = Eventual parameter for performing Count activities. .EXAMPLE Get-AzureADIRHeader -Token $Token Constructs a header with an obtained token for using with Invoke-WebRequest. .EXAMPLE Get-AzureADIRHeader -Token $Token -ConsistencyLevelEventual Constructs a header with an obtained token for using with Invoke-WebRequest. Uses the optional -ConsistencyLevelEventual switch for use in conjunction with the count call. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [string]$Token, #Switch to include ConsistencyLevel = Eventual for $count operations [Parameter(Position=1)] [switch]$ConsistencyLevelEventual ) ############################################################################ if ($ConsistencyLevelEventual) { return @{ "Authorization" = ("Bearer {0}" -f $Token); "Content-Type" = "application/json"; "ConsistencyLevel" = "eventual"; } } else { return @{ "Authorization" = ("Bearer {0}" -f $Token); "Content-Type" = "application/json"; } } } #end function ################################## #FUNCTION: Get-AzureADIRDoWhile ################################## function Invoke-AzureADIRDoWhile { ############################################################################ <# .SYNOPSIS Performs the API pagination loop. .DESCRIPTION Calls the Invoke-AzureADIRWebRequest to obtain target information with a supplied query URL and authentication header. Handles pagination with @odata.nextLink. Adds the returned content for each call to an array that is ultimately returned by the function. .EXAMPLE Invoke-AzureADIRDoWhile -Header $Header -Url $Url Calls the Invoke-AzureADIRWebRequest to obtain target information with a supplied query URL and authentication header. #> ############################################################################ [CmdletBinding()] param( #The header for the API call [Parameter(Mandatory,Position=0)] $Header, #the query Url [Parameter(Mandatory,Position=1)] [string]$Url ) ############################################################################ ###################################### ##Do while the fetch URL is populated do { Write-Verbose -Message "$(Get-Date -f T) - Invoking web request for $Url" $MyReport = Invoke-AzureADIRWebRequest -Header $Header -Url $Url ############################### #Convert the content from JSON $ConvertedReport = ($MyReport.Content | ConvertFrom-Json).value #Add to concatenated findings [array]$TotalReport += $ConvertedReport #Update the fetch url to include the paging element $Url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink' #Update the access token on the second iteration if ($OneSuccessfulFetch) { $Token = (Get-AzureADIRApiToken -TenantId $TenantId -ForceRefresh).AccessToken $Header = Get-AzureADIRHeader -Token $Token } #Update count and show for this cycle $Count = $Count + $ConvertedReport.Count Write-Verbose -Message "$(Get-Date -f T) - Total records fetched: $count" #Update tracking variables $OneSuccessfulFetch = $true } while ($Url) #end do / while return $TotalReport } #end function ####################################### #FUNCTION: Invoke-AzureADIRWebRequest ####################################### function Invoke-AzureADIRWebRequest { ############################################################################ <# .SYNOPSIS Perform Invoke-WebRequest with additional error handling. .DESCRIPTION Perform Invoke-WebRequest with additional error handling for supplied query URL and authentication header. Has retry logic. .EXAMPLE Invoke-AzureADIRWebRequest -Header $Header -Url $Url Calls Invoke-Webrequest with the supplied authentication header and query URL with error checking and retry logic. #> ############################################################################ [CmdletBinding()] param( #The header for the API call [Parameter(Mandatory,Position=0)] $Header, #the query Url [Parameter(Mandatory,Position=1)] [string]$Url ) ############################################################################ $RetryCount = 0 ################################## #Do our stuff with error handling try { #Invoke the web request $MyReport = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $Url -Verbose:$false) } catch [System.Net.WebException] { $StatusCode = [int]$_.Exception.Response.StatusCode Write-Warning -Message "$(Get-Date -f T) - $($_.Exception.Message)" #Check what's gone wrong if (($StatusCode -eq 401) -and ($OneSuccessfulFetch)) { #Token might have expired; renew token and try again $Token = (Get-AzureADIRApiToken -TenantId $TenantId -InterActive).AccessToken $Header = Get-AzureADIRHeader -Token $Token $OneSuccessfulFetch = $False } elseif (($StatusCode -eq 429) -or ($StatusCode -eq 504) -or ($StatusCode -eq 503)) { #Throttled request or a temporary issue, wait for a few seconds and retry Start-Sleep -Seconds 5 } elseif (($StatusCode -eq 403) -or ($StatusCode -eq 401)) { Write-Warning -Message "$(Get-Date -f T) - Please check the permissions of the user" break } elseif ($StatusCode -eq 400) { Write-Warning -Message "$(Get-Date -f T) - Please check the query used" break } else { #Retry up to 5 times if ($RetryCount -lt 5) { write-output "Retrying..." $RetryCount++ } else { #Write to host and exit loop Write-Warning -Message "$(Get-Date -f T) - Download request failed. Please try again in the future" break } } } catch { #Write error details to host Write-Warning -Message "$(Get-Date -f T) - $($_.Exception)" #Retry up to 5 times if ($RetryCount -lt 5) { write-output "Retrying..." $RetryCount++ } else { #Write to host and exit loop Write-Warning -Message "$(Get-Date -f T) - Download request failed - please try again in the future" break } } # end try / catch return $MyReport } #end function #endregion ################################# ################################# #region 2) DOMAINS ################################################### #FUNCTION: Get-AzureADIRDomainRegistrationDetail ################################################### function Get-AzureADIRDomainRegistrationDetail { ############################################################################ <# .SYNOPSIS Generates a list of domains from Azure AD and then checks whois information to display Name Servers, Admin and Registrant. .DESCRIPTION Generates a list of domains and uses whois to get additional information. Flags if the domain is verified in Azure AD and if its whois information is available. Will attempt to retrieve name server, admin and registrant information from the whois output. Writes all of the raw whois information to a txt file per domain and then zips the results. Can create date and time stamped CSV output. .EXAMPLE Get-AzureADIRDomainRegistrationDetail -TenantId 98cfcac2-9255-41a9-b206-a8cfad3998cc -CsvOutput Creates a CSV file containing domains listed in Azure AD, containing name server, admin and registrant information where available from whois. Also creates a zip file containing the raw who is output. .EXAMPLE Get-AzureADIRDomainRegistrationDetail -TenantId 98cfcac2-9255-41a9-b206-a8cfad3998cc Displays a list of domains from Azure AD, containing name server, admin and registrant information where available from whois. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Use this switch to create a date and time stamped CSV file [Parameter(Position=1)] [switch]$CsvOutput ) ############################################################################ #Get tenant details to test that Connect-AzureADIR has been called try { $TenantInfo = Get-AzureADTenantDetail } catch { Write-Warning -Message "$(Get-Date -f T) - You must call Connect-AzureADIR to run this function" Write-Verbose "$(Get-Date -f T) - Calling Connect AzureADIR" Connect-AzureADIR -TenantId $TenantId } if ($TenantInfo) { $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" #Define module base and whois location $ModuleBase = (Get-Module -Name AzureADIncidentResponse).ModuleBase $WhoIsBase = "$ModuleBase\Components\Whois" $WhoIsExe = "$WhoIsBase\WhoIs.exe" if (!(Test-Path $WhoIsExe)) { Write-Warning -Message "$(Get-Date -f T) - Unable to locate Whois.exe. Attempting to download..." $WhoIsDlUrl = "https://download.sysinternals.com/files/WhoIs.zip" $Tempfile = [System.IO.Path]::GetTempFileName() $TempFolder = [System.IO.Path]::GetDirectoryName($TempFile) $WebClient = New-Object System.Net.WebClient while ($WebClient.DownloadFile($WhoIsDlUrl,$TempFile)) { Start-Sleep -Seconds 1 } if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Successfully downloaded WhoIs.zip to $TempFolder\$Tempfile" Rename-Item -Path $TempFile -NewName "WhoIs.zip" -Force -ErrorAction SilentlyContinue if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Renamed $TempFile to WhoIs.zip" $WhoIsPath = "$($TempFolder)\WhoIs.zip" Copy-Item $WhoIsPath -Destination $WhoIsBase -Force if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Copied $($TempFolder)\WhoIs.zip to $WhoIsBase" Expand-Archive -Path "$WhoIsBase\WhoIs.zip" -DestinationPath "$WhoIsBase\" -Force -ErrorAction SilentlyContinue if ($?) { Write-Verbose -Message "$(Get-Date -f T) - $WhoIsBase\WhoIs.zip archive expanded" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to expand archive $WhoIsBase\WhoIs.zip" } } else { Write-Warning -Message "$(Get-Date -f T) - Failed to copy zip file from $TempFolder to $WhoIsBase" } } else { Write-Warning -Message "$(Get-Date -f T) - Failed to rename temp file - $Tempfile" } } else { Write-Warning -Message "$(Get-Date -f T) - Failed to download WhoIs.zip to $TempFolder\$Tempfile" } } if (Test-Path $WhoIsExe) { Write-Verbose -Message "$(Get-Date -f T) - WhoIs.exe located in $WhoIsExe" #Output files $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $DomainRegistrations = "DomainRegistrations_$now.csv" $DomainZip = "DomainRegistrations_$now.zip" #Get a list of domains Write-Verbose -Message "$(Get-Date -f T) - Attempting to get domains" try {$Domains = Get-AzureADDomain -ErrorAction SilentlyContinue | Where-Object {$_.Name -notlike "*.onmicrosoft.com"}} catch {} if ($Domains) { Write-Verbose -Message "$(Get-Date -f T) - $(($Domains).Count) domains found" #Loop through the domains and check whois information foreach ($Domain in $Domains) { #Blank variables $NameServerDetails = $null #Get whois information Write-Verbose -Message "$(Get-Date -f T) - Attempting to get whois information for $(($Domain).Name)" $WhoIsCmd = cmd /c $WhoIsExe /v $Domain.Name /accepteula /nobanner $WhoIs = $WhoIsCmd #Check we have whois info and parse if (($LASTEXITCODE -eq 0) -and ($WhoIs)) { $DomainRaw = "DomainRaw_$(($Domain).Name)_$now.txt" Write-Verbose -Message "$(Get-Date -f T) - Saving whois raw information for $(($Domain).Name) to $DomainRaw" $WhoIs > $DomainRaw Write-Verbose -Message "$(Get-Date -f T) - Parsing whois information for $(($Domain).Name)" #Is there a message saying we can't match a registrant? $NotMatchRegistrant = $WhoIs | Select-String -Pattern "as not able to match the registrant's name" if ($NotMatchRegistrant) { $WhoIsRegistrant = "UNKNOWN" $NameSeverLine = ($WhoIs | Select-String -Pattern "Name Servers:").LineNumber $NameServerDetails = "$(($WhoIs[$NameSeverLine + 1]).Trim())`n$(($WhoIs[$NameSeverLine + 3]).Trim())`n$(($WhoIs[$NameSeverLine + 5]).Trim())`n$(($WhoIs[$NameSeverLine + 7]).Trim())" } else { #Grab some details $WhoIsRegistrant = "Known" [string]$AdminDetails = ($WhoIs | Select-String -Pattern "^Admin ") -join "`n" [string]$RegistrantDetails = ($WhoIs | Select-String -Pattern "^Registrant ") -join "`n" $NameServerDetails = $WhoIs | Select-String -Pattern "Name Server:" $NameServerDetails = ($NameServerDetails | ForEach-Object {($_ -split ":")[1]} | Sort-Object -Unique).Trim() -join "`n" } #Create PS Custom Object with domain detais $WhoIsDetails = [pscustomobject]@{ DomainName = $Domain.Name AzureADVerified = $Domain.IsVerified WhoIsRegistrant = $WhoIsRegistrant NameServers = $NameServerDetails AdminDetails = $AdminDetails Registrantdetails = $RegistrantDetails RawFile = $DomainRaw } } else { Write-Warning -Message "$(Get-Date -f T) - Unable to get whois information for $(($Domain).Name)" } #Zip up files Compress-Archive -Path .\*.txt -DestinationPath $DomainZip -Update -ErrorAction SilentlyContinue if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Raw whois domain files zipped to $(Get-Location)\$DomainZip" Remove-Item -Path .\*.txt -Force -ErrorAction SilentlyContinue if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Removed raw domain txt files from $(Get-Location)" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to remove raw domain txt files from $(Get-Location)" } } else { Write-Warning -Message "$(Get-Date -f T) - Failed to zip raw whois domain files - txt files still available" } #Add PS Custom Object to array [array]$TotalObjects += $WhoIsDetails } } else { Write-Warning -Message "$(Get-Date -f T) - No domains found" } #See if we need to write to CSV if ($CsvOutput) { Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for domain ownership" $TotalObjects | Export-Csv -Path $DomainRegistrations -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Domain ownership CSV written to $(Get-Location)\$DomainRegistrations" } else { $TotalObjects } } else { Write-Warning -Message "$(Get-Date -f T) - Unable to locate Whois.exe." } } } #end function #endregion ################################# ################################# #region 3) APPLICATIONS ###################################### #FUNCTION: Get-AzureADIRPermission ###################################### function Get-AzureADIRPermission { ############################################################################ <# .SYNOPSIS Produces CSV reports of app permissions. Can also list all permissions. .DESCRIPTION Produces two date and time stamped CSV reports with the -CsvOutput switch: * one for delegated permissions (OAuth2PermissionGrants) * one for application permissions (AppRoleAssignments) Can also list all permissions to the host without the -CsvOutput switch. .EXAMPLE Get-AzureADIRPermission -TenantId 98cfcac2-9255-41a9-b206-a8cfad3998cc -CsvOutput Creates two date and time stamped CSV files of tenant permissions, one for delegated permissions, one for application permissions and svaes them to the execution directory. .EXAMPLE Get-AzureADIRPermission -TenantId 98cfcac2-9255-41a9-b206-a8cfad3998cc Displays a list of tenant permissions to the host. .NOTES Thanks to Philippe Signoret for Get-AzureADPSPermission. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Use this switch to create a date and time stamped CSV file [Parameter(Position=1)] [switch]$CsvOutput ) ############################################################################ function Get-AzureADPSPermission { <# .SYNOPSIS Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments). .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-AzureADPSPermission.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation Generates a CSV report of all permissions granted to all apps. .EXAMPLE PS C:\> .\Get-AzureADPSPermission.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" } Get all apps which have application permissions for Directory.Read.All. .EXAMPLE PS C:\> .\Get-AzureADPSPermission.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId") Gets all permissions granted to all apps and includes additional properties for users and service principals. .NOTES Taken from https://gist.github.com/psignoret/41793f8c6211d2df5051d77ca3728c09 #> [CmdletBinding()] param( [switch] $DelegatedPermissions, [switch] $ApplicationPermissions, [string[]] $UserProperties = @("DisplayName"), [string[]] $ServicePrincipalProperties = @("DisplayName"), [switch] $ShowProgress, [int] $PrecacheSize = 999 ) # Get tenant details to test that Connect-AzureADIR has been called try { $tenant_details = Get-AzureADTenantDetail } catch { Write-Warning -Message "$(Get-Date -f T) - You must call Connect-AzureADIR to run this function" Write-Verbose "$(Get-Date -f T) - Calling Connect AzureADIR" Connect-AzureADIR -TenantId $TenantId } Write-Verbose -Message ("$(Get-Date -f T) - TenantId - {0}, InitialDomain - {1}" -f ` $tenant_details.ObjectId, ` ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name) # An in-memory cache of objects by {object ID} andy by {object class, object ID} $script:ObjectByObjectId = @{} $script:ObjectByObjectClassId = @{} # Function to add an object to the cache function CacheObject ($Object) { if ($Object) { if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) { $script:ObjectByObjectClassId[$Object.ObjectType] = @{} } $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object $script:ObjectByObjectId[$Object.ObjectId] = $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 -Message ("$(Get-Date -f T) - Querying Azure AD for object '{0}'" -f $ObjectId) try { $object = Get-AzureADObjectByObjectId -ObjectIds $ObjectId CacheObject -Object $object } catch { Write-Verbose -Message "$(Get-Date -f T) - Object not found." } } return $script:ObjectByObjectId[$ObjectId] } # Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode) # or by iterating over all ServicePrincipal objects. The latter is required if there are more than # 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD. function GetOAuth2PermissionGrants ([switch]$FastMode) { if ($FastMode) { Get-AzureADOAuth2PermissionGrant -All $true } else { $i = 0 $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { if ($ShowProgress) { Write-Progress -Activity "Retrieving delegated permissions..." ` -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` -PercentComplete (($i / $servicePrincipalCount) * 100) } $client = $_.Value Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId } } } $empty = @{} # Used later to avoid null checks # Get all ServicePrincipal objects and add to the cache Write-Verbose -Message "$(Get-Date -f T) - Retrieving all ServicePrincipal objects..." Get-AzureADServicePrincipal -All $true | ForEach-Object { CacheObject -Object $_ } $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) { # Get one page of User objects and add to the cache Write-Verbose -Message ("$(Get-Date -f T) - Retrieving up to {0} User objects..." -f $PrecacheSize) Get-AzureADUser -Top $PrecacheSize | Where-Object { CacheObject -Object $_ } Write-Verbose -Message "$(Get-Date -f T) - Testing for OAuth2PermissionGrants bug before querying..." $fastQueryMode = $false try { # There's a bug in Azure AD Graph which does not allow for directly listing # oauth2PermissionGrants if there are more than 999 of them. The following line will # trigger this bug (if it still exists) and throw an exception. $null = Get-AzureADOAuth2PermissionGrant -Top 999 $fastQueryMode = $true } catch { if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) { Write-Verbose -Message ("$(Get-Date -f T) - Fast query for delegated permissions failed, using slow method...") } else { throw $_ } } # Get all existing OAuth2 permission grants, get the client, resource and scope details Write-Verbose -Message "$(Get-Date -f T) - Retrieving OAuth2PermissionGrants..." GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object { $grant = $_ if ($grant.Scope) { $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object { $scope = $_ $grantDetails = [ordered]@{ "PermissionType" = "Delegated" "ClientObjectId" = $grant.ClientId "ResourceObjectId" = $grant.ResourceId "Permission" = $scope "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 (-not ($DelegatedPermissions -or $ApplicationPermissions))) { # Iterate over all ServicePrincipal objects and get app permissions Write-Verbose -Message "$(Get-Date -f T) - Retrieving AppRoleAssignments..." $i = 0 $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { if ($ShowProgress) { Write-Progress -Activity "Retrieving application permissions..." ` -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) ` -PercentComplete (($i / $servicePrincipalCount) * 100) } $sp = $_.Value Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true ` | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object { $assignment = $_ $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id } $grantDetails = [ordered]@{ "PermissionType" = "Application" "ClientObjectId" = $assignment.PrincipalId "ResourceObjectId" = $assignment.ResourceId "Permission" = $appRole.Value } # 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 } } } } ############################################################################ #Check if we need to produce CSV files if ($CsvOutput) { #Output files $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $DelegatedPermissions = "DelegatedPermissions_$now.csv" $ApplicationPermissions = "ApplicationPermissions_$now.csv" #Call Philippe's script and output to CSV Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for delegated permissions" Get-AzureADPSPermission -DelegatedPermissions -ServicePrincipalProperties @("DisplayName","AppId","AppOwnerTenantId") ` -UserProperties @("DisplayName","UserPrincipalName") -ShowProgress | Export-Csv -Path $DelegatedPermissions -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - CSV written to $(Get-Location)\$DelegatedPermissions" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for application permissions" Get-AzureADPSPermission -ApplicationPermissions -ServicePrincipalProperties @("DisplayName","AppId","AppOwnerTenantId") ` -UserProperties @("DisplayName","UserPrincipalName") -ShowProgress | Export-Csv -Path $ApplicationPermissions -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - CSV written to $(Get-Location)\$ApplicationPermissions" } else { Get-AzureADPSPermission -ServicePrincipalProperties @("DisplayName","AppId","AppOwnerTenantId") ` -UserProperties @("DisplayName","UserPrincipalName") -ShowProgress } } #end function #endregion ################################# ################################# #region 4) ACTIVITY ####################################### #FUNCTION: Get-AzureADIRSignInDetail ####################################### function Get-AzureADIRSignInDetail { ############################################################################ <# .SYNOPSIS Gets Sign-In log details for target users, clients or resources. .DESCRIPTION Produces filtered output for the sign-in log. Can target specific users, client applications, resources or IP addresses. Also has an option to specify a date range. Can send the filtered logs to Out-GridView for detailed examination. For more information run: Get-Help Out-GridView -Full | More .EXAMPLE Get-AzureADIRSignInDetail -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -UserId 8a734f47-0641-4b6d-ac10-3f47b55ab270 Gets all sign-in log events for the target user (by Object ID) for the specified tenant. Outputs retrieved events to screen. .EXAMPLE Get-AzureADIRSignInDetail -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -UserId 8a734f47-0641-4b6d-ac10-3f47b55ab270,729d870c-337b-432e-8e3a-2b4a4c87506e -OutGridView Gets all sign-in log events for the target users (by Object ID) for the specified tenant. Outputs the events to Out-GridView for detailed examination. .EXAMPLE Get-AzureADIRSignInDetail -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -ClientId 1b730954-1685-4b74-9bfd-dac224a7b894,c44b4083-3bb0-49c1-b47d-974e53cbdf3c -OutGridView Gets all sign-in log events for the target client apps (by Object ID) for the specified tenant. The target apps are: * 1b730954-1685-4b74-9bfd-dac224a7b894 (Azure Active Directory PowerShell) * c44b4083-3bb0-49c1-b47d-974e53cbdf3c (Azure Portal) Outputs the events to Out-GridView for detailed examination. .EXAMPLE Get-AzureADIRSignInDetail -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -ResourceId 00000003-0000-0000-c000-000000000000,797f4846-ba00-4fd7-ba43-dac1f8f63013 | Export-Csv .\Sign-Ins-By_ResourceId.csv -NoTypeInformation Gets all sign-in log events for the target client apps (by Object ID) for the specified tenant. The target apps are: * 00000003-0000-0000-c000-000000000000 (Microsoft Graph) * 797f4846-ba00-4fd7-ba43-dac1f8f63013 (Windows Azure Service Management API) Exports the events to a CSV file called Sign-Ins-By_ResourceId.csv without the type information header. .EXAMPLE Get-AzureADIRSignInDetail -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -IpAddress 212.100.128.76 Gets all sign-in log events for the target IP Address for the specified tenant. Outputs retrieved events to screen. .EXAMPLE Get-AzureADIRSignInDetail -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -CorrelationId 6b8fb7d2-3461-43d6-9a7a-296e105b713c Gets all sign-in log events for the target correlation ID for the specified tenant. #> ############################################################################ [CmdletBinding(DefaultParameterSetName="User")] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #The target user ID on which to filter [Parameter(Mandatory,Position=1,ParameterSetName="User")] [array]$UserId, #The target client ID on which to filter [Parameter(Mandatory,Position=2,ParameterSetName="Client")] [array]$ClientId, #The target client ID on which to filter [Parameter(Mandatory,Position=3,ParameterSetName="Resource")] [array]$ResourceId, #The target IP address on which to filter [Parameter(Mandatory,Position=4,ParameterSetName="IpAddress")] [ValidateScript({$_ -match [IPAddress]$_ })] [String]$IpAddress, #The target IP address on which to filter [Parameter(Mandatory,Position=4,ParameterSetName="Correlation")] [array]$CorrelationId, #The number of days ago after which events are retrieved, i.e. get events older than this point in time [Parameter(Position=5)] [ValidateRange(1,29)] [int32]$RangeFromDaysAgo, #The number of days ago before which events are retrieved, i.e. get events previous to this point in time [Parameter(Position=6)] [ValidateRange(2,30)] [int32]$RangeToDaysAgo, #Use this switch to output to the Grid View [Parameter(Position=7)] [switch]$OutGridView ) ############################################################################ #Deal with different search criterea if ($UserId) { $Target = "UserId" $Objects = $UserId Write-Verbose -Message "$(Get-Date -f T) - User mode selected" } elseif ($ClientId) { $Target = "AppId" $Objects = $ClientId Write-Verbose -Message "$(Get-Date -f T) - Client mode selected" } elseif ($ResourceId) { $Target = "ResourceId" $Objects = $ResourceId Write-Verbose -Message "$(Get-Date -f T) - Resource mode selected" } elseif ($IpAddress) { $Target = "ipAddress" $Objects = $IpAddress Write-Verbose -Message "$(Get-Date -f T) - IpAddress mode selected" } elseif ($CorrelationId) { $Target = "correlationId" $Objects = $CorrelationId Write-Verbose -Message "$(Get-Date -f T) - CorrelationId mode selected" } #Deal with different date criterea if ($RangeFromDaysAgo -and $RangeToDaysAgo){ Write-Verbose -Message "$(Get-Date -f T) - RangeFrom and RangeTo selected" if ($RangeFromDaysAgo -lt $RangeToDaysAgo) { Write-Verbose -Message "$(Get-Date -f T) - RangeFrom is less than RangeTo" } else { Write-Warning -Message "$(Get-Date -f T) - RangeFrom is greater than or equal to RangeTo" Write-Warning -Message "$(Get-Date -f T) - Setting RangeTo to $($RangeFromDaysAgo + 1)" $RangeToDaysAgo = $RangeFromDaysAgo +1 } #Create the datetime values $RangeFrom = (Get-Date (Get-Date).AddDays(-$RangeFromDaysAgo) -Format s) + "Z" $RangeTo = (Get-Date (Get-Date).AddDays(-$RangeToDaysAgo) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Getting events from $RangeFrom to $RangeTo" $DateFilter = "and createdDateTime le $RangeFrom and createdDateTime ge $RangeTo" } elseif ($RangeFromDaysAgo) { $RangeFrom = (Get-Date (Get-Date).AddDays(-$RangeFromDaysAgo) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Getting events from $RangeFrom" $DateFilter = "and createdDateTime le $RangeFrom" } elseif ($RangeToDaysAgo) { $RangeTo = (Get-Date (Get-Date).AddDays(-$RangeToDaysAgo) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Getting events to $RangeTo" $DateFilter = "and createdDateTime ge $RangeTo" } else { $DateFilter = "" } #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $TotalReport = $null #Loop through the supplied users foreach ($Object in $Objects) { ########################################### #Filter $Filter = "?`$filter=$Target eq '$Object'" #API endpoint $Url = "https://graph.microsoft.com/beta/auditLogs/signIns$Filter$DateFilter" ########################################### #Call the API query loop $TotalReport = Invoke-AzureADIRDoWhile -Header $Header -Url $Url } #end foreach #See if we need to write to CSV if ($OutGridView) { Write-Verbose -Message "$(Get-Date -f T) - Sending to Out-GridView" $TotalReport | Out-GridView -Title "Azure AD Incident Response Sign-In Detail - $Target" } else { #Return stuff $TotalReport } } #end if ($Token) } #end function ######################################## #FUNCTION: Get-AzureADIRAuditActivity ######################################## function Get-AzureADIRAuditActivity { ############################################################################ <# .SYNOPSIS Gets Audit log details for target Users, Service Principals, Event Categories or services logging the events. .DESCRIPTION Produces filtered output for the Audit log. Can target specific users, Service Principals, Audit Categories, Logging Services or Activity Display Names. Can specifiy a date range for a more targeted retrieval. Use this to reference Audit Categories and Activity Display Names: https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/reference-audit-activities Can send the filtered logs to Out-GridView for detailed examination. For more information run: Get-Help Out-GridView -Full | More .EXAMPLE Get-AzureADIRAuditActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -InitiatedByUser "3bdd577c-716f-4d6d-ba83-6daf8c439cdb","eed48f42-72c6-4c0f-b405-701f0558e07d" Retrieves audit events for actions initiated by the two user object IDs: * 3bdd577c-716f-4d6d-ba83-6daf8c439cdb * eed48f42-72c6-4c0f-b405-701f0558e07d .EXAMPLE Get-AzureADIRAuditActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -InitiatedByServicePrincipal "Microsoft.Azure.SyncFabric" -OutGridView Retrives audit events for actions intiated by the "Microsoft.Azure.SyncFabric" (case sensitive) Service Princiapl. Here we have to use the Display Name as supplying the Object ID is disallowed. Sends to Out-Gridview for examination. .EXAMPLE Get-AzureADIRAuditActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -Category "UserManagement","DirectoryManagement","ApplicationManagment" -OutGridView Retrives audit events for the following categories (case sensitive): * UserManagement * DirectoryManagement * ApplicationManagment Sends to Out-Gridview for examination. .EXAMPLE Get-AzureADIRAuditActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -LoggedByService "Core Directory" | Export-Csv .\Audits_By_ResourceId.csv -NoTypeInformation Retrieves events logged by the "Core Directory" (case sensitive) service. Exports the events to a CSV file called Audits_By_ResourceId.csv without the type information header. .EXAMPLE Get-AzureADIRAuditActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -ActivityDisplayName "Consent to application" Retrieves events that relate to the "Consent to application" (case sensitive) activity. .EXAMPLE Get-AzureADIRAuditActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -CorrelationId 7f0ecc65-27c6-4486-984d-073b843b5161,62992a74-5474-4910-b935-2f3156c351ea Retrieves events that relate to the correlation IDs 7f0ecc65-27c6-4486-984d-073b843b5161 and 62992a74-5474-4910-b935-2f3156c351ea #> ############################################################################ [CmdletBinding(DefaultParameterSetName="User")] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #The user or users initiating the action by Object ID [Parameter(Mandatory,Position=1,ParameterSetName="User")] [array]$InitiatedByUser, #The service principal or principals initiating the action by Display Name on which to filter [Parameter(Mandatory,Position=2,ParameterSetName="ServicePrincipal")] [array]$InitiatedByServicePrincipal, #The audit event category or categories on which to filter [Parameter(Mandatory,Position=3,ParameterSetName="Category")] [array]$Category, #The service or services logging the event on which to filter [Parameter(Mandatory,Position=4,ParameterSetName="Service")] [array]$LoggedByService, #The activity display name on which to filter [Parameter(Mandatory,Position=5,ParameterSetName="Activity")] [array]$ActivityDisplayName, #The correlation ID on which to filter [Parameter(Mandatory,Position=6,ParameterSetName="Correlation")] [array]$CorrelationId, #The number of days ago after which events are retrieved, i.e. get events older than this point in time [Parameter(Position=7)] [ValidateRange(1,29)] [int32]$RangeFromDaysAgo, #The number of days ago before which events are retrieved, i.e. get events previous to this point in time [Parameter(Position=8)] [ValidateRange(2,30)] [int32]$RangeToDaysAgo, #Use this switch to output to the Grid View [Parameter(Position=9)] [switch]$OutGridView ) ############################################################################ #Deal with different search criterea if ($InitiatedByUser) { $Target = "initiatedBy/user/id" $Objects = $InitiatedByUser Write-Verbose -Message "$(Get-Date -f T) - InitiatedByUser mode selected" } elseif ($InitiatedByServicePrincipal) { $Target = "initiatedBy/app/displayName" $Objects = $InitiatedByServicePrincipal Write-Verbose -Message "$(Get-Date -f T) - InitiatedByServicePrincipal mode selected" } elseif ($Category) { $Target = "category" $Objects = $Category Write-Verbose -Message "$(Get-Date -f T) - Category mode selected" } elseif ($LoggedByService) { $Target = "LoggedByService" $Objects = $LoggedByService Write-Verbose -Message "$(Get-Date -f T) - LoggedByService mode selected" } elseif ($ActivityDisplayName) { $Target = "ActivityDisplayName" $Objects = $ActivityDisplayName Write-Verbose -Message "$(Get-Date -f T) - ActivityDisplayName mode selected" } elseif ($CorrelationId) { $Target = "correlationId" $Objects = $CorrelationId Write-Verbose -Message "$(Get-Date -f T) - Correlation ID mode selected" } #Deal with different date criterea if ($RangeFromDaysAgo -and $RangeToDaysAgo){ Write-Verbose -Message "$(Get-Date -f T) - RangeFrom and RangeTo selected" if ($RangeFromDaysAgo -lt $RangeToDaysAgo) { Write-Verbose -Message "$(Get-Date -f T) - RangeFrom is less than RangeTo" } else { Write-Warning -Message "$(Get-Date -f T) - RangeFrom is greater than or equal to RangeTo" Write-Warning -Message "$(Get-Date -f T) - Setting RangeTo to $($RangeFromDaysAgo + 1)" $RangeToDaysAgo = $RangeFromDaysAgo +1 } #Create the datetime values $RangeFrom = (Get-Date (Get-Date).AddDays(-$RangeFromDaysAgo) -Format s) + "Z" $RangeTo = (Get-Date (Get-Date).AddDays(-$RangeToDaysAgo) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Getting events from $RangeFrom to $RangeTo" $DateFilter = "and activityDateTime le $RangeFrom and activityDateTime ge $RangeTo" } elseif ($RangeFromDaysAgo) { $RangeFrom = (Get-Date (Get-Date).AddDays(-$RangeFromDaysAgo) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Getting events from $RangeFrom" $DateFilter = "and activityDateTime le $RangeFrom" } elseif ($RangeToDaysAgo) { $RangeTo = (Get-Date (Get-Date).AddDays(-$RangeToDaysAgo) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Getting events to $RangeTo" $DateFilter = "and activityDateTime ge $RangeTo" } else { $DateFilter = "" } #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $TotalReport = $null #Loop throughthe supplied users foreach ($Object in $Objects) { ########################################### #Filter $Filter = "?`$filter=$Target eq '$Object'" #API endpoint $Url = "https://graph.microsoft.com/beta/auditLogs/directoryAudits$Filter$DateFilter" ########################################### #Call the API query loop $TotalReport = Invoke-AzureADIRDoWhile -Header $Header -Url $Url } #end foreach #See if we need to write to CSV if ($OutGridView) { Write-Verbose -Message "$(Get-Date -f T) - Sending to Out-GridView" $TotalReport | Out-GridView -Title "Azure AD Incident Response Audit Detail - $Target" } else { #Return stuff $TotalReport } } #end if ($Token) } #end function ############################################ #FUNCTION: Get-AzureADIRDismissedUserRisk ############################################ function Get-AzureADIRDismissedUserRisk { ############################################################################ <# .SYNOPSIS Gets all Identity Protection User Risk dismissals. .DESCRIPTION Gets User Risk dismissals showing target user details, with last sign-in activity, and the initiating object details, app or user, also with last sign-in activity where that information is available. Also produces optional date and time stamped CSV output. .EXAMPLE Get-AzureADIRDismissedUserRisk -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f Gets user risk dismissals for the target tenant with additional details to the event, i.e. user information. .EXAMPLE Get-AzureADIRDismissedUserRisk -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -CsvOutput Gets user risk dismissals for the target tenant with additional details to the event, i.e. user information. Writes found events to a date and time stamped CSV file in the executing directory. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Use this switch to create a date and time stamped CSV file [Parameter(Position=1)] [switch]$CsvOutput ) ############################################################################ #Filter(s) $Filter = "?`$filter=(activityDisplayName eq 'DismissUser')" ############################################################################ #API endpoint $Url = "https://graph.microsoft.com/beta/auditLogs/directoryAudits$Filter" ############################################################################ #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $Count = 0 $OneSuccessfulFetch = $false $TotalReport = $null #Do while the fetch URL is populated do { Write-Verbose -Message "$(Get-Date -f T) - Invoking web request for $Url" $MyReport = Invoke-AzureADIRWebRequest -Header $Header -Url $Url ############################### #Convert the content from JSON $ConvertedReport = ($MyReport.Content | ConvertFrom-Json).value #Create / null objects array $TotalObjects = @() foreach ($Event in $ConvertedReport) { Write-Verbose -Message "$(Get-Date -f T) - Looking up target ObjectId - $(($Event).TargetResources.id)" $TargetUser = $null $InitiatingObject = $null $InitiatingObjectName = $null $InitiatngObjectId = $null #Get some user details $UserUrl = "https://graph.microsoft.com/beta/users?`$filter=ID eq '$(($Event).TargetResources.id)'&`$select=displayName,userPrincipalName,Id,signInActivity" try { $TargetUser = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $UserUrl -Verbose:$false) } catch {} if ($TargetUser) { Write-Verbose -Message "$(Get-Date -f T) - Target object found" $TargetUser = ($TargetUser.Content | ConvertFrom-Json).Value } else { Write-Warning -Message "$(Get-Date -f T) - Target object not found" } #Check for app or user if ($Event.InitiatedBy.user) { $InitiatngObjectId = $Event.InitiatedBy.user.id Write-Verbose -Message "$(Get-Date -f T) - Looking up initiating ObjectId - $(($Event).InitiatedBy.user.id)" #Get some user details $ObjectUrl = "https://graph.microsoft.com/beta/users?`$filter=ID eq '$(($Event).InitiatedBy.user.id)'&`$select=displayName,userPrincipalName,signInActivity" try { $InitiatingObject = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $ObjectUrl -Verbose:$false) } catch {} if ($InitiatingObject) { Write-Verbose -Message "$(Get-Date -f T) - Object found" $InitiatingObject = ($InitiatingObject.Content | ConvertFrom-Json).Value $InitiatingObjectName = $InitiatingObject.DisplayName } else { Write-Warning -Message "$(Get-Date -f T) - Object not found" } } elseif ($Event.InitiatedBy.app.displayName) { Write-Verbose -Message "$(Get-Date -f T) - Capturing intitiating app details" $InitiatingObjectName = $Event.InitiatedBy.app.displayName } #Construct a custom object $Properties = [PSCustomObject]@{ LoggingService = $Event.loggedByService ActivityStatus = $Event.result ActivityType = $Event.activityDisplayName EventTime = $Event.activityDateTime CorrelationId = $Event.correlationId TargetObjectDisplayName = $TargetUser.DisplayName TargetObjectId = $TargetUser.Id TargetObjectUpn = $TargetUser.userPrincipalName TargetObjectLastSignIn = $TargetUser.signInActivity.lastSignInDateTime InitiatingObjectDisplayName = $InitiatingObjectName InitiatingObjectId = $InitiatngObjectId InitiatingObjectUpn = $InitiatingObject.userPrincipalName InitiatingtObjectLastSignIn = $InitiatingObject.signInActivity.lastSignInDateTime } $TotalObjects += $Properties } #Add to concatenated findings [array]$TotalReport += $TotalObjects #Update the fetch url to include the paging element $Url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink' #Update the access token on the second iteration if ($OneSuccessfulFetch) { $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken $Header = Get-AzureADIRHeader -Token $Token } #Update count and show for this cycle $Count = $Count + $ConvertedReport.Count Write-Verbose -Message "$(Get-Date -f T) - Total records fetched: $count" #Update tracking variables $OneSuccessfulFetch = $true } while ($Url) #end do / while #See if we need to write to CSV if ($CsvOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $DismissedUserRiskEvents = "DismissedUserRiskEvents_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for dismissed user risk events" $TotalReport | Export-Csv -Path $DismissedUserRiskEvents -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Dismissed user risk events written to $(Get-Location)\$DismissedUserRiskEvents" } else { #Return stuff $TotalReport } } } #end function ########################################## #FUNCTION: Get-AzureADIRSsprUsageHistory ########################################## function Get-AzureADIRSsprUsageHistory { ############################################################################ <# .SYNOPSIS Gets SSPR usage history. .DESCRIPTION Gets SSPR usage history, i.e. reset related events in the tenant. Can retrieve just successful or just failure events. Can also produce a date and time stamped CSV file as output. .EXAMPLE Get-AzureADIRSsprUsageHistory -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f Gets all SSPR usage history for the target tenant. .EXAMPLE Get-AzureADIRSsprUsageHistory -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -CsvOutput Gets all SSPR usage history for the target tenant. Writes the output to a date and time stamped CSV file in the execution directory. .EXAMPLE Get-AzureADIRSsprUsageHistory -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -Status Success -CsvOutput Gets all successful SSPR usage events for the target tenant. Writes the output to a date and time stamped CSV file in the execution directory. .EXAMPLE Get-AzureADIRSsprUsageHistory -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -Status Failure Gets all failed SSPR usage events for the target tenant. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Filter on success or failure events [Parameter(Position=1)] [ValidateSet('Success','Failure')] [string]$Status, #Use this switch to create a date and time stamped CSV file [Parameter(Position=2)] [switch]$CsvOutput ) ############################################################################ #Deal with different search criterea if ($Status -eq 'Success') { $Filter = "&`$filter=(isSuccess eq true)" Write-Verbose -Message "$(Get-Date -f T) - Successful SSPR events selected" } elseif ($Status -eq 'Failure') { $Filter = "&`$filter=(isSuccess eq false)" Write-Verbose -Message "$(Get-Date -f T) - failed SSPR events selected" } ############################################################################ #API endpoint $Url = "https://graph.microsoft.com/beta/reports/userCredentialUsageDetails?`$orderby=userDisplayName asc$Filter" ############################################################################ #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $TotalReport = $null #Call the API query loop $TotalReport = Invoke-AzureADIRDoWhile -Header $Header -Url $Url } #See if we need to write to CSV if ($CsvOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $CsvName = "SsprUsageHistory_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for SSPR usage history" $TotalReport | Export-Csv -Path $CsvName -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - SSPR usage history written to $(Get-Location)\$CsvName" } else { #Return stuff $TotalReport } } #end function ################################################# #FUNCTION: Get-AzureADIRUserLastSignInActivity ################################################# function Get-AzureADIRUserLastSignInActivity { ############################################################################ <# .SYNOPSIS Gets Azure Active Directory user last interactive sign-in activity details. .DESCRIPTION Gets Azure Active Directory user last interactive sign-in activity details using the signInActivity.lastSignInDateTime attribute. Use -All to get details for all users in the target tenant. Use -UserObjectId to target a single user or groups of users. Use -StaleThreshold to see details of users whose sign-in activity is before a certain datetime threshold. Use -GuestInfo to include additional information specific to guest accounts Can also produce a date and time stamped CSV file as output. .EXAMPLE Get-AzureADIRUserLastSignInActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -All Gets the last interactive sign-in activity for all users on the tenant. .EXAMPLE Get-AzureADIRUserLastSignInActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -UserObjectId 69447235-0974-4af6-bfa3-d0e922a92048 -CsvOutput Gets the last interactive sign-in activity for the user, targeted by their object ID. Writes the output to a date and time stamped CSV file in the execution directory. .EXAMPLE Get-AzureADIRUserLastSignInActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -StaleThreshold 60 -GuestInfo -CsvOutput Gets all users whose last interactive sign-in activity is before the stale threshold of 60 days. Writes the output to a date and time stamped CSV file in the execution directory. Includes additional attributes for guest user insight. .EXAMPLE Get-AzureADIRUserLastSignInActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -StaleThreshold 30 Gets all users whose last interactive sign-in activity is before the stale threshold of 30 days. .EXAMPLE Get-AzureADIRUserLastSignInActivity -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -StaleThreshold 30 -GuestInfo Gets all users whose last interactive sign-in activity is before the stale threshold of 30 days. Includes additional attributes for guest user insight. #> ############################################################################ [CmdletBinding(DefaultParameterSetName="All")] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Get sign-in activity for all users in the tenant [Parameter(Mandatory,Position=1,ParameterSetName="All")] [switch]$All, #Get the sign-in activity for a single user by object ID [Parameter(Mandatory,Position=2,ParameterSetName="UserObjectId")] [string]$UserObjectId, #The number of days before which accounts are considered stale [Parameter(Mandatory,Position=3,ParameterSetName="Threshold")] [ValidateSet(30,60,90)] [int32]$StaleThreshold, #Include additio al information for guest accounts [Parameter(Position=4)] [switch]$GuestInfo, #Use this switch to create a date and time stamped CSV file [Parameter(Position=5)] [switch]$CsvOutput ) ############################################################################ #Deal with different search criterea if ($All) { #API endpoint $Filter = "?`$select=displayName,userPrincipalName,Id,signInActivity,userType,externalUserState,creationType,createdDateTime" Write-Verbose -Message "$(Get-Date -f T) - All user mode selected" } elseif ($UserObjectId) { #API endpoint $Filter = "?`$filter=ID eq '$UserObjectId'&`$select=displayName,userPrincipalName,Id,signInActivity,userType,externalUserState,creationType,createdDateTime" Write-Verbose -Message "$(Get-Date -f T) - Single user mode selected" } elseif ($StaleThreshold) { Write-Verbose -Message "$(Get-Date -f T) - Stale mode selected" #Obtain a datetime object before which accounts are considered stale $DaysAgo = (Get-Date (Get-Date).AddDays(-$StaleThreshold) -Format s) + "Z" Write-Verbose -Message "$(Get-Date -f T) - Stale threshold set to $DaysAgo" #API endpoint $Select = "&`$select=displayName,userPrincipalName,Id,signInActivity,userType,externalUserState,creationType,createdDateTime" $Filter = "?`$filter=signInActivity/lastSignInDateTime le $DaysAgo$Select" } ############################################################################ $Url = "https://graph.microsoft.com/beta/users$Filter" ############################################################################ #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { if ($All) { #Construct header with access token and ConsistencyLevel = Eventual $Header = Get-AzureADIRHeader -Token $Token -ConsistencyLevelEventual $CountUrl = "https://graph.microsoft.com/beta/users/`$count" Write-Verbose -Message "$(Get-Date -f T) - Invoking web request for $CountUrl" #Now make a call to get the number of users try { $UserCount = (Invoke-WebRequest -Headers $Header -Uri $CountUrl -Verbose:$false) } catch {} if ($UserCount) { Write-Verbose -Message "$(Get-Date -f T) - $UserCount users found in tenant" #Estimate execution time if ($CsvOutput) { $ExTime = (0.03 * $UserCount.Content) } else { $ExTime = (0.035 * $UserCount.Content) } $ExTimeSpan = [timespan]::FromSeconds($ExTime) Write-Verbose -Message "$(Get-Date -f T) - Estimated function execution time is $($ExTimeSpan.Hours) hours, $($ExTimeSpan.Minutes) minutes, $($ExTimeSpan.Seconds) seconds" #Light up the progress bar in the later loop $ShowProgress = $true } else { Write-Warning -Message "$(Get-Date -f T) - User count unobtainable - unable to estimate function execution time" } } else { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token } #Tracking variables $Count = 0 $OneSuccessfulFetch = $false $TotalReport = $null $i = 1 #Do while the fetch URL is populated do { Write-Verbose -Message "$(Get-Date -f T) - Invoking web request for $Url" $MyReport = Invoke-AzureADIRWebRequest -Header $Header -Url $Url ############################### #Convert the content from JSON $ConvertedReport = ($MyReport.Content | ConvertFrom-Json).value $TotalObjects = @() foreach ($User in $ConvertedReport) { if ($GuestInfo) { #Construct a custom object $Properties = [PSCustomObject]@{ displayName = $User.displayName userPrincipalName = $User.userPrincipalName objectId = $User.Id lastSignInDateTime = $User.signInActivity.lastSignInDateTime lastSignInRequestId = $User.signInActivity.lastSignInRequestId userType = $User.userType createdDateTime = $User.createdDateTime externalUserState = $User.externalUserState creationType = $User.creationType } } else { #Construct a custom object $Properties = [PSCustomObject]@{ displayName = $User.displayName userPrincipalName = $User.userPrincipalName objectId = $User.Id lastSignInDateTime = $User.signInActivity.lastSignInDateTime lastSignInRequestId = $User.signInActivity.lastSignInRequestId } } $TotalObjects += $Properties #Progress bar when targeting all users if ($ShowProgress) { Write-Progress -Activity "Processing..." ` -Status ("Checked {0}/{1} user accounts" -f $i++, $UserCount.Content) ` -PercentComplete ((($i -1) / $UserCount.Content) * 100) } } #Add to concatenated findings [array]$TotalReport += $TotalObjects #Update the fetch url to include the paging element $Url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink' #Update the access tokenon the second iteration if ($OneSuccessfulFetch) { $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken $Header = Get-AzureADIRHeader -Token $Token } #Update count and show for this cycle $Count = $Count + $ConvertedReport.Count Write-Verbose -Message "$(Get-Date -f T) - Total records fetched: $count" #Update tracking variables $OneSuccessfulFetch = $true } while ($Url) #end do / while } #See if we need to write to CSV if ($CsvOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $CsvName = "UserLastSignInDetails_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for last user Sign-In details" $TotalReport | Export-Csv -Path $CsvName -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Last user sign-in details written to $(Get-Location)\$CsvName" } else { #Return stuff $TotalReport } } #end function #endregion ################################# ################################# #region 5) PRIVILEGE #################################################### #FUNCTION: Get-AzureADIRPrivilegedRoleAssignment #################################################### function Get-AzureADIRPrivilegedRoleAssignment { ############################################################################ <# .SYNOPSIS Gets a list of directory roles and members. .DESCRIPTION Gets the currently populated directory roles and finds their members. Can write the results to a time and date-stamped CSV. .EXAMPLE Get-AzureADIRPrivilegedRoleAssignment -TenantId 98cfcac2-9255-41a9-b206-a8cfad3998cc -CsVOutput Gets a list of directory roles and members and saves then to a date and time stamped CSV file in the execution directory. .EXAMPLE Get-AzureADIRPrivilegedRoleAssignment -TenantId 98cfcac2-9255-41a9-b206-a8cfad3998cc Gets a list of directory roles and members and displays them to the host. .EXAMPLE Get-AzureADIRPrivilegedRoleAssignment -UserObjectId 704bb78b-103f-4e22-807f-4312c68af4c1 Gets a list of directory roles that the target user is a member of. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #User objectID to target [Parameter(Position=1)] [string]$UserObjectId, #Use this switch to create a date and time stamped CSV file [Parameter(Position=2)] [switch]$CsvOutput ) ############################################################################ #Get tenant details to test that Connect-AzureADIR has been called try { $TenantDetails = Get-AzureADTenantDetail } catch { Write-Warning -Message "$(Get-Date -f T) - You must call Connect-AzureADIR to run this function" Write-Verbose "$(Get-Date -f T) - Calling Connect AzureADIR" Connect-AzureADIR -TenantId $TenantId } $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" #Get a list of directory roles Write-Verbose -Message "$(Get-Date -f T) - Attempting to get directory roles" try {$Roles = Get-AzureADDirectoryRole -ErrorAction SilentlyContinue} catch {} if ($Roles) { Write-Verbose -Message "$(Get-Date -f T) - $(($Roles).Count) directory roles found" #Loop through the roles, get members and add as a ps cutome object to an array foreach ($Role in $Roles) { #Make Company Admin show as Global Admin if ($Role.DisplayName -eq "Company Administrator") { $DirectoryRole = "Global Administrator" } else { $DirectoryRole = $Role.DisplayName } #Get role members Write-Verbose -Message "$(Get-Date -f T) - Attempting to get role members for $DirectoryRole" try {$RoleMembers = Get-AzureADDirectoryRoleMember -ObjectId $Role.ObjectId -ErrorAction SilentlyContinue} catch {} if ($RoleMembers) { Write-Verbose -Message "$(Get-Date -f T) - $(($RoleMembers).Count) members found for $DirectoryRole" Write-Verbose -Message "$(Get-Date -f T) - Looping through role members" foreach ($RoleMember in $RoleMembers) { $AlternateEmail = $RoleMember.OtherMails -join ";" $Properties = [PSCustomObject]@{ DirectoryRole = $DirectoryRole DirectoryRoleObjectId =$Role.ObjectId RoleMemberName = $RoleMember.DisplayName RoleMemberObjectType = $RoleMember.ObjectType RoleMemberUPN = $RoleMember.UserPrincipalName RoleMemberObjectId = $RoleMember.ObjectId RoleMemberEnabled = $RoleMember.AccountEnabled RoleMemberMail = $RoleMember.Mail RoleMemberAlternateEmail = $AlternateEmail RoleMemberOnPremDn = $RoleMember.ExtensionProperty.onPremisesDistinguishedName } [array]$TotalObjects += $Properties } } else { Write-Warning -Message "$(Get-Date -f T) - No role memberships obtained for $DirectoryRole" } } } else { Write-Warning -Message "$(Get-Date -f T) - No directory roles obtained" } #Filter for target user object ID if ($UserObjectId) { $TotalObjects = $TotalObjects | Where-Object {$_.RoleMemberObjectId -eq $UserObjectId} } #See if we need to write to CSV if ($CsvOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $PrivilegedRoleAssignments = "PrivilegedRoleAssignments_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for privileged role assignments" $TotalObjects | Export-Csv -Path $PrivilegedRoleAssignments -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Privileged role assignments CSV written to $(Get-Location)\$PrivilegedRoleAssignments" } else { $TotalObjects } } #end function ######################################################### #FUNCTION: Get-AzureADIRPrivilegedUserOnPremCorrelation ######################################################### function Get-AzureADIRPrivilegedUserOnPremCorrelation { ############################################################################ <# .SYNOPSIS Gets a list of directory roles, members and any associated on-premises privileged groups. .DESCRIPTION Gets the currently populated directory roles, finds their members, checks to see if the member has an on-premises Distinsguished Name and checks for on-premises privilege. If privilege exists, enumerates the users on-premises groups and checks the groups privilege status. Can write the results to a time and date-stamped CSV. NB - requires the Active Directory PowerShell module and line of siight of a domain controller in the target domain. .EXAMPLE Get-AzureADIRPrivilegedUserOnPremCorrelation -TenandId 98cfcac2-9255-41a9-b206-a8cfad3998cc -OnPremDomain "Consoto.local" -CsVOutput Gets a list of directory roles and members that have a privileged status in the on-premises domain contoso.com. Writes the output to a date and time stamped CSV file in the execution directory. .EXAMPLE Get-AzureADIRPrivilegedUserOnPremCorrelation -TenandId 98cfcac2-9255-41a9-b206-a8cfad3998cc -OnPremDomain "Consoto.local" Gets a list of directory roles and members that have a privileged status in the on-premises domain contoso.local. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #The target Windows Server Active Directory domain in which to find the linked accounts [Parameter(Mandatory,Position=1)] [string]$OnPremDomain, #Use this switch to create a date and time stamped CSV file [Parameter(Position=2)] [switch]$CsvOutput ) ############################################################################ #Check to see if we have the on-prem Active Directory powershell module $ActiveDirectory = Get-Module -ListAvailable ActiveDirectory -Verbose:$false -ErrorAction SilentlyContinue if ($ActiveDirectory) { Write-Verbose -Message "$(Get-Date -f T) - ActiveDirectory PowerShell module installed" try {$RetrieveObject = Get-ADDomain -Server $OnPremDomain} catch {} if ($RetrieveObject) { Write-Verbose -Message "$(Get-Date -f T) - Active Directory domain - $OnPremDomain - contacted" #Get tenant details to test that Connect-AzureADIR has been called try { $TenantInfo = Get-AzureADTenantDetail } catch { Write-Warning -Message "$(Get-Date -f T) - You must call Connect-AzureADIR to run this function" Write-Verbose "$(Get-Date -f T) - Calling Connect AzureADIR" Connect-AzureADIR -TenantId $TenantId } } else { Write-Error -Message "Please ensure you have line of site to a domain controller for the target domain - $OnPremDomain" ` -ErrorAction Stop } } else { Write-Error -Message "Please install the Windows Server Active Directory PowerShell module" ` -ErrorAction Stop } #Display Azure AD Domain $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" #Display Windows Server AD Domain Write-Verbose -Message "$(Get-Date -f T) - Target Active Directory domain - $(($RetrieveObject).DistinguishedName)" #Call the Get-AzureADIRPrivilegedRoleAssignment function Write-Verbose -Message "$(Get-Date -f T) - Calling Get-AzureADIRPrivilegedRoleAssignment..." $RoleAssigments = Get-AzureADIRPrivilegedRoleAssignment | Where-Object {$_.RoleMemberOnPremDn} Write-Verbose -Message "$(Get-Date -f T) - $($RoleAssigments.Count) users with an on-prem Distinguished Name" Write-Verbose -Message "$(Get-Date -f T) - Looping through users to check for on-prem privileges" #Loop through the users with an on-prem DN and see if they're privileged foreach ($RoleAssigment in $RoleAssigments) { #Nullify variables $PrivGroups = $null try {$AdUser = Get-ADUser -Server $OnPremDomain -Identity $RoleAssigment.RoleMemberOnPremDn -Properties adminCount,memberOf -ErrorAction SilentlyCOntinue} catch {} if ($AdUser) { Write-Verbose -Message "$(Get-Date -f T) - Windows Server AD user object found for $(($RoleAssigment).RoleMemberOnPremDn)" if ($AdUser.adminCount) { Write-Verbose -Message "$(Get-Date -f T) - User is currently or has been a member of a privileged group" Write-Verbose -Message "$(Get-Date -f T) - Checking user's groups for privileged status" #Update PS Custom Object $RoleAssigment | Add-Member -MemberType NoteProperty -Name 'OnPremPrivilegedStatus' -Value $true foreach ($GroupDn in $AdUser.memberof) { try {$AdGroup = Get-ADGroup -Server $OnPremDomain -Identity $GroupDn -Properties adminCount -ErrorAction SilentlyCOntinue} catch {} if ($AdGroup) { Write-Verbose -Message "$(Get-Date -f T) - Windows Server AD group object found for $GroupDn" if ($AdGroup.adminCount) { Write-Verbose -Message "$(Get-Date -f T) - Group is currently privileged or has been privileged" if ($CsvOutput) { [string]$PrivGroups += ";'$GroupDn'" } else { [array]$PrivGroups += $GroupDn } } } else { Write-Warnimg -Message "$(Get-Date -f T) - Windows Server AD group object not found for $GroupDn" } } #Update PS Custom Object if ($CsvOutput) { $RoleAssigment | Add-Member -MemberType NoteProperty -Name 'OnPremPrivilegedGroups' -Value $PrivGroups.TrimStart(";") } else { $RoleAssigment | Add-Member -MemberType NoteProperty -Name 'OnPremPrivilegedGroups' -Value $PrivGroups } #Add to total array [array]$TotalObjects += $RoleAssigment } } else { Write-Warnimg -Message "$(Get-Date -f T) - Windows Server AD user object not found for $(($RoleAssigment).RoleMemberOnPremDn)" } } #See if we need to write to CSV if ($CsvOutput) { #Output files $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $PrivilegedRoleAssignments = "PrivilegedRolesOnPremCorrelations_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for privileged role assignments and on-prem correlations" $TotalObjects | Export-Csv -Path $PrivilegedRoleAssignments -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Privileged role assignments and on-prem correlations CSV written to $(Get-Location)\$PrivilegedRoleAssignments" } else { $TotalObjects } } #end function ####################################################### #FUNCTION: Get-AzureADIRPimPrivilegedRoleAssignment ####################################################### function Get-AzureADIRPimPrivilegedRoleAssignment { ############################################################################ <# .SYNOPSIS Gets PIM privileged roles assignments. .DESCRIPTION Gets PIM privileged roles assignments with additional role and user details. Can produce CSV output. .EXAMPLE Get-AzureADIRPimPrivilegedRoleAssignment -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -All Gets PIM privileged role assignments for the target tenant. .EXAMPLE Get-AzureADIRPimPrivilegedRoleAssignment -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -ActiveOnly Gets PIM privileged role assignments for the target tenant. Only returns active users. .EXAMPLE Get-AzureADIRPimPrivilegedRoleAssignment -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -UserObjectId 704bb78b-103f-4e22-807f-4312c68af4c1 Gets PIM privileged role assignments for the target user in the target tenant. .EXAMPLE Get-AzureADIRPimPrivilegedRoleAssignment -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -All -CsvOutput Gets PIM privileged role assignments for the target tenant. Writes the output to a date and time stamped CSV file in the target directory. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Get sign-in activity for all users in the tenant [Parameter(Mandatory,Position=1,ParameterSetName="All")] [switch]$All, #Use this switch to list active assignments (includes permanent assignments) [Parameter(Mandatory,Position=2,ParameterSetName="Active")] [switch]$ActiveOnly, #Use this parameter to target a specific user [Parameter(Mandatory,Position=3,ParameterSetName="User")] [string]$UserObjectId, #Use this switch to create a date and time stamped CSV file [Parameter(Position=4)] [switch]$CsvOutput ) ############################################################################ #API endpoint if ($ActiveOnly) { $Url = "https://graph.microsoft.com/beta/privilegedAccess/aadroles/resources/$TenantId/roleAssignments?`$filter=assignmentState eq 'Active'" } elseif ($UserObjectId) { $Url = "https://graph.microsoft.com/beta/privilegedAccess/aadroles/resources/$TenantId/roleAssignments?`$filter=subjectId eq '$UserObjectId'" } else { $Url = "https://graph.microsoft.com/beta/privilegedAccess/aadroles/resources/$TenantId/roleAssignments" } ############################################################################ #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $Count = 0 $OneSuccessfulFetch = $false $TotalReport = $null #Do while the fetch URL is populated do { Write-Verbose -Message "$(Get-Date -f T) - Invoking web request for $Url" $MyReport = Invoke-AzureADIRWebRequest -Header $Header -Url $Url ############################### #Convert the content from JSON $ConvertedReport = ($MyReport.Content | ConvertFrom-Json).value #Create / null objects array $TotalObjects = @() foreach ($Event in $ConvertedReport) { #Get some role details Write-Verbose -Message "$(Get-Date -f T) - Looking up role definition details - $(($Event).roleDefinitionId)" $TargetRole = $null $RoleUrl = "https://graph.microsoft.com/beta/privilegedAccess/aadroles/resources/$TenantId/roleDefinitions?`$filter=(id eq '$(($Event).roleDefinitionId)')&`$Select=displayName,Type" try { $TargetRole = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $RoleUrl -Verbose:$false) } catch {} if ($TargetRole) { Write-Verbose -Message "$(Get-Date -f T) - Target role found" $TargetRole = ($TargetRole.Content | ConvertFrom-Json).Value } else { Write-Warning -Message "$(Get-Date -f T) - Target role not found" } #Get some user details Write-Verbose -Message "$(Get-Date -f T) - Looking up assigned user details - $(($Event).subjectId)" $TargetUser = $null $UserUrl = "https://graph.microsoft.com/beta/users?`$filter=ID eq '$(($Event).subjectId)'&`$select=displayName,userPrincipalName,Id,signInActivity" try { $TargetUser = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $UserUrl -Verbose:$false) } catch {} if ($TargetUser) { Write-Verbose -Message "$(Get-Date -f T) - Target user found" $TargetUser = ($TargetUser.Content | ConvertFrom-Json).Value } else { Write-Warning -Message "$(Get-Date -f T) - Target user not found" } #Construct a custom object $Properties = [PSCustomObject]@{ RoleName = $TargetRole.displayName RoleType = $TargetRole.Type RoleId = $Event.roleDefinitionId UserDisplayName = $TargetUser.DisplayName UserId = $TargetUser.Id UserUpn = $TargetUser.userPrincipalName UserLastSignIn = $TargetUser.signInActivity.lastSignInDateTime AssignmentType = $Event.memberType AssignmentState = $Event.assignmentState AssignmentStatus = $Event.status AssignmentStart = $Event.startDateTime AssignmentEnd = $Event.endDateTime } $TotalObjects += $Properties } #Add to concatenated findings [array]$TotalReport += $TotalObjects #Update the fetch url to include the paging element $Url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink' #Update the access token on the second iteration if ($OneSuccessfulFetch) { $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken $Header = Get-AzureADIRHeader -Token $Token } #Update count and show for this cycle $Count = $Count + $ConvertedReport.Count Write-Verbose -Message "$(Get-Date -f T) - Total records fetched: $count" #Update tracking variables $OneSuccessfulFetch = $true } while ($Url) #end do / while #See if we need to write to CSV if ($CsvOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $CsvName = "PimPrivilegedRoleAssignments_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for PIM role assignments" $TotalReport | Export-Csv -Path $CsvName -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - PIM role assignment details written to $(Get-Location)\$CsvName" } else { #Return stuff $TotalReport } } } #end function ############################################################# #FUNCTION: Get-AzureADIRPimPrivilegedRoleAssignmentRequest ############################################################# function Get-AzureADIRPimPrivilegedRoleAssignmentRequest { ############################################################################ <# .SYNOPSIS Gets PIM assignment related activity. .DESCRIPTION Gets all PIM assignment related events from the target tenant. Can produce a time and date stamped CSV file. .EXAMPLE Get-AzureADIRPimPrivilegedRoleAssignmentRequest -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f Gets all PIM assignment related events from the target tenant. .EXAMPLE Get-AzureADIRPimPrivilegedRoleAssignmentRequest -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -CsvOutput Gets all PIM assignment related events from the target tenant. Produces a time and date stamped CSV file. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Use this switch to create a date and time stamped CSV file [Parameter(Position=1)] [switch]$CsvOutput ) ############################################################################ #API endpoint $Url = "https://graph.microsoft.com/beta/privilegedAccess/aadroles/resources/$TenantId/roleAssignmentRequests" ############################################################################ #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $Count = 0 $OneSuccessfulFetch = $false $TotalReport = $null #Do while the fetch URL is populated do { Write-Verbose -Message "$(Get-Date -f T) - Invoking web request for $Url" $MyReport = Invoke-AzureADIRWebRequest -Header $Header -Url $Url ############################### #Convert the content from JSON $ConvertedReport = ($MyReport.Content | ConvertFrom-Json).value #Create / null objects array $TotalObjects = @() foreach ($Event in $ConvertedReport) { #Get some role details Write-Verbose -Message "$(Get-Date -f T) - Looking up role definition details - $(($Event).roleDefinitionId)" $TargetRole = $null $RoleUrl = "https://graph.microsoft.com/beta/privilegedAccess/aadroles/resources/$TenantId/roleDefinitions?`$filter=(id eq '$(($Event).roleDefinitionId)')&`$Select=displayName,Type" try { $TargetRole = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $RoleUrl -Verbose:$false) } catch {} if ($TargetRole) { Write-Verbose -Message "$(Get-Date -f T) - Target role found" $TargetRole = ($TargetRole.Content | ConvertFrom-Json).Value } else { Write-Warning -Message "$(Get-Date -f T) - Target role not found" } #Get some user details Write-Verbose -Message "$(Get-Date -f T) - Looking up assigned user details - $(($Event).subjectId)" $TargetUser = $null $UserUrl = "https://graph.microsoft.com/beta/users?`$filter=ID eq '$(($Event).subjectId)'&`$select=displayName,userPrincipalName,Id,signInActivity" try { $TargetUser = (Invoke-WebRequest -UseBasicParsing -Headers $Header -Uri $UserUrl -Verbose:$false) } catch {} if ($TargetUser) { Write-Verbose -Message "$(Get-Date -f T) - Target user found" $TargetUser = ($TargetUser.Content | ConvertFrom-Json).Value } else { Write-Warning -Message "$(Get-Date -f T) - Target user not found" } #Construct a custom object $Properties = [PSCustomObject]@{ RequestedRoleName = $TargetRole.displayName RequestedRoleType = $TargetRole.Type RequestedRoleId = $Event.roleDefinitionId RequestingUserDisplayName = $TargetUser.DisplayName RequestingUserId = $TargetUser.Id RequestingUserUpn = $TargetUser.userPrincipalName RequestingUserLastSignIn = $TargetUser.signInActivity.lastSignInDateTime AssignmentRequestType = $Event.type AssignmentRequestState = $Event.assignmentState AssignmentRequestStatus = $Event.status.status AssignmentRequestDate = $Event.requestedDateTime AssignmentRequestId = $Event.id } $TotalObjects += $Properties } #Add to concatenated findings [array]$TotalReport += $TotalObjects #Update the fetch url to include the paging element $Url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink' #Update the access token on the second iteration if ($OneSuccessfulFetch) { $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken $Header = Get-AzureADIRHeader -Token $Token } #Update count and show for this cycle $Count = $Count + $ConvertedReport.Count Write-Verbose -Message "$(Get-Date -f T) - Total records fetched: $count" #Update tracking variables $OneSuccessfulFetch = $true } while ($Url) #end do / while #See if we need to write to CSV if ($CsvOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $CsvName = "PimPrivilegedRoleAssignments_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Generating a CSV for PIM assignment requests" $TotalReport | Export-Csv -Path $CsvName -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - PIM assignment request details written to $(Get-Location)\$CsvName" } else { #Return stuff $TotalReport } } } #end function #endregion ################################# ################################# #region 6) SECURITY CREDENTIALS ################################################ #FUNCTION: Get-AzureADIRMfaAuthMethodAnalysis ############################################### function Get-AzureADIRMfaAuthMethodAnalysis { ########################################################################################################## ########################################################################################################## <# .SYNOPSIS Analyses Azure AD users to make recommendations on how to improve their MFA stance. .DESCRIPTION Analyses Azure AD users to make recommendations on how to improve each user's MFA configuration. Can target a group by ObjectId or analyse all users in a tenant. Can add user-specific location information: UPN domain, usage location and country. Can produce a date and time stamped CSV report of per user recommendations. IMPORTANT: * You can not use a guest (B2B) account to run this script against the target tenant. This is a limitation of the MSOnline PowerShell module. The script will execute in the guest's home tenant, not the target tenant. * Ensure you run the script with an account that can enumerate user properties. For least privilege use the User Administrator role .EXAMPLE Get-AzureADIRMfaAuthMethodAnalysis -TenantId 9959f32b-837b-41db-b6e5-32277e344292 Creates per user recommendations for all users in the target tenant and displays the results to screen. .EXAMPLE Get-AzureADIRMfaAuthMethodAnalysis -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -TargetGroup 6424cd24-ee16-472f-bad6-85427c9febc2 Creates per user recommendations for each user in the target group and displays the results to screen. .EXAMPLE Get-AzureADIRMfaAuthMethodAnalysis -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -CsvOutput -Verbose Creates a date and time stamped CSV file in the scripts execution directory with per user recommendations for all users in the tenant. Has verbose notation to screen. .EXAMPLE Get-AzureADIRMfaAuthMethodAnalysis -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -LocationInfo -CsvOutput Creates a date and time stamped CSV file in the scripts execution directory with per user recommendations for all users in the tenant. Includeds location information: UPN domain, usage location and country. .EXAMPLE Get-AzureADIRMfaAuthMethodAnalysis -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -TargetUser b24c24ac-5671-444b-ba58-0305c1c72cb0 Creates a user recommendation for the target user b24c24ac-5671-444b-ba58-0305c1c72cb0. .EXAMPLE Get-Content .\User_ObjectIDs.txt | ForEach-Object { Get-AzureADIRMfaAuthMethodAnalysis -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -TargetUser $_ } Gets the contents of user_objectIDs.txt. Takes each user object ID from the file and runs it against the function to return a per user analysis to screen. #> ########################################################################################################## ################################ #Define and validate Parameters ################################ [CmdletBinding()] param( #The unique ID of the tenant to target for analysis [Parameter(Mandatory,Position=0)] [guid]$TenantId, #The unique ID of the group to analyse [Parameter(Position=1)] [string]$TargetGroup, #The unique ID of the group to analyse [Parameter(Position=2)] [string]$TargetUser, #Use this switch to include user-specific location information [Parameter(Position=3)] [switch]$LocationInfo, #Use this switch to create a date and time stamped CSV file [Parameter(Position=4)] [switch]$CsvOutput ) ########################################################################################################## ################## #region Functions ################## ############################################# function Measure-MsolUserStrongAuthMethod { [CmdletBinding()] param( #A user object to process [Parameter(ValueFromPipeline,Position=0)] [Microsoft.Online.Administration.User]$User ) #Set user properties $UserPrincipalName = $_.UserPrincipalName $DisplayName = $_.DisplayName [string]$ObjectId = $_.ObjectId if ($LocationInfo) { $UpnDomain = ($_.UserPrincipalName).Split("@")[1] $UsageLocation = $_.UsageLocation $Country = $_.Country } $MfaAuthMethodCount = $_.StrongAuthenticationMethods.Count #Count number of methods if ($MfaAuthMethodCount -eq 0) { [array]$Recommendations = "'Register for MFA, preferably with the Microsoft Authenticator mobile app and also with a phone number, used for SMS or Voice.'" } else { #Do some analysis switch ($_.StrongAuthenticationMethods) { #Check default method {$_.IsDefault -eq $true} { $DefaultMethod = $_.MethodType if ($_.MethodType -ne "PhoneAppNotification") { [array]$Recommendations += "'Consider setting the Microsoft Authenticator mobile app as the default method.'" } } #Check for method type - PhoneAppNotification {$_.MethodType -eq "PhoneAppNotification"} { $AppNotification = "Yes" if ($MfaAuthMethodCount -eq 1) { [array]$Recommendations += "'Register at least another authentication method, preferably a verification code from the mobile app or hardware OATH token. A user can have up to five hardware OATH tokens or mobile apps registered. Phone number can also be used for Voice or SMS.'" } } #Check for method type - PhoneAppOTP {$_.MethodType -eq "PhoneAppOTP"} { $OathTotp = "Yes" if ($MfaAuthMethodCount -eq 1) { [array]$Recommendations += "'Register at least another authentication method, preferably the Microsoft Authenticator mobile app. A user can have up to five hardware OATH tokens or mobile apps registered.'" } } #Check for method type - OneWaySMS {$_.MethodType -eq "OneWaySMS"} { $SMS = "Yes" } #Check for method type - TwoWayVoiceMobile {$_.MethodType -eq "TwoWayVoiceMobile"} { $Phone = "Yes" } #Check for method type - OneWaySMS {$_.MethodType -eq "TwoWayVoiceAlternateMobile"} { $AltPhone = "Yes" } } } #More recommendations - phone options only if ((($SMS) -and ($Phone)) -and ((!$OathTotp) -and (!$AppNotification))) { [array]$Recommendations += "'Register at least another authentication method, preferably the Microsoft Authenticator mobile app or hardware OATH token. A user can have up to five hardware OATH tokens or mobile apps registered.'" } #More recommendations - Notification and OATH OTP, no phone nubers if (((!$SMS) -and (!$Phone) -and (!$AltPhone)) -and (($OathTotp) -and ($AppNotification))) { [array]$Recommendations += "'Register a phone number to be used for SMS and Voice.'" } #More recommendations - if no Alternative phone number if (!$AltPhone) { [array]$Recommendations += "'Consider adding an alternative phone number for additional resilience.'" } if ($LocationInfo) { $AnalysedUser = [pscustomobject]@{ UserPrincipalName = $UserPrincipalName DisplayName = $DisplayName ObjectId = $ObjectId UpnDomain = $UpnDomain UsageLocation = $UsageLocation Country = $Country MfaAuthMethodCount = $MfaAuthMethodCount DefaultMethod = $DefaultMethod AppNotification = $AppNotification OathTotp = $OathTotp Sms = $Sms Phone = $Phone AltPhone = $AltPhone Recommendations = $Recommendations } } else { $AnalysedUser = [pscustomobject]@{ UserPrincipalName = $UserPrincipalName DisplayName = $DisplayName ObjectId = $ObjectId MfaAuthMethodCount = $MfaAuthMethodCount DefaultMethod = $DefaultMethod AppNotification = $AppNotification OathTotp = $OathTotp Sms = $Sms Phone = $Phone AltPhone = $AltPhone Recommendations = $Recommendations } } Write-Verbose -Message "$(Get-Date -f T) - User anaylsis completed" return $AnalysedUser } #end function ######################################################### #Function to create a CSV friendly object for conversion function Expand-Recommendation { [cmdletbinding()] param ( [parameter(ValueFromPipeline)] [psobject]$PsCustomObject ) begin { #Mark that we don't have properties $SchemaObtained = $False } process { #If this is the first iteration get object properties if (!$SchemaObtained) { $OutputOrder = $PsCustomObject.psobject.properties.name $SchemaObtained = $true } #Loop thorugh the supplied object and process individually $PsCustomObject | ForEach-Object { #Capture each element $singleGraphObject = $_ #New parent object for edited / expanded values $ExpandedObject = New-Object -TypeName PSObject #Loop through the properties $OutputOrder | ForEach-Object { #Recommendations property has to have commas added if ($_ -eq "Recommendations") { #Ensure we have a non-empty value if there's nothing in Recommendations $CSVLine = " " #Get variables from authMethods property $Properties = $singleGraphObject.$($_) #Loop through each property and add to a single string with a seperating comma (for CSV) $Properties | ForEach-Object { $CSVLine += "$_," } #Add edited list of values for authmethods property to parent object Add-Member -InputObject $ExpandedObject -MemberType NoteProperty -Name $_ -Value $CSVLine.TrimEnd(0,",").TrimStart() } else { #Add single value property to parent object Add-Member -InputObject $ExpandedObject -MemberType NoteProperty -Name $_ -Value $(($singleGraphObject.$($_) | Out-String).Trim()) } } #Return completed parent object $ExpandedObject } } } #end function #endregion functions ############# #region Main ############# #Tracking variables $UsersProcessed = 0 $ScriptStartTime = Get-Date #Verbose output Write-Verbose -Message "$(Get-Date -f T) - Function started..." if ($LocationInfo) {Write-Verbose -Message "$(Get-Date -f T) - User location information included"} if ($CsvOutput) {Write-Verbose -Message "$(Get-Date -f T) - CSV output selected"} #Some additional paramter validation outside of param() #Try and connect to Azure AD try {$DomainInfo = Get-MsolDomain -TenantId $TenantId -ErrorAction SilentlyContinue} catch {} if ($DomainInfo) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId established" } else { #Present connection pop-up Write-Verbose -Message "$(Get-Date -f T) - Calling Connect-MsolService cmdlet" Connect-MsolService -ErrorAction SilentlyContinue #Populate the DomainInfo variable if Connect-MsolService works if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId established" $DomainInfo = $true } else { Write-Verbose "$(Get-Date -f T) - Connection to $TenantId could not be established" } } #Check if we have a connection if ($DomainInfo) { #Check if we need to create a CSV file if ($CsvOutput) { #Output file $Now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $OutputFile = "MfaAuthMethodAnalysis_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Creating CSV file - $OutputFile" #Create file with header if ($LocationInfo) { Add-Content -Value "UserPrincipalName,DisplayName,ObjectId,UpnDomain,UsageLocation,Country,MfaAuthMethodCount,DefaultMethod,AppNotification,OathTotp,Sms,Phone,AltPhone,Recommendations" ` -Path $OutputFile } else { Add-Content -Value "UserPrincipalName,DisplayName,ObjectId,MfaAuthMethodCount,DefaultMethod,AppNotification,OathTotp,Sms,Phone,AltPhone,Recommendations" ` -Path $OutputFile } if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Header written to CSV file - $OutputFile" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write header to CSV file - $OutputFile" Write-Warning -Message "$(Get-Date -f T) - Reverting to non-CSV output mode" #Prevent further CSV processing $CsvOutput = $false } } #We have a connction so start doing stuff... let's check if we are targetting a group if ($TargetGroup) { Write-Verbose -Message "$(Get-Date -f T) - Checking for target group - $TargetGroup" #Ensure the group is valid try {$GroupInfo = Get-MsolGroup -ObjectId $TargetGroup -ErrorAction SilentlyContinue} catch {} if ($GroupInfo) { Write-Verbose -Message "$(Get-Date -f T) - Group $TargetGroup confirmed as valid" Write-Verbose -Message "$(Get-Date -f T) - Group Display Name = $(($GroupInfo).Displayname); Group Type = $(($GroupInfo).GroupType)" Write-Verbose -Message "$(Get-Date -f T) - Enumerating users for $TargetGroup..." #We have he target group so let's enumerate the users try {$TargetUsers = Get-MsolGroupMember -GroupObjectId $TargetGroup -All} catch {} if ($TargetUsers) { Write-Verbose -Message "$(Get-Date -f T) - $(($TargetUsers).Count) users found" #Now we have users let's get an msol user object $TargetUsers | ForEach-Object { Get-MsolUser -ObjectId $_.objectID -ErrorAction SilentlyContinue | ForEach-Object { Write-Verbose -Message "$(Get-Date -f T) - Processing $(($_).UserPrincipalName)" #Call the analysis function $TargetUserDetail = Measure-MsolUserStrongAuthMethod -User $_ #Determine if we write to screen or file if ($CsvOutput) { Write-Verbose -Message "$(Get-Date -f T) - Converting analysis to CSV format" #Call property expansion function and pipe into a CSV format $CsvFormat = $TargetUserDetail | Expand-Recommendation | ConvertTo-Csv -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Writing conversion to CSV file" #Write the pertinent CSV line Add-Content -Value $CsvFormat[1] -Path $OutputFile if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Details successfully written to CSV file" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write details to CSV file" } } else { #Show user analysis in host $TargetUserDetail } #Increment user count $UsersProcessed++ } } } else { Write-Verbose -Message "$(Get-Date -f T) - $($error[0])" Write-Warning -Message "$(Get-Date -f T) - Issue obtaining members for target group $TargetGroup" Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } } else { Write-Verbose -Message "$(Get-Date -f T) - $($error[0])" Write-Warning -Message "$(Get-Date -f T) - Issue obtaining the target group $TargetGroup" Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } } elseif ($TargetUser) { Write-Verbose -Message "$(Get-Date -f T) - Checking for target user - $TargetUser" #Ensure the group is valid try {$UserInfo = Get-MsolUser -ObjectId $TargetUser -ErrorAction SilentlyContinue} catch {} if ($UserInfo) { $UserInfo | ForEach-Object { Write-Verbose -Message "$(Get-Date -f T) - User $TargetUser confirmed as valid" Write-Verbose -Message "$(Get-Date -f T) - User Display Name = $(($UserInfo).Displayname)" #Call the analysis function $TargetUserDetail = Measure-MsolUserStrongAuthMethod #Determine if we write to screen or file if ($CsvOutput) { Write-Verbose -Message "$(Get-Date -f T) - Converting analysis to CSV format" #Call property expansion function and pipe into a CSV format $CsvFormat = $TargetUserDetail | Expand-Recommendation | ConvertTo-Csv -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Writing conversion to CSV file" #Write the pertinent CSV line Add-Content -Value $CsvFormat[1] -Path $OutputFile if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Details successfully written to CSV file" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write details to CSV file" } } else { #Show user analysis in host $TargetUserDetail } #Increment user count $UsersProcessed++ } } else { Write-Verbose -Message "$(Get-Date -f T) - $($error[0])" Write-Warning -Message "$(Get-Date -f T) - Issue obtaining the target user $TargetUser" Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } } else { Write-Verbose -Message "$(Get-Date -f T) - Targetting all users in $TenantId" #We're not tagtetting a group, so let's process all users Get-MsolUser -All -ErrorAction SilentlyContinue | ForEach-Object { Write-Verbose -Message "$(Get-Date -f T) - Processing $(($_).UserPrincipalName)" #Call the analysis function $TargetUserDetail = Measure-MsolUserStrongAuthMethod #Determine if we write to screen or file if ($CsvOutput) { Write-Verbose -Message "$(Get-Date -f T) - Converting analysis to CSV format" #Call property expansion function and pipe into a CSV format $CsvFormat = $TargetUserdetail | Expand-Recommendation | ConvertTo-Csv -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Writing conversion to CSV file" #Write the pertinent CSV line Add-Content -Value $CsvFormat[1] -Path $OutputFile if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Details successfully written to CSV file" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write details to CSV file" } } else { #Show user analysis in host $TargetUserDetail } #Increment user count $UsersProcessed++ } } } else { #We can't connect... say goodbye Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } #Tracking stuff $ScriptEndTime = Get-Date $TimeSpan = $ScriptEndTime - $ScriptStartTime $ProcessingTime = "{0:c}" -f $TimeSpan Write-Verbose -Message "$(Get-Date -f T) - Total users processed: $UsersProcessed" Write-Verbose -Message "$(Get-Date -f T) - Total processing time: $ProcessingTime" Write-Verbose -Message "$(Get-Date -f T) - Function finished!" #endregion main } #end function ################################################### #FUNCTION: Get-AzureADIRMfaPhoneToLocationCheck ################################################### function Get-AzureADIRMfaPhoneToLocationCheck { ########################################################################################################## ########################################################################################################## <# .SYNOPSIS Analyses Azure AD users to compare usage location to MFA / alternative phone number location. .DESCRIPTION Analyses Azure AD users to compare the ISO country code for populated usage location to the international dialling code for the registered MFA or alternative phone number. Displays any users whose MFA phone number or alternative phone number differs from their usage location. Can target an individual user (by ObjectId), a group (by ObjectId) or analyse all users in a tenant. IMPORTANT: * You can not use a guest (B2B) account to run this script against the target tenant. This is a limitation of the MSOnline PowerShell module. The script will execute in the guest's home tenant, not the target tenant. * Ensure you run the script with an account that can enumerate user properties. For least privilege use the User Administrator role. .EXAMPLE Get-AzureADIRMfaPhoneToLocationCheck -TenantId 9959f32b-837b-41db-b6e5-32277e344292 Analyses the usage location information and MFA phone number on a per user basis, for all users in the tenant. Displays the results to screen. .EXAMPLE Get-AzureADIRMfaPhoneToLocationCheck -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -TargetUser 6a9bcbeb-06e8-4af1-bcfa-37099d5127ee Analyses the usage location information and MFA phone number for the target user. Displays the results to screen. .EXAMPLE Get-AzureADIRMfaPhoneToLocationCheck -TenantId 9959f32b-837b-41db-b6e5-32277e344292 -CsvOutput -Verbose Creates a date and time stamped CSV file in the scripts execution directory with per user analysis of usage location and MFA phone number for all users in the tenant. Has verbose notation to screen. #> ########################################################################################################## ################################ #Define and validate Parameters ################################ [CmdletBinding()] param( #The unique ID of the tenant to target for analysis [Parameter(Mandatory,Position=0)] [guid]$TenantId, #The unique ID of the user to analyse [Parameter(Position=1)] [string]$TargetUser, #Use this switch to create a date and time stamped CSV file [Parameter(Position=2)] [switch]$CsvOutput ) ########################################################################################################## ################## #region Functions ################## ############################################# function Measure-MsolMfaPhoneToLocationCorrelation { [CmdletBinding()] param( #A user object to process [Parameter(ValueFromPipeline,Position=0)] [Microsoft.Online.Administration.User]$User ) #Set some variables $UserPrincipalName = $User.UserPrincipalName $DisplayName = $User.DisplayName [string]$ObjectId = $User.ObjectId $UsageLocation = $User.UsageLocation $MfaPhoneNumberPrefix = ($User.StrongAuthenticationUserDetails.PhoneNumber -split " ")[0] $AltPhoneNumberPrefix = ($User.StrongAuthenticationUserDetails.AlternativePhoneNumber -split " ")[0] if ($MfaPhoneNumberPrefix -or $AltPhoneNumberPrefix) { Write-Verbose -Message "$(Get-Date -f T) - User has usage location and phone number present - analysing $(($_).UserPrincipalName)" $TargetUsageCode = $CountryCodes | Where-Object {$_.IsoCode -eq $UsageLocation} $UsageCountry = $TargetUsageCode.Country $TargetMfaCode = $CountryCodes | Where-Object {$_.CountryCode -eq $MfaPhoneNumberPrefix} [array]$MfaPhoneNumberLocation = $TargetMfaCode.IsoCode [array]$MfaPhoneNumberCountry = $TargetMfaCode.Country $TargetAltCode = $CountryCodes | Where-Object {$_.CountryCode -eq $AltPhoneNumberPrefix} [array]$AltPhoneNumberLocation = $TargetAltCode.IsoCode [array]$AltPhoneNumberCountry = $TargetAltCode.Country } #Perform the analysis if (($MfaPhoneNumberPrefix -and ($MfaPhoneNumberLocation -notcontains $UsageLocation)) -or ($AltPhoneNumberPrefix -and ($AltPhoneNumberLocation -notcontains $UsageLocation))) { Write-Warning -Message "$(Get-Date -f T) - User has usage location and phone number mismatch - $(($_).UserPrincipalName)" $AnalysedUser = [pscustomobject]@{ UserPrincipalName = $UserPrincipalName UserDisplayName = $DisplayName UserObjectId = $ObjectId UserUsageLocation = $UsageLocation UserUsageCountry = $UsageCountry MfaPhoneNumberPrefix = $MfaPhoneNumberPrefix MfaPhoneNumberLocation = $MfaPhoneNumberLocation -join "," MfaPhoneNumberCountry = $MfaPhoneNumberCountry -join "," AltPhoneNumberPrefix = $AltPhoneNumberPrefix AltPhoneNumberLocation = $AltPhoneNumberLocation -join "," AltPhoneNumberCountry = $AltPhoneNumberCountry -join "," } return $AnalysedUser } } #endregion Functions ############# #region Main ############# $JsonCodes = @" [ { "Country": "Afghanistan", "CountryCode": "+93", "IsoCode": "AF" }, { "Country": "Albania", "CountryCode": "+355", "IsoCode": "AL" }, { "Country": "Algeria", "CountryCode": "+213", "IsoCode": "DZ" }, { "Country": "American Samoa", "CountryCode": "+1-684", "IsoCode": "AS" }, { "Country": "Andorra", "CountryCode": "+376", "IsoCode": "AD" }, { "Country": "Angola", "CountryCode": "+244", "IsoCode": "AO" }, { "Country": "Anguilla", "CountryCode": "+1-264", "IsoCode": "AI" }, { "Country": "Antarctica", "CountryCode": "+672", "IsoCode": "AQ" }, { "Country": "Antigua and Barbuda", "CountryCode": "+1-268", "IsoCode": "AG" }, { "Country": "Argentina", "CountryCode": "+54", "IsoCode": "AR" }, { "Country": "Armenia", "CountryCode": "+374", "IsoCode": "AM" }, { "Country": "Aruba", "CountryCode": "+297", "IsoCode": "AW" }, { "Country": "Australia", "CountryCode": "+61", "IsoCode": "AU" }, { "Country": "Austria", "CountryCode": "+43", "IsoCode": "AT" }, { "Country": "Azerbaijan", "CountryCode": "+994", "IsoCode": "AZ" }, { "Country": "Bahamas", "CountryCode": "+1-242", "IsoCode": "BS" }, { "Country": "Bahrain", "CountryCode": "+973", "IsoCode": "BH" }, { "Country": "Bangladesh", "CountryCode": "+880", "IsoCode": "BD" }, { "Country": "Barbados", "CountryCode": "+1-246", "IsoCode": "BB" }, { "Country": "Belarus", "CountryCode": "+375", "IsoCode": "BY" }, { "Country": "Belgium", "CountryCode": "+32", "IsoCode": "BE" }, { "Country": "Belize", "CountryCode": "+501", "IsoCode": "BZ" }, { "Country": "Benin", "CountryCode": "+229", "IsoCode": "BJ" }, { "Country": "Bermuda", "CountryCode": "+1-441", "IsoCode": "BM" }, { "Country": "Bhutan", "CountryCode": "+975", "IsoCode": "BT" }, { "Country": "Bolivia", "CountryCode": "+591", "IsoCode": "BO" }, { "Country": "Bosnia and Herzegovina", "CountryCode": "+387", "IsoCode": "BA" }, { "Country": "Botswana", "CountryCode": "+267", "IsoCode": "BW" }, { "Country": "Brazil", "CountryCode": "+55", "IsoCode": "BR" }, { "Country": "British Indian Ocean Territory", "CountryCode": "+246", "IsoCode": "IO" }, { "Country": "British Virgin Islands", "CountryCode": "+1-284", "IsoCode": "VG" }, { "Country": "Brunei", "CountryCode": "+673", "IsoCode": "BN" }, { "Country": "Bulgaria", "CountryCode": "+359", "IsoCode": "BG" }, { "Country": "Burkina Faso", "CountryCode": "+226", "IsoCode": "BF" }, { "Country": "Myanmar", "CountryCode": "+95", "IsoCode": "MM" }, { "Country": "Burundi", "CountryCode": "+257", "IsoCode": "BI" }, { "Country": "Cambodia", "CountryCode": "+855", "IsoCode": "KH" }, { "Country": "Cameroon", "CountryCode": "+237", "IsoCode": "CM" }, { "Country": "Canada", "CountryCode": "+1", "IsoCode": "CA" }, { "Country": "Cape Verde", "CountryCode": "+238", "IsoCode": "CV" }, { "Country": "Cayman Islands", "CountryCode": "+1-345", "IsoCode": "KY" }, { "Country": "Central African Republic", "CountryCode": "+236", "IsoCode": "CF" }, { "Country": "Chad", "CountryCode": "+235", "IsoCode": "TD" }, { "Country": "Chile", "CountryCode": "+56", "IsoCode": "CL" }, { "Country": "China", "CountryCode": "+86", "IsoCode": "CN" }, { "Country": "Christmas Island", "CountryCode": "+61", "IsoCode": "CX" }, { "Country": "Cocos Islands", "CountryCode": "+61", "IsoCode": "CC" }, { "Country": "Colombia", "CountryCode": "+57", "IsoCode": "CO" }, { "Country": "Comoros", "CountryCode": "+269", "IsoCode": "KM" }, { "Country": "Republic of the Congo", "CountryCode": "+242", "IsoCode": "CG" }, { "Country": "Democratic Republic of the Congo", "CountryCode": "+243", "IsoCode": "CD" }, { "Country": "Cook Islands", "CountryCode": "+682", "IsoCode": "CK" }, { "Country": "Costa Rica", "CountryCode": "+506", "IsoCode": "CR" }, { "Country": "Croatia", "CountryCode": "+385", "IsoCode": "HR" }, { "Country": "Cuba", "CountryCode": "+53", "IsoCode": "CU" }, { "Country": "Curacao", "CountryCode": "+599", "IsoCode": "CW" }, { "Country": "Cyprus", "CountryCode": "+357", "IsoCode": "CY" }, { "Country": "Czech Republic", "CountryCode": "+420", "IsoCode": "CZ" }, { "Country": "Denmark", "CountryCode": "+45", "IsoCode": "DK" }, { "Country": "Djibouti", "CountryCode": "+253", "IsoCode": "DJ" }, { "Country": "Dominica", "CountryCode": "+1-767", "IsoCode": "DM" }, { "Country": "Dominican Republic", "CountryCode": "+1-809, 1-829, 1-849", "IsoCode": "DO" }, { "Country": "East Timor", "CountryCode": "+670", "IsoCode": "TL" }, { "Country": "Ecuador", "CountryCode": "+593", "IsoCode": "EC" }, { "Country": "Egypt", "CountryCode": "+20", "IsoCode": "EG" }, { "Country": "El Salvador", "CountryCode": "+503", "IsoCode": "SV" }, { "Country": "Equatorial Guinea", "CountryCode": "+240", "IsoCode": "GQ" }, { "Country": "Eritrea", "CountryCode": "+291", "IsoCode": "ER" }, { "Country": "Estonia", "CountryCode": "+372", "IsoCode": "EE" }, { "Country": "Ethiopia", "CountryCode": "+251", "IsoCode": "ET" }, { "Country": "Falkland Islands", "CountryCode": "+500", "IsoCode": "FK" }, { "Country": "Faroe Islands", "CountryCode": "+298", "IsoCode": "FO" }, { "Country": "Fiji", "CountryCode": "+679", "IsoCode": "FJ" }, { "Country": "Finland", "CountryCode": "+358", "IsoCode": "FI" }, { "Country": "France", "CountryCode": "+33", "IsoCode": "FR" }, { "Country": "French Polynesia", "CountryCode": "+689", "IsoCode": "PF" }, { "Country": "Gabon", "CountryCode": "+241", "IsoCode": "GA" }, { "Country": "Gambia", "CountryCode": "+220", "IsoCode": "GM" }, { "Country": "Georgia", "CountryCode": "+995", "IsoCode": "GE" }, { "Country": "Germany", "CountryCode": "+49", "IsoCode": "DE" }, { "Country": "Ghana", "CountryCode": "+233", "IsoCode": "GH" }, { "Country": "Gibraltar", "CountryCode": "+350", "IsoCode": "GI" }, { "Country": "Greece", "CountryCode": "+30", "IsoCode": "GR" }, { "Country": "Greenland", "CountryCode": "+299", "IsoCode": "GL" }, { "Country": "Grenada", "CountryCode": "+1-473", "IsoCode": "GD" }, { "Country": "Guam", "CountryCode": "+1-671", "IsoCode": "GU" }, { "Country": "Guatemala", "CountryCode": "+502", "IsoCode": "GT" }, { "Country": "Guernsey", "CountryCode": "+44-1481", "IsoCode": "GG" }, { "Country": "Guinea", "CountryCode": "+224", "IsoCode": "GN" }, { "Country": "Guinea-Bissau", "CountryCode": "+245", "IsoCode": "GW" }, { "Country": "Guyana", "CountryCode": "+592", "IsoCode": "GY" }, { "Country": "Haiti", "CountryCode": "+509", "IsoCode": "HT" }, { "Country": "Honduras", "CountryCode": "+504", "IsoCode": "HN" }, { "Country": "Hong Kong", "CountryCode": "+852", "IsoCode": "HK" }, { "Country": "Hungary", "CountryCode": "+36", "IsoCode": "HU" }, { "Country": "Iceland", "CountryCode": "+354", "IsoCode": "IS" }, { "Country": "India", "CountryCode": "+91", "IsoCode": "IN" }, { "Country": "Indonesia", "CountryCode": "+62", "IsoCode": "ID" }, { "Country": "Iran", "CountryCode": "+98", "IsoCode": "IR" }, { "Country": "Iraq", "CountryCode": "+964", "IsoCode": "IQ" }, { "Country": "Ireland", "CountryCode": "+353", "IsoCode": "IE" }, { "Country": "Isle of Man", "CountryCode": "+44-1624", "IsoCode": "IM" }, { "Country": "Israel", "CountryCode": "+972", "IsoCode": "IL" }, { "Country": "Italy", "CountryCode": "+39", "IsoCode": "IT" }, { "Country": "Ivory Coast", "CountryCode": "+225", "IsoCode": "CI" }, { "Country": "Jamaica", "CountryCode": "+1-876", "IsoCode": "JM" }, { "Country": "Japan", "CountryCode": "+81", "IsoCode": "JP" }, { "Country": "Jersey", "CountryCode": "+44-1534", "IsoCode": "JE" }, { "Country": "Jordan", "CountryCode": "+962", "IsoCode": "JO" }, { "Country": "Kazakhstan", "CountryCode": "+7", "IsoCode": "KZ" }, { "Country": "Kenya", "CountryCode": "+254", "IsoCode": "KE" }, { "Country": "Kiribati", "CountryCode": "+686", "IsoCode": "KI" }, { "Country": "Kosovo", "CountryCode": "+383", "IsoCode": "XK" }, { "Country": "Kuwait", "CountryCode": "+965", "IsoCode": "KW" }, { "Country": "Kyrgyzstan", "CountryCode": "+996", "IsoCode": "KG" }, { "Country": "Laos", "CountryCode": "+856", "IsoCode": "LA" }, { "Country": "Latvia", "CountryCode": "+371", "IsoCode": "LV" }, { "Country": "Lebanon", "CountryCode": "+961", "IsoCode": "LB" }, { "Country": "Lesotho", "CountryCode": "+266", "IsoCode": "LS" }, { "Country": "Liberia", "CountryCode": "+231", "IsoCode": "LR" }, { "Country": "Libya", "CountryCode": "+218", "IsoCode": "LY" }, { "Country": "Liechtenstein", "CountryCode": "+423", "IsoCode": "LI" }, { "Country": "Lithuania", "CountryCode": "+370", "IsoCode": "LT" }, { "Country": "Luxembourg", "CountryCode": "+352", "IsoCode": "LU" }, { "Country": "Macau", "CountryCode": "+853", "IsoCode": "MO" }, { "Country": "Macedonia", "CountryCode": "+389", "IsoCode": "MK" }, { "Country": "Madagascar", "CountryCode": "+261", "IsoCode": "MG" }, { "Country": "Malawi", "CountryCode": "+265", "IsoCode": "MW" }, { "Country": "Malaysia", "CountryCode": "+60", "IsoCode": "MY" }, { "Country": "Maldives", "CountryCode": "+960", "IsoCode": "MV" }, { "Country": "Mali", "CountryCode": "+223", "IsoCode": "ML" }, { "Country": "Malta", "CountryCode": "+356", "IsoCode": "MT" }, { "Country": "Marshall Islands", "CountryCode": "+692", "IsoCode": "MH" }, { "Country": "Mauritania", "CountryCode": "+222", "IsoCode": "MR" }, { "Country": "Mauritius", "CountryCode": "+230", "IsoCode": "MU" }, { "Country": "Mayotte", "CountryCode": "+262", "IsoCode": "YT" }, { "Country": "Mexico", "CountryCode": "+52", "IsoCode": "MX" }, { "Country": "Micronesia", "CountryCode": "+691", "IsoCode": "FM" }, { "Country": "Moldova", "CountryCode": "+373", "IsoCode": "MD" }, { "Country": "Monaco", "CountryCode": "+377", "IsoCode": "MC" }, { "Country": "Mongolia", "CountryCode": "+976", "IsoCode": "MN" }, { "Country": "Montenegro", "CountryCode": "+382", "IsoCode": "ME" }, { "Country": "Montserrat", "CountryCode": "+1-664", "IsoCode": "MS" }, { "Country": "Morocco", "CountryCode": "+212", "IsoCode": "MA" }, { "Country": "Mozambique", "CountryCode": "+258", "IsoCode": "MZ" }, { "Country": "Namibia", "CountryCode": "+264", "IsoCode": "NA" }, { "Country": "Nauru", "CountryCode": "+674", "IsoCode": "NR" }, { "Country": "Nepal", "CountryCode": "+977", "IsoCode": "NP" }, { "Country": "Netherlands", "CountryCode": "+31", "IsoCode": "NL" }, { "Country": "Netherlands Antilles", "CountryCode": "+599", "IsoCode": "AN" }, { "Country": "New Caledonia", "CountryCode": "+687", "IsoCode": "NC" }, { "Country": "New Zealand", "CountryCode": "+64", "IsoCode": "NZ" }, { "Country": "Nicaragua", "CountryCode": "+505", "IsoCode": "NI" }, { "Country": "Niger", "CountryCode": "+227", "IsoCode": "NE" }, { "Country": "Nigeria", "CountryCode": "+234", "IsoCode": "NG" }, { "Country": "Niue", "CountryCode": "+683", "IsoCode": "NU" }, { "Country": "Northern Mariana Islands", "CountryCode": "+1-670", "IsoCode": "MP" }, { "Country": "North Korea", "CountryCode": "+850", "IsoCode": "KP" }, { "Country": "Norway", "CountryCode": "+47", "IsoCode": "NO" }, { "Country": "Oman", "CountryCode": "+968", "IsoCode": "OM" }, { "Country": "Pakistan", "CountryCode": "+92", "IsoCode": "PK" }, { "Country": "Palau", "CountryCode": "+680", "IsoCode": "PW" }, { "Country": "Palestine", "CountryCode": "+970", "IsoCode": "PS" }, { "Country": "Panama", "CountryCode": "+507", "IsoCode": "PA" }, { "Country": "Papua New Guinea", "CountryCode": "+675", "IsoCode": "PG" }, { "Country": "Paraguay", "CountryCode": "+595", "IsoCode": "PY" }, { "Country": "Peru", "CountryCode": "+51", "IsoCode": "PE" }, { "Country": "Philippines", "CountryCode": "+63", "IsoCode": "PH" }, { "Country": "Pitcairn", "CountryCode": "+64", "IsoCode": "PN" }, { "Country": "Poland", "CountryCode": "+48", "IsoCode": "PL" }, { "Country": "Portugal", "CountryCode": "+351", "IsoCode": "PT" }, { "Country": "Puerto Rico", "CountryCode": "+1-787, 1-939", "IsoCode": "PR" }, { "Country": "Qatar", "CountryCode": "+974", "IsoCode": "QA" }, { "Country": "Reunion", "CountryCode": "+262", "IsoCode": "RE" }, { "Country": "Romania", "CountryCode": "+40", "IsoCode": "RO" }, { "Country": "Russia", "CountryCode": "+7", "IsoCode": "RU" }, { "Country": "Rwanda", "CountryCode": "+250", "IsoCode": "RW" }, { "Country": "Saint Barthelemy", "CountryCode": "+590", "IsoCode": "BL" }, { "Country": "Samoa", "CountryCode": "+685", "IsoCode": "WS" }, { "Country": "San Marino", "CountryCode": "+378", "IsoCode": "SM" }, { "Country": "Sao Tome and Principe", "CountryCode": "+239", "IsoCode": "ST" }, { "Country": "Saudi Arabia", "CountryCode": "+966", "IsoCode": "SA" }, { "Country": "Senegal", "CountryCode": "+221", "IsoCode": "SN" }, { "Country": "Serbia", "CountryCode": "+381", "IsoCode": "RS" }, { "Country": "Seychelles", "CountryCode": "+248", "IsoCode": "SC" }, { "Country": "Sierra Leone", "CountryCode": "+232", "IsoCode": "SL" }, { "Country": "Singapore", "CountryCode": "+65", "IsoCode": "SG" }, { "Country": "Sint Maarten", "CountryCode": "+1-721", "IsoCode": "SX" }, { "Country": "Slovakia", "CountryCode": "+421", "IsoCode": "SK" }, { "Country": "Slovenia", "CountryCode": "+386", "IsoCode": "SI" }, { "Country": "Solomon Islands", "CountryCode": "+677", "IsoCode": "SB" }, { "Country": "Somalia", "CountryCode": "+252", "IsoCode": "SO" }, { "Country": "South Africa", "CountryCode": "+27", "IsoCode": "ZA" }, { "Country": "South Korea", "CountryCode": "+82", "IsoCode": "KR" }, { "Country": "South Sudan", "CountryCode": "+211", "IsoCode": "SS" }, { "Country": "Spain", "CountryCode": "+34", "IsoCode": "ES" }, { "Country": "Sri Lanka", "CountryCode": "+94", "IsoCode": "LK" }, { "Country": "Saint Helena", "CountryCode": "+290", "IsoCode": "SH" }, { "Country": "Saint Kitts and Nevis", "CountryCode": "+1-869", "IsoCode": "KN" }, { "Country": "Saint Lucia", "CountryCode": "+1-758", "IsoCode": "LC" }, { "Country": "Saint Martin", "CountryCode": "+590", "IsoCode": "MF" }, { "Country": "Saint Pierre and Miquelon", "CountryCode": "+508", "IsoCode": "PM" }, { "Country": "Saint Vincent and the Grenadines", "CountryCode": "+1-784", "IsoCode": "VC" }, { "Country": "Sudan", "CountryCode": "+249", "IsoCode": "SD" }, { "Country": "Suriname", "CountryCode": "+597", "IsoCode": "SR" }, { "Country": "Svalbard and Jan Mayen", "CountryCode": "+47", "IsoCode": "SJ" }, { "Country": "Swaziland", "CountryCode": "+268", "IsoCode": "SZ" }, { "Country": "Sweden", "CountryCode": "+46", "IsoCode": "SE" }, { "Country": "Switzerland", "CountryCode": "+41", "IsoCode": "CH" }, { "Country": "Syria", "CountryCode": "+963", "IsoCode": "SY" }, { "Country": "Taiwan", "CountryCode": "+886", "IsoCode": "TW" }, { "Country": "Tajikistan", "CountryCode": "+992", "IsoCode": "TJ" }, { "Country": "Tanzania", "CountryCode": "+255", "IsoCode": "TZ" }, { "Country": "Thailand", "CountryCode": "+66", "IsoCode": "TH" }, { "Country": "Togo", "CountryCode": "+228", "IsoCode": "TG" }, { "Country": "Tokelau", "CountryCode": "+690", "IsoCode": "TK" }, { "Country": "Tonga", "CountryCode": "+676", "IsoCode": "TO" }, { "Country": "Trinidad and Tobago", "CountryCode": "+1-868", "IsoCode": "TT" }, { "Country": "Tunisia", "CountryCode": "+216", "IsoCode": "TN" }, { "Country": "Turkey", "CountryCode": "+90", "IsoCode": "TR" }, { "Country": "Turkmenistan", "CountryCode": "+993", "IsoCode": "TM" }, { "Country": "Turks and Caicos Islands", "CountryCode": "+1-649", "IsoCode": "TC" }, { "Country": "Tuvalu", "CountryCode": "+688", "IsoCode": "TV" }, { "Country": "United Arab Emirates", "CountryCode": "+971", "IsoCode": "AE" }, { "Country": "Uganda", "CountryCode": "+256", "IsoCode": "UG" }, { "Country": "United Kingdom", "CountryCode": "+44", "IsoCode": "GB" }, { "Country": "Ukraine", "CountryCode": "+380", "IsoCode": "UA" }, { "Country": "Uruguay", "CountryCode": "+598", "IsoCode": "UY" }, { "Country": "United States", "CountryCode": "+1", "IsoCode": "US" }, { "Country": "Uzbekistan", "CountryCode": "+998", "IsoCode": "UZ" }, { "Country": "Vanuatu", "CountryCode": "+678", "IsoCode": "VU" }, { "Country": "Vatican", "CountryCode": "+379", "IsoCode": "VA" }, { "Country": "Venezuela", "CountryCode": "+58", "IsoCode": "VE" }, { "Country": "Vietnam", "CountryCode": "+84", "IsoCode": "VN" }, { "Country": "U.S. Virgin Islands", "CountryCode": "+1-340", "IsoCode": "VI" }, { "Country": "Wallis and Futuna", "CountryCode": "+681", "IsoCode": "WF" }, { "Country": "Western Sahara", "CountryCode": "+212", "IsoCode": "EH" }, { "Country": "Yemen", "CountryCode": "+967", "IsoCode": "YE" }, { "Country": "Zambia", "CountryCode": "+260", "IsoCode": "ZM" }, { "Country": "Zimbabwe", "CountryCode": "+263", "IsoCode": "ZW" } ] "@ $CountryCodes = $JsonCodes | ConvertFrom-Json #Tracking variables $UsersProcessed = 0 $ScriptStartTime = Get-Date #Verbose output Write-Verbose -Message "$(Get-Date -f T) - Function started..." if ($CsvOutput) {Write-Verbose -Message "$(Get-Date -f T) - CSV output selected"} #Some additional paramter validation outside of param() #Try and connect to Azure AD try {$DomainInfo = Get-MsolDomain -TenantId $TenantId -ErrorAction SilentlyContinue} catch {} if ($DomainInfo) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId established" } else { #Present connection pop-up Write-Verbose -Message "$(Get-Date -f T) - Calling Connect-MsolService cmdlet" Connect-MsolService -ErrorAction SilentlyContinue #Populate the DomainInfo variable if Connect-MsolService works if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Connection to $TenantId established" $DomainInfo = $true } else { Write-Verbose "$(Get-Date -f T) - Connection to $TenantId could not be established" } } #Check if we have a connection if ($DomainInfo) { #Check if we need to create a CSV file if ($CsvOutput) { #Output file $Now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $OutputFile = "MfaPhoneToLocationCorrelation_$now.csv" Write-Verbose -Message "$(Get-Date -f T) - Creating CSV file - $OutputFile" Add-Content -Value "UserPrincipalName,UserDisplayName,UserObjectId,UserUsageLocation,UserUsageCountry,MfaPhoneNumberPrefix,MfaPhoneNumberLocation,MfaPhoneNumberCountry,AltPhoneNumberPrefix,AltPhoneNumberLocation,AltPhoneNumberCountry" ` -Path $OutputFile -ErrorAction SilentlyContinue if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Header written to CSV file - $OutputFile" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write header to CSV file - $OutputFile" Write-Warning -Message "$(Get-Date -f T) - Reverting to non-CSV output mode" #Prevent further CSV processing $CsvOutput = $false } } #Check if we need to run for one user or the whole tenant if ($TargetUser) { Write-Verbose -Message "$(Get-Date -f T) - Checking for target user - $TargetUser" #Single user $ObtainedUser = Get-MsolUser -ObjectId $TargetUser -TenantId $TenantId -ErrorAction SilentlyContinue if ($ObtainedUser) { Write-Verbose -Message "$(Get-Date -f T) - User $TargetUser confirmed as valid" Write-Verbose -Message "$(Get-Date -f T) - User Display Name = $(($ObtainedUser).Displayname)" if ($ObtainedUser.UsageLocation) { $ObtainedUser | ForEach-Object { #Analyse the user $AnalysedUser = Measure-MsolMfaPhoneToLocationCorrelation -User $_ #Check for CSV output if ($CsvOutput) { Write-Verbose -Message "$(Get-Date -f T) - Converting analysis to CSV format" #Call property expansion function and pipe into a CSV format $CsvFormat = $AnalysedUser | ConvertTo-Csv -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Writing conversion to CSV file" #Write the pertinent CSV line Add-Content -Value $CsvFormat[1] -Path $OutputFile if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Details successfully written to CSV file" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write details to CSV file" } } else { $AnalysedUser } #Increment user count $UsersProcessed++ } } else { Write-Warning -Message "$(Get-Date -f T) - The target user - $TargetUser - does not have usage location populated" Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } } else { Write-Verbose -Message "$(Get-Date -f T) - $($error[0])" Write-Warning -Message "$(Get-Date -f T) - Issue obtaining the target user $TargetUser" Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } } else { Write-Verbose -Message "$(Get-Date -f T) - Checking all users in the tenant" Get-MsolUser -All -TenantId $TenantId -ErrorAction SilentlyContinue | ForEach-Object { if ($_.UsageLocation) { #Analyse the user $AnalysedUser = Measure-MsolMfaPhoneToLocationCorrelation -User $_ #We populate the UsageCountry if an MFA phone number or alternative phone number are found in the analysis if ($AnalysedUser.UserUsageCountry) { #Check for CSV output if ($CsvOutput) { Write-Verbose -Message "$(Get-Date -f T) - Converting analysis to CSV format" #Call property expansion function and pipe into a CSV format $CsvFormat = $AnalysedUser | ConvertTo-Csv -NoTypeInformation Write-Verbose -Message "$(Get-Date -f T) - Writing conversion to CSV file" #Write the pertinent CSV line Add-Content -Value $CsvFormat[1] -Path $OutputFile if ($?) { Write-Verbose -Message "$(Get-Date -f T) - Details successfully written to CSV file" } else { Write-Warning -Message "$(Get-Date -f T) - Failed to write details to CSV file" } } else { $AnalysedUser } } } #Increment user count $UsersProcessed++ } } } else { #We can't connect... say goodbye Write-Warning -Message "$(Get-Date -f T) - Exiting script..." } #Tracking stuff $ScriptEndTime = Get-Date $TimeSpan = $ScriptEndTime - $ScriptStartTime $ProcessingTime = "{0:c}" -f $TimeSpan Write-Verbose -Message "$(Get-Date -f T) - Total users processed: $UsersProcessed" Write-Verbose -Message "$(Get-Date -f T) - Total processing time: $ProcessingTime" Write-Verbose -Message "$(Get-Date -f T) - Function finished!" #endregion Main } #end function #endregion ################################# ################################# #region 7) POLICIES ################################################## #FUNCTION: Get-AzureADIRConditionalAccessPolicy ################################################## function Get-AzureADIRConditionalAccessPolicy { ############################################################################ <# .SYNOPSIS Gets Azure Active Directory Conditional Access policies. .DESCRIPTION Gets Azure Active Directory Conditional Access policies for the target tenant. Use -All to get details for all policies. Use -PolicyDisplayName to target a single policy. Can also produce a date and time stamped XML file as output to capture multi-layered arrays. .EXAMPLE Get-AzureADIRConditionalAccessPolicy -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -All Gets all Conditional Access policies for the tenant. .EXAMPLE Get-AzureADIRConditionalAccessPolicy -TenantId b446a536-cb76-4360-a8bb-6593cf4d9c7f -PolicyDisplayName "Box Block" -XmlOutput Gets the details of the Conditional Access policy called "Box Block" Writes the output to a date and time stamped XML file in the execution directory. #> ############################################################################ [CmdletBinding()] param( #The tenant ID [Parameter(Mandatory,Position=0)] [guid]$TenantId, #Bring back all conditional access policies [Parameter(Mandatory,Position=1,ParameterSetName="All")] [switch]$All, #Bring back a specific policy by display name [Parameter(Mandatory,Position=2,ParameterSetName="PolicyDisplayName")] [string]$PolicyDisplayName, #Use this switch to create a date and time stamped XML file [Parameter(Position=3)] [switch]$XmlOutput ) ############################################################################ #Deal with different search criterea if ($All) { #API endpoint $Filter = "" Write-Verbose -Message "$(Get-Date -f T) - All policies mode selected" } elseif ($PolicyDisplayName) { #API endpoint $Filter = "?`$filter=displayName eq '$PolicyDisplayName'" Write-Verbose -Message "$(Get-Date -f T) - Single policy mode selected" } ############################################################################ $Url = "https://graph.microsoft.com/beta/conditionalAccess/policies$Filter" ############################################################################ #Get / refresh an access token $Token = (Get-AzureADIRApiToken -TenantId $TenantId).AccessToken if ($Token) { #Construct header with access token $Header = Get-AzureADIRHeader -Token $Token #Tracking variables $TotalReport = $null #Call the API query loop $TotalReport = Invoke-AzureADIRDoWhile -Header $Header -Url $Url } #See if we need to write to XML if ($XmlOutput) { #Output file $now = "{0:yyyyMMdd_hhmmss}" -f (Get-Date) $XmlName = "ConditionalAccess_$now.xml" Write-Verbose -Message "$(Get-Date -f T) - Generating a XML for Conditional Access details" $TotalReport | Export-Clixml -Path $XmlName Write-Verbose -Message "$(Get-Date -f T) - Conditional Access details written to $(Get-Location)\$XmlName" } else { #Return stuff $TotalReport } } #end function #endregion ################################# ################################# #region 8) MISC ############################################### #FUNCTION: Get-AzureADIRObjectIdToDisplayName ############################################### function Get-AzureADIRObjectIdToDisplayName { ############################################################################ <# .SYNOPSIS Looks up an ObjectId and displays its human-friendly properties. .DESCRIPTION Looks up an ObjectId and shows its display name and other associated properties: * DisplayName * ObjectType * ObjectId .EXAMPLE Get-AzureADIRObjectIdToDisplayName -ObjectId 69447235-0974-4af6-bfa3-d0e922a92048 Gets the displayname for the supplied ObjectID - 69447235-0974-4af6-bfa3-d0e922a92048. .EXAMPLE Get-AzureADIRObjectIdToDisplayName -ObjectId 69447235-0974-4af6-bfa3-d0e922a92048,21fda713-d825-4a80-8e3a-7323b9dcd4b3 Gets the displayname for the supplied ObjectIDs - 69447235-0974-4af6-bfa3-d0e922a92048, 21fda713-d825-4a80-8e3a-7323b9dcd4b3 #> ############################################################################ [CmdletBinding()] param( #The object IDs to look up a display name for [Parameter(Mandatory,Position=0)] [array]$ObjectIds ) ############################################################################ #Get tenant details to test that Connect-AzureADIR has been called try { $TenantInfo = Get-AzureADTenantDetail } catch { throw "You must call Connect-AzureADIR to run this function" } $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" #Get a list of directory roles Write-Verbose -Message "$(Get-Date -f T) - Attempting to get display name for $ObjectIds" try {$DisplayNames = Get-AzureADObjectByObjectId -ObjectIds $ObjectIds -ErrorAction SilentlyContinue} catch {} if ($DisplayNames) { Write-Verbose -Message "$(Get-Date -f T) - $(($DisplayNames).Count) objects found" $DisplayNames | ForEach-Object { $Properties = [PSCustomObject]@{ ObjectId = $_.ObjectId DisplayName = $_.DisplayName ObjectType = "$($_.UserType)$($_.ObjectType)" } [array]$TotalObjects += $Properties } $TotalObjects } else { Write-Warning -Message "$(Get-Date -f T) - Issue with objectID list" } } #end function ############################################### #FUNCTION: Get-AzureADIRDisplayNameToObjectId ############################################### function Get-AzureADIRDisplayNameToObjectId { ############################################################################ <# .SYNOPSIS Looks up a display name and shows its object ID. .DESCRIPTION Looks up a display name with a StartsWith filter. Shows its object ID and associated properties: * DisplayName * ObjectId * ObjectType Use with the -ObjectType parameter to target a specific object type. .EXAMPLE Get-AzureADIRDisplayNameToObjectId -DisplayName "Ian Dreamer" -ObjectType User Gets the DisplayName, ObjectId and Object type for the supplied user display name - "Ian Dreamer". .EXAMPLE Get-AzureADIRDisplayNameToObjectId -DisplayNameStartsWith "All Sales" -ObjectType Group Gets the DisplayName, ObjectId and Object type for the supplied group display name - "All Sales". .EXAMPLE Get-AzureADIRDisplayNameToObjectId -DisplayNameStartsWith WinSrv -ObjectType Device Gets the DisplayName, ObjectId and Object type for the supplied device display name - WinSrv. .EXAMPLE Get-AzureADIRDisplayNameToObjectId -DisplayNameStartsWith Microsoft -ObjectType ServicePrincipal Gets the DisplayName, ObjectId and Object type for the supplied ServicePrincipal display name - Microsoft. .EXAMPLE Get-AzureADIRDisplayNameToObjectId -DisplayNameStartsWith Sales -ObjectType Application Gets the DisplayName, ObjectId and Object type for the supplied Application display name - Sales. #> ############################################################################ [CmdletBinding()] param( #The display name to look up an object ID for [Parameter(Mandatory,Position=0)] [string]$DisplayNameStartsWith, #The object type to perform the look-up against [Parameter(Mandatory,Position=1)] [ValidateSet("User","Group","Device","ServicePrincipal","Application")] [string]$ObjectType ) ############################################################################ #Get tenant details to test that Connect-AzureADIR has been called try { $TenantInfo = Get-AzureADTenantDetail } catch { throw "You must call Connect-AzureADIR to run this function" } $InitialDomain = ($TenantInfo.VerifiedDomains | Where-Object {$_.Initial}).Name Write-Verbose -Message "$(Get-Date -f T) - Target tenant ID initial domain name - $InitialDomain" #Select look-up mode switch ($ObjectType) { "User" { Write-Verbose -Message "$(Get-Date -f T) - ObjectType is 'User'" try {$Objects = Get-AzureADUser -SearchString $DisplayNameStartsWith -ErrorAction SilentlyContinue} catch {} } "Group" { Write-Verbose -Message "$(Get-Date -f T) - ObjectType is 'Group'" try {$Objects = Get-AzureADGroup -SearchString $DisplayNameStartsWith -ErrorAction SilentlyContinue} catch {} } "Device" { Write-Verbose -Message "$(Get-Date -f T) - ObjectType is 'Device'" try {$Objects = Get-AzureADDevice -SearchString $DisplayNameStartsWith -ErrorAction SilentlyContinue} catch {} } "ServicePrincipal" { Write-Verbose -Message "$(Get-Date -f T) - ObjectType is 'ServicePrincipal'" try {$Objects = Get-AzureADServicePrincipal -SearchString $DisplayNameStartsWith -ErrorAction SilentlyContinue} catch {} } "Application" { Write-Verbose -Message "$(Get-Date -f T) - ObjectType is 'Application'" try {$Objects = Get-AzureADApplication -SearchString $DisplayNameStartsWith -ErrorAction SilentlyContinue} catch {} } } if ($Objects) { Write-Verbose -Message "$(Get-Date -f T) - Display name - `'$DisplayNameStartsWith`' - found" $Objects | ForEach-Object { $Properties = [PSCustomObject]@{ DisplayName = $_.DisplayName ObjectId = $_.ObjectId ObjectType = "$($_.UserType)$($_.ObjectType)" } [array]$TotalObjects += $Properties } $TotalObjects } else { Write-Warning -Message "$(Get-Date -f T) - Issue with display name" } } #end function #endregion ############################################################################################################# ############################################################################################################# # SIG # Begin signature block # MIIjgwYJKoZIhvcNAQcCoIIjdDCCI3ACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCCQtFwWPksSdiV # mF5SXiqVCK2iDbjgMPgjWxRF93VMf6CCDYEwggX/MIID56ADAgECAhMzAAAB32vw # LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn # s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw # PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS # yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG # 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh # EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH # tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS # 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp # TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok # t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4 # b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao # mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD # Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt # VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G # CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+ # Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82 # oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVWDCCFVQCAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN # BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgBIp70FK1 # q2kGKsTfFiZDzKAeQlxVNoa3V7/ONQc3EXEwQgYKKwYBBAGCNwIBDDE0MDKgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN # BgkqhkiG9w0BAQEFAASCAQCGraI7X+il105Y9BCt7k63bOrMTt+ZwpDDqiGlIWFt # epPMo8Kar2CE8QJxDC/uqH7u4liLUsiVkgrTEulpEFuCrA4QC0y/lLILXydsxeW8 # bdTYz2dJpAIVXMbgJ232ph+Zs9kpMyA4g0BeZ+u6Wgdd5eh1aGNkCcEHfV2bm6e4 # vr5gjMwTMSBj6qJ1t1iW7U2gZa8H4Nd0o0pOW8N2J4u/U9jOXE7/QjmmnkXTgT8G # dZLetyCJA+ukmFiunr2ZCnr4cFW9l179zSB8PgWrx/twcw1m5Gv/P/EDW/FHKwPs # 25goO7De9uTRWUYBZiXzSw8KOHjkb0T106OhpzwdybR2oYIS4jCCEt4GCisGAQQB # gjcDAwExghLOMIISygYJKoZIhvcNAQcCoIISuzCCErcCAQMxDzANBglghkgBZQME # AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB # MDEwDQYJYIZIAWUDBAIBBQAEIET0wITbDFq/PfCF7uQTwOjuO50jGJXHlCyFgYkl # CGw7AgZgYyMmk4AYEzIwMjEwNDAxMTYzOTI3LjcxN1owBIACAfSggdCkgc0wgcox # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1p # Y3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg # RVNOOjNFN0EtRTM1OS1BMjVEMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt # cCBTZXJ2aWNloIIOOTCCBPEwggPZoAMCAQICEzMAAAFSMEtdiazmcEcAAAAAAVIw # DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN # MjAxMTEyMTgyNjA1WhcNMjIwMjExMTgyNjA1WjCByjELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg # T3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0U3QS1FMzU5LUEy # NUQxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0G # CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuzG6EiZh0taCSbswMiupMTYnbboFz # jj1DuDbbvT0RXKBCVl/umA+Uy214DmHiFhkeuRdlLB0ya5S9um5aKr7lBBqZzvtK # gGNgCRbDTG9Yu6kzDzPTzQRulVIvoWVy0gITnEyoJ1O3m5IPpsLBNQCdXsh+3TZF # 73JAcub21bnxm/4sxe4zTdbdttBrqX8/JJF2VEnAP+MBvF2UQSo6XUAaTKC/HPDP # Cce/IsNoAxxLDI1wHhIlqjRBnt4HM5HcKHrZrvH+vHnihikdlEzh3fjQFowk1fG7 # PVhmO60O5vVdqA+H9314hHENQI0cbo+SkSi8SSJSLNixgj0eWePTh7pbAgMBAAGj # ggEbMIIBFzAdBgNVHQ4EFgQUhN2u2qwj1l2c2h/kULDuBRJsexQwHwYDVR0jBBgw # FoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDov # L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENB # XzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0 # cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAx # MC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDAN # BgkqhkiG9w0BAQsFAAOCAQEAVcUncfFqSazQbDEXf3d10/upiWQU5HdTbwG9v9be # VIDaG4oELyIcNE6e6CbOBMlPU+smpYYcnK3jucNqChwquLmxdi2iPy4iQ6vjAdBp # 9+VFWlrBqUsNXZzjCpgMCZj6bu8Xq0Nndl4WyBbI0Jku68vUNG4wsMdKP3dz+1Mz # k9SUma3j7HyNA559do9nhKmoZMn5dtf03QvxlaEwMAaPk9xuUv9BN8cNvFnpWk4m # LERQW6tA3rXK0soEISKTYG7Ose7oMXZDYPWxf9oFhYKzZw/SwnhdBoj2S5eyYE3A # uF/ZXzR3hdp3/XGzZeOdERfFy1rC7ZBwhDIajeFMi53GnzCCBnEwggRZoAMCAQIC # CmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRp # ZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcwMTIx # NDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV # BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG # A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqGSIb3 # DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUKAIKF # ++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCIhFRD # DNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5FfgVSx # z5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx898Fd1 # rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg16Hgc # sOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHmMIIB # 4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8UzaFqF # bVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud # EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYD # VR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwv # cHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEB # BE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j # ZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSBlTCB # kjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwICMDQe # MiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBuAHQA # LiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+umzPUx # vs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM9GAS # inbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84Dxf1 # L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+tuWO # M7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32WapB4 # pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUPei45 # V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAeb73x # 4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4hQEe # gPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanblrKn # QqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLFVeNp # 3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLraNtvT # X4/edIhJEqGCAsswggI0AgEBMIH4oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP # cGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozRTdBLUUzNTktQTI1 # RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcG # BSsOAwIaAxUAv26eVJaumcmTchd6hqayQMNDXluggYMwgYCkfjB8MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg # VGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOQQRJIwIhgPMjAy # MTA0MDEyMTA5MzhaGA8yMDIxMDQwMjIxMDkzOFowdDA6BgorBgEEAYRZCgQBMSww # KjAKAgUA5BBEkgIBADAHAgEAAgIGTTAHAgEAAgIRijAKAgUA5BGWEgIBADA2Bgor # BgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAID # AYagMA0GCSqGSIb3DQEBBQUAA4GBAFdnAMYA4OTwQRT+atcrlX6Rg493mVJ8Tyw0 # 2JAODc2XcWOIJy83mJTODUGDlPffgAuY6ZCUhEOFEm5Lvj/3Hs2t+I2FBagU/+YI # jRW5CpHMeI7t6QE6v32xGESoC4jwXT2cFashSgu83EvODtBpSdRUp7N09D7aiQIh # f2oz/NKeMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAFSMEtdiazmcEcAAAAAAVIwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqG # SIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgwR4DGEGerhtW # spp0nHkt2sNItyan45eDxDQEufDXYrEwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHk # MIG9BCCT7lzHo4slUIxfEGp8LXQNik/ecK6vuuGWIcmBrrsnpjCBmDCBgKR+MHwx # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABUjBLXYms5nBHAAAAAAFS # MCIEIBtYNEKIsOvvnrH7gKsgI696Vhem+3atCtWtHKZcVPgmMA0GCSqGSIb3DQEB # CwUABIIBAAHyvhD2cwhwtrzt+piVnZRLhoEY7B0YEwem9OatXNRWA/zjvFHxYHRO # 3DLum33MnJ2SypeL5MaBJhzgkPQGTJUguhPfNZHX4hgaIRNo2I2mLlkTmbq1zMst # HJu0NNrORJxaGOt/EPnmQWORaxyi5yHspflEr80MeKn9VsytsPBEK8SyiKXjYMzH # lY+zZZ5ChjaZfg1KRiZt9GbM9L/qE4oDpaUs7w51I0Ea86Lcr77rLOvh5M1FFFKk # lFlH6IUFXE0pZ718+e7dSIKPe2dfpFU0L9uOvUFqRtM6Vb4s2kIqQYiqeY3mzIfZ # 2YJIAKL6KZKutz7CR3InHmWHHLYEUpA= # SIG # End signature block |