Monitoring with PowerShell: O365 location alerts

A while back someone asked me to convert a script they had to the Secure Application Model. This specific script checked the Office 365 audit log IPs against a online database of locations. I declined at first and suggested it to look into Microsoft 365, a P1 or P2 subscription which allows you to do this native.

The reader came back to me recently asking once more, and giving a explanation on why P1/P2 or M365 was not possible in her case. I understand that sometimes you might have to make due with what you have. I also figured others might be in the same boat.

So this script is made to monitor the O365 unified audit log and to compare the IP addresses to a database online. The database she used previously was very rate-limited and as such not really suitable. Not a lot of people know that there actually is a great 100% free online lookup service for IPs at https://ip2c.org/. The script uses that database as there are no API limitations. 🙂

As always, I’d like to note that this should just be one layer in your entire security model and you should not put all your faith in this. Enable MFA, and keep good security hygiene.

The Script

You’ll need the Secure Application Model for this script. The script currently checks all your tenants except the ones you state in “$Skiplist”.

######### Secrets #########
$ApplicationId = 'ApplicationID'
$ApplicationSecret = 'ApplicationSecret' | ConvertTo-SecureString -Force -AsPlainText
$TenantID = 'TenantID'
$RefreshToken = 'VeryLongRefreshToken'
$ExchangeRefreshToken = 'LongExchangeToken'
$UPN = "UPN-User-To-Generate-IDs"
######### Secrets #########

$AllowedCountries = @('Belgium', 'Netherlands', 'Germany', 'United Kingdom')
$Skiplist = @("bla1.onmicrosoft.com", "bla2.onmicrosoft.com", "bla2.onmicrosoft.com")

$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken

$customers = Get-MsolPartnerContract -All | Where-Object { $_.DefaultDomainName -notin $SkipList }

$StrangeLocations = foreach ($customer in $customers) {
    Write-Host "Getting logon location details for $($customer.Name)" -ForegroundColor Green
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    $null = Import-PSSession $session -CommandName Search-UnifiedAuditLog -AllowClobber
 

    $startDate = (Get-Date).AddDays(-1)
    $endDate = (Get-Date)
    Write-Host "Retrieving logs for $($customer.name)" -ForegroundColor Blue
    $logs = do {
        $log = Search-unifiedAuditLog -SessionCommand ReturnLargeSet -SessionId $customer.name -ResultSize 5000 -StartDate $startDate -EndDate $endDate -Operations UserLoggedIn
        Write-Host "Retrieved $($log.count) logs" -ForegroundColor Yellow
        $log
    } while ($Log.count % 5000 -eq 0 -and $log.count -ne 0)
    Write-Host "Finished Retrieving logs" -ForegroundColor Green
 
    $userIds = $logs.userIds | Sort-Object -Unique

    $LocationMonitoring = foreach ($userId in $userIds) {
 
        $searchResult = ($logs | Where-Object { $_.userIds -contains $userId }).auditdata | ConvertFrom-Json -ErrorAction SilentlyContinue
        $ips = $searchResult.clientip | Sort-Object -Unique
        foreach ($ip in $ips) {
            $IsIp = ($ip -as [ipaddress]) -as [bool]
            if ($IsIp) { $ipresult = (Invoke-restmethod -method get -uri "https://ip2c.org/$($ip)") -split ';' }
            [PSCustomObject]@{
                user              = $userId
                IP                = $ip
                Country           = ($ipresult | Select-Object -index 3)
                CountryCode       = ($ipresult | Select-Object -Index 1)
                Company           = $customer.Name
                TenantID          = $customer.tenantID
                DefaultDomainName = $customer.DefaultDomainName
            }
           
        }

    }
    foreach ($Location in $LocationMonitoring) {
        if ($Location.country -notin $AllowedCountries) { $Location }
    }
}
if (!$StrangeLocations) {
    $StrangeLocations = 'Healthy'
}

$StrangeLocations

And that’s it! Now you’re monitoring the locations from where users are logging on. As always, Happy PowerShelling!

3 thoughts on “Monitoring with PowerShell: O365 location alerts

  1. Zachery Frazier

    Great script, I’ve used something similar from GCITS before. I just tried this script but i’m getting an error on the invoke web request. It appears it returns WRONG INPUT. Went to the website and tested the request and it dumps out right. Currently looking through and see if I deleted something i shouldn’t have in the script.

    Love your stuff btw!

    Reply
    1. VMarinaro

      I’m seeing the same “WRONG INPUT” error, which seems to happen if you pass an IPv6 address from the audit log to the https://ip2c.org API – it can only handle IPv4 addresses.

      I fixed it by adding the following line before the invoke-restmethod call to ip2c:

      if ($ip.length -gt 15) {continue}

      Also, I love your stuff as well!

      Reply
      1. Zachery Frazier

        This is awesome and it fixed me all up. I didn’t realize those logs were pulling both IPv4 & IPv6. I wonder if I can implement my ip-api.com to get IPv6 also. Thanks again!

        Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.