Sunday, April 12, 2020

Search for EXO legacy clients with Graph API

While ago I promised to get back on this subject of how to use Graph API to hunt down basic auth clients. Wait no more! Here's the script that pulls all Exchange Online legacy auth sign-in events from Azure AD sign-in logs from the last 30 days. Implementation of my version of the script is heavily inspired by GitHub project PullAzureADSignInReports and by a blog by Stephan Wälde.

Complete script can be found here: get-exolegacysignins.ps1 but if you want to know what's happening within, check detailed walkthrough below.

Graph API authentication stuff

I'll be using ADAL.NET / Active Directory Authentication Library (ADAL) for authentication. Dll's can be found in AzureRM or AzureAD PowerShell modules.

# to make code more readable and rows shorter, 
# ADAL namespace is introduced (must be the first line of the script)
using namespace Microsoft.IdentityModel.Clients.ActiveDirectory

# Binding AAD dlls, AzureADPreview works as well
$AADModule = Get-Module -Name "AzureAD" -ListAvailable
[System.Reflection.Assembly]::LoadFrom($(Join-Path $AADModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll")) | out-null
[System.Reflection.Assembly]::LoadFrom($(Join-Path $AADModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll")) | out-null

Then we need Azure application registation for Graph API access. Luckily there's a global one that can be used with delegated permissions. Global multi-tenant "Azure AD PowerShell ClientID" 1b730954-1685-4b74-9bfd-dac224a7b894, includes delegated permissions for
  • AuditLog.Read.All
  • Directory.AccessAsUser.All
  • Directory.ReadWrite.All
  • Group.ReadWrite.All
$ClientID  = "1b730954-1685-4b74-9bfd-dac224a7b894" 
#Parameters for Graph API
$resourceURI = "https://graph.microsoft.com"
$authority = "https://login.microsoftonline.com/common"


The Azure app we're using uses delegated permissions. So, we'll have to pass credentials to get the token for Graph API access. To read AAD signin logs admin permissions are required, eg. Security Reader or Global Reader role for example.

$userid = read-host "Enter user name (upn)"
$securepwd = read-host "Enter pa$$w0rd! for $userid" -AsSecureString

$uc = new-object UserPasswordCredential -ArgumentList $userid, $securepwd

Then we can get the token using PowerShell ClientID and previously entered user credentials. Let's authenticate!

$authContext = New-Object AuthenticationContext -ArgumentList $authority
$authResponse = [AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($authContext, $resourceURI, $ClientID, $uc)
$authResult = $authResponse.result 

Write-Debug "Auth status: $($authResponse.Status)"
Write-Debug "Auth access token type: $($authResult.AccessTokenType)"

After getting the token we can build header for Graph API requests.

$headers = @{}
$headers.Add('Authorization','Bearer ' + $authResult.AccessToken)
$headers.Add('Content-Type', "application/json")

API Request filters

Then we only want sign-in events using basic authentication. Here we have a list all "clientApps" we are interested in, e.g. Exchange Online basic auth client apps.

$legacyClients = @(
 "Other clients",
 "Exchange Web Services",
 "MAPI Over HTTP",
 "POP3",
 "Outlook Anywhere (RPC over HTTP)",
 "IMAP4",
 "AutoDiscover",
 "Offline Address Book",
 "Authenticated SMTP",
 "Exchange Online PowerShell",
 "Exchange ActiveSync"
)

There's probably thousands of sign-in events and those should be fetched in smaller "chunks". In this script one request will pull 8 hours of events per query URL. If querying very large tenant, you might want to reduce time frame to even smaller time window. In this script I'm getting all possible events e.g. fetching all legacy sign-ins from Azure AD logs during the last 30 days. NOTE: query filtering is very sensitive to datetime format, dates must be like: "2020-02-05T14:01:02Z".

$reportingStartDate = (Get-Date).ToUniversalTime().Date.AddDays(-30)
$reportingEndDate = (Get-Date).ToUniversalTime().Date

$timespanMinutes = 480 #8h

We're getting closer of looping all together. Here we initialize the start time for filtering events by createdDateTime property.

# set initial "nextstarttime" for web request
$nextstarttime = $reportingStartDate
Do { ...

Finally, we're inside the main loop that keeps goinging on until all blocks are finished. 8 hours at the time. First thing in the outer do-while loop we'll have to initialize datetime query filters. Also note that export CSV filepath is defined in this section. This will generate one CSV for each day. Of course, by changing csv naming you can export all to single file. Events are later on appended to this file.

 # get log entries for a specified time span
 $fromtime = $nextstarttime # set "from" as the ending datetime from the previous time window
 $totime = $fromtime.AddMinutes($timespanMinutes)
 
 # ensure totime is in spesified timeframe, if passes it, set end date as report end datetime
 if ($totime -gt $reportingEndDate) { $totime = $reportingEndDate }

 # NOTE: filtering is very sensitive to datetime format, date must be like: 2020-02-05T14:01:02Z
 # convert to "sortable" format
 $from = $($fromtime.ToString("s")) + "Z"
 $to = $($totime.ToString("s")) + "Z"

 Write-Debug "Request from $from"
 Write-Debug "Request to $to"

 #set start for the next round in do-while
 $nextstarttime = $totime

 # generate only one log file for each day, append results to AAD_LegacySignInReport_yyyyMMdd.csv
 $now = "{0:yyyyMMdd}" -f $fromtime
 $outputFile = ".\AAD_LegacySignInReport_$now.csv"

Next step, generating query URLs. We're genarating separate query URLs for each client app in "legacyClients" in given 8 hour time frame. So, lots of queries happening soon.

 # generate request URLs for each legacy client type, using given time range
 $urls = @()
 $legacyClients|%{
  $urls += "https://graph.microsoft.com/beta/auditLogs/signIns?"+`
  "`$filter="+`
  "createdDateTime%20ge%20" + $from + "%20"+`
  "and%20createdDateTime%20le%20" + $to + "%20"+`
  "and%20clientAppUsed%20eq%20'" + $_ +`
  "'&`$top=1000"
 }

 foreach ($url in $urls) { ...

Pulling sign-in events from Graph API

Finally we're getting something out of logs.... Another do-while loop to get all rows related to single query filtered by
  1. "createdDateTime" - 8 hour at the time
  2. "clientAppUsed" - value within defined client apps 
Request is sent to Invoke-WebRequest cmdlet and results are returned in Json format in Content property.
From converted PSObject collection, formatted results are exported to CSV file.

If query returns more than top 1000 rows specified in URL, new query url is fetched from "@odata.nextLink" property. Queries are done in do-while loop until nextLink is empty.

  Do {
   Write-Output "Requesting sign-ins: $url"

   Try {
    
    # get data and convert json payload
    $myReport = (Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $url)
    $results = ($myReport.Content | ConvertFrom-Json).value
        
    # export sign-in data to CSV
    
    $results | `
     select `
     createdDateTime,
     userDisplayName,userPrincipalName,userId,`
     appId,appDisplayName,`
     ipAddress,`
     clientAppUsed,`
     userAgent,`
     conditionalAccessStatus,`
     isInteractive,`
     resourceDisplayName,resourceId,`
     mfaDetail,`
     @{Name='status.errorCode'; Expression={$_.status.errorCode}},`
     @{Name='status.failureReason'; Expression={$_.status.failureReason}},`
     @{Name='status.additionalDetails'; Expression={$_.status.additionalDetails}},`
     @{Name='location.countryOrRegion'; Expression={$_.location.countryOrRegion}},`
     @{Name='location.city'; Expression={$_.location.city}},`
     @{Name='device.deviceId'; Expression={$_.deviceDetail.deviceId}},`
     @{Name='device.displayName'; Expression={$_.DeviceDetail.displayName}},`
     @{Name='device.operatingSystem'; Expression={$_.DeviceDetail.operatingSystem}},`
     @{Name='device.browser'; Expression={$_.DeviceDetail.browser}},`
     @{Name='device.isCompliant'; Expression={$_.DeviceDetail.isCompliant}},`
     @{Name='device.isManaged'; Expression={$_.DeviceDetail.isManaged}},`
     @{Name='device.trustType'; Expression={$_.DeviceDetail.trustType}}`
     | Export-Csv $outputFile -Append -NoTypeInformation -Encoding UTF8
    
    # if request returns over 1000 events, next query url is returned
    $url = ($myReport.Content | ConvertFrom-Json).'@odata.nextLink'
    
    $count = $count+$results.Count
    Write-Output "$count events returned"
    
   } Catch { # retry logic related stuff here ... }
   
  } while($url -ne $null)
  

And that's it!

Just import CSVs to Excel or to Power BI and start analysing :)



Sunday, February 23, 2020

Getting EXO Mobile Device Report by using RobustCloudCommand

Getting mobile device statistics out of Exchange Online must be one of the most time consuming cmdlets. I once tried to generate a report in a tenant I consider large (around 60k users) and it took 2 weeks calendar time with connections expiring all the time and requests dropped due to throttling limits.

I'm currently ramping up Outlook App usage with my customers to get rid of ActiveSync. During that process reporting all EXO connected mobile devices is again required. I was already depressed by the fact that I must again run this sloooow and time consumig operation until I remembered that @nestafo (thanks again!) once mentioned some kind of "robust framework" for running long scripts.

After googling a bit I found it. The RobustCloudCommand. I thought I'd give it a go.

Using RobustCloudCommand


First you have to install it from PS Gallery. It installs also dependencies (CloudConnect, MSOnline, AzureAD). I already had AzureADPreview installed, so AllowClobber parameter was required to force installation of AzureAD module.


Install-Module RobustCloudCommand -AllowClobber
Import-Module RobustCloudCommand


Basically it has only few things to do:
- Create credential object
- Generate a CSV including objects you want to address with a script
- Write a script block for single object, whatever you want to do with single mailbox or user
- Pass all above to RobustCloudCommand and wait few days

RobustCloudCommand reconnects services automatically, adds delays to prevent throttling issues and even writes out estimates when script will be completed. Great!

Kudos to Matthew Byrd for developing the module!
Check his latest RobustCloudCommand post on Exchange Team Blog.

Mobile Device Statistics Report


Here's my take on "Exchange Online Mobile Device Statistics" script using RobustCloudCommand


# declare paths for csvs and log
$csvpath = "C:\Scripts\mbx.csv"
$reportpath = "C:\Scripts\mbx-mobiledevice-report.csv"
$logpath = "C:\Scripts\mbx-mobiledevice-report.log"

# create credentials and connect to EXO
$adminAccount = "exo.admin@tenant.onmicrosoft.com"
$securePassword = ConvertTo-SecureString -String "VeryComplex-pa$$w0rd" -AsPlainText -Force

$cred = New-Object System.Management.Automation.PSCredential ($adminAccount, $securePassword)

Connect-ExchangeOnline -Credential $cred

# get list of mailboxes for processing
Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox | Select Displayname,PrimarySMTPAddress,Identity | Export-Csv $csvpath

# import mailboxes from csv
$mailboxes = Import-Csv $csvpath

# start processing
Start-RobustCloudCommand -Credential $cred -recipients $mailboxes -logfile $logpath -ScriptBlock {
 Get-MobileDeviceStatistics -Mailbox $input.PrimarySMTPAddress.tostring() -ErrorAction "SilentlyContinue" | Select @{name="PrimarySMTPAddress"; exp={$input.PrimarySMTPAddress.tostring()}}, FirstSyncTime,LastPolicyUpdateTime,LastSyncAttemptTime,LastSuccessSync,DeviceType,DeviceID,DeviceUserAgent,LastPingHeartbeat,DeviceModel,DeviceImei,DeviceFriendlyName,DeviceOS,DeviceOSLanguage,DevicePhoneNumber,DeviceEnableOutboundSMS,DeviceMobileOperator,Identity,Guid,Status,StatusNote,DeviceAccessState,DeviceAccessStateReason,DeviceAccessControlRule,DevicePolicyApplied,DevicePolicyApplicationStatus,ClientVersion,NumberOfFoldersSynced,SyncStateUpgradeTime,ClientType | Export-Csv $reportpath -Encoding UTF8 -NoTypeInformation -Append
}





Saturday, February 15, 2020

Hunting down accounts using Exchange Online basic authentication

Exactly 240 days to go when I'm writing this. World as we know it will end by Oct 13th 2020.

Microsoft claims it will shut down basic authentication for IMAP, POP, EWS, ActiveSync and PowerShell in Exchange Online on Oct 13th 2020. That means you'd have to use "modern authentication" for connecting these services in Office 365. This won't affect on-prem Exchange Servers, you'd "only" be worried about apps connecting cloud mailboxes.

Edit: Microsoft postponed deprecation of Exchange Online basic authentication due to COVID-19 situation. New estimated deadline is around Q2/2021.


Impact is brutal. Clients using basic authentication after deadline will fail to connect Exchange Online. That includes all Android devices using native mail or calendar clients, iOS devices OS older than 12.1 using native mail or calendar apps. Backend systems connecting mailboxes with Exchange Web Services, POP or IMAP. All dead. Död. Kaputt.

Remediation actions recommended by Microsoft: update backend systems to use OAuth and switch to Graph API. Instruct mobile users to use Outlook App.

So, how can I know if my organization is using basic auth?

Short answer: investigate Azure AD sign-in logs.

Azure Ad Sign-ins log

We can access AAD logs directly from Azure Active Directory - Monitoring - Sign-ins.
You can try to guess which client apps to follow. I'd say that basic "Sign-ins" logs view is only usable when investigating connectivity of a single account or a very narrow timeframe.


Azure Active Directory Workbooks

Relatively new AAD workbooks offer good overview of tenant sign-ins. First graph shows number of total signins and by clicking bars, you'll get individual signins of selected protocol. From details you can click yourself to Log Analytics Workspace query window.

Using AAD Workbooks requires connecting AAD logs to Log Analytics Workspace (or to Sentinel). If connection has been established and LA Workspace access granted, you can take advantage of pre-built workbooks like "Sign-ins using Legacy Authentication"



Log Analytics or Sentinel

In Azure you can do whatever you want. With AAD logs I mean. Build your own dashboards, hunting workbooks, analytics, alerts, playbooks... Here's couple of simple queries to start with.

List of all unique accounts using legacy client connectivity:

SigninLogs
| where ClientAppUsed in (
"Exchange ActiveSync",
"Other clients",
"IMAP4",
"POP3")
and ResultType == "0"
and AppDisplayName =~ "Office 365 Exchange Online"
and TimeGenerated > ago(30d)
| distinct UserPrincipalName , ClientAppUsed , Location , IPAddress


And overview of usage of the same set of selected client apps:

SigninLogs
| where ClientAppUsed in (
"Exchange ActiveSync",
"Other clients",
"IMAP4",
"POP3")
and ResultType == "0"
and AppDisplayName =~ "Office 365 Exchange Online"
and TimeGenerated > ago(30d)
| summarize dcount(UserPrincipalName) by ClientAppUsed
| sort by dcount_UserPrincipalName desc 
| render columnchart


NOTE that Microsoft has changed ClientAppUsed app names for all legacy clients starting Jan 25th. In case you have script/log analytics based monitoring, you should review new app names and update your kusto queries.

Old legacy auth ClientAppUsed values:
  • Exchange ActiveSync (supported)
  • Exchange ActiveSync (unsupported)
  • Other clients
  • Other clients; Older Office clients
  • Other clients; IMAP
  • Other clients; POP
  • Other clients; SMTP
  • Other clients; MAPI

New ones (visible in logs from Jan 25th until Feb 15th):
  • Exchange ActiveSync
  • Other clients
  • IMAP4
  • POP3
  • Authenticated SMTP

There might be others in the future. Logs are lacking EWS, AutoDiscover and PowerShell at least (were already clickable in AAD sign-ins filtering).

Custom scripting

There's Graph API available for reading sign-in logs: https://graph.microsoft.com/beta/auditLogs/signIns

I will be posting a separate article on that one. See you soon!


Saturday, January 25, 2020

Putting Exchange Online PowerShell V2 to Work

So, they say it's faster. And more reliable. Had to run few tests with the most common queries.

Getting single mailboxes

When requesting single mailboxes, difference is already notable. When using old Get-Mailbox it took around 450ms (from Finland where I'm performing my tests, over quite bad ADSL line, thanks to my ISP). New Get-EXOMailbox cmdlet returned lightweight mailbox objects around 200ms.


Get-EXOMailbox -Identity pete@phelme.onmicrosoft.com


Old: ~450ms
New: ~200ms

When testing single mailboxes, I was running 2 parallel powershell sessions, old cmdlets with c2r-MFA-capable-EXOService-module and other with new V2 ExchangeOnlineManagement. Note that legacy Get-Mailbox cmdlet in the V2 module is as slow as it is in the old one.

There's major difference in returned payload. With the EXO V2 you only get the minimum set of properties:

More mailboxes

Next bit more challenging query. Show me all your UserMailboxes (out of approx. 22k boxes).

$mailboxes = Get-EXOMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited


Old: 350675ms (5 min, 50 sec)
New: 115325ms (1 min, 55 sec)

It IS faster. Both returned same set of items, just under 18k mailboxes.

Mailbox Statistics

How about the most expensive one, mailbox statistics. Let's get statistics for first 1000 mailboxes.

$stats = $mailboxes | Select -f 1000 | Get-EXOMailboxStatistics -ErrorAction Continue


Old: 480486ms (8 min)
New: 477638ms (7 min, 57 sec)

No notable improvement there. Of course you'll be saving memory, only most used properties are passed by default when using new EXO V2 module:

Mobile Device Statistics

When running statistics for mobile devices I realized that new V2 module is not surfacing errors even if I define -ErrorAction "Continue" in the cmdlet. It just goes on as if all's good. Actually Get-EXOMobileDeviceStatistics doesn't raise errors at all for missing statistics. Might be by design though.

The old one is writing out errors as expected:

And how about the speed? A bit faster, saves you 20%.


$mobilestats = $mailboxes | Select -f 1000 | Get-EXOMobileDeviceStatistics


Old: 7 minutes
New: 5 min 19 sec

Mobile stats in module V2 gives larger set by default:

Session lifetime

I was surprised that module V2 renewed session automatically when old module just prompted for new credentials.


So, based on first runs it's good, it's faster. It's lighter. Reliability remains to be seen. When testing cmdlets in your own tenant you might experience totally different performance for better or for worse.

Keep on scripting, see you!

Wednesday, January 22, 2020

Finally, Exchange Online PowerShell module available in PS Gallery

As you should know already, Microsoft is axing Exchange Online legacy authentication this year (Oct 13th 2020 to be exact). Still many of automations are relying on basic authentication, which is bad.

Edit: Microsoft postponed deprecation of Exchange Online basic authentication due to COVID-19 situation. New estimated deadline is around Q2/2021.

Before there was this odd click-2-run package of modern authentication capable EXO module. I never quite got why they did it that way but I'm glad we can soon forget it completely. You can now install new Exchange Online PowerShell V2 module from the PS Gallery as it always should've been. Note that it was still in preview when writing this post.

There's really good documentation on the new module (the previous link), but let me save you a click and introduce it here briefly.

How to install

Note: All install cmdlets must be run with elevated PowerShell session (as administrator).

Install-Module -Name ExchangeOnlineManagement


You might get an error: WARNING: The specified module ... with PowerShellGetFormatVersion ‘2.0’ is not supported by
the current version of PowerShellGet. Get the latest version of the PowerShellGet module to install this module ...


If error occurs, you have to update PowerShellGet.


Install-PackageProvider -Name NuGet -Force
Exit

Install-Module -Name PowerShellGet -Force
Exit

Update-Module -Name PowerShellGet
Exit



How to use


Import-Module ExchangeOnlineManagement
$cred = Get-Credential

Connect-ExchangeOnline -Credential $cred

So, you can basically use stored credentials in your scripts and connection is made using "Modern Authentication". Azure AD sees actor as "Mobile Apps and Desktop clients" type of rich client. Actually, new EXO PS module cannot be used with basic auth at all.



Whats new?

EXO V2 module also has a few new cmdlets, prefixed with "EXO". Old versions of these cmdlets are still there for backward compatibility.

Get-Command -Module ExchangeOnlineManagement -Noun "EXO*"

New cmdlets revealed:

Get-EXOCasMailbox
Get-EXOMailbox
Get-EXOMailboxFolderPermission
Get-EXOMailboxFolderStatistics
Get-EXOMailboxPermission
Get-EXOMailboxStatistics
Get-EXOMobileDeviceStatistics
Get-EXORecipient
Get-EXORecipientPermission

New cmdlets should be more robust and therefore new V2 module introduces "property sets". You can request only properties that are relevant to your specific use case. Check available sets here.

Happy scripting and start planning modern authentication for all your scripts now!