Entra / Microsoft 365 · Users & guests
Report external tenant access by guests
Generate a report of member accounts who have accessed apps in external tenants in a Microsoft 365 environment.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -NoWelcome -Scopes $RequiredScopes
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([int] $LookbackDays = 30,[string] $StartDate = (Get-Date).AddDays(-$LookbackDays).ToString('yyyy-MM-ddT00:00:00Z'),[string] $EndDate = (Get-Date).ToString('yyyy-MM-ddT23:59:59Z'))[array]$RequiredScopes = "AuditLog.Read.All","Organization.Read.All","CrossTenantInformation.ReadBasic.All"$Interactive = $false# Determine if we're interactive or notIf ([Environment]::UserInteractive) {# We're running interactively...Clear-HostWrite-Host "Script running interactively... connecting to the Graph" -ForegroundColor YellowConnect-MgGraph -NoWelcome -Scopes $RequiredScopes$Interactive = $true} Else {# We're not, so likely in Azure AutomationWrite-Output "Executing the runbook to create the last activity report for guest access outside the tenant..."Connect-MgGraph -Identity -NoWelcome}# Check that we have the right permissions - in Azure Automation, we assume that the automation account has the right permissionsIf ($Interactive) {[int]$RequiredScopesCount = $RequiredScopes.Count[string[]]$CurrentScopes = (Get-MgContext).Scopes[string[]]$RequiredScopes = $RequiredScopes$CheckScopes =[object[]][Linq.Enumerable]::Intersect($RequiredScopes,$CurrentScopes)If ($CheckScopes.Count -ne $RequiredScopesCount ) {Write-Host ("To run this script, you need to connect to Microsoft Graph with the following scopes: {0}" -f $RequiredScopes) -ForegroundColor RedBreak}}# Define the date range for the report (last 30 days) - Reduce this is it takes too long to fetch audit sign-in records# Find this tenant informationTry {$ThisTenantData = Get-MgOrganization -ErrorAction Stop$ThisTenantId = $ThisTenantData.Id} catch {Write-Warning "Failed to retrieve tenant information for this organization: $($_.Exception.Message)"Return}# Build OData filter string safely$Filter = "createdDateTime ge $StartDate and createdDateTime le $EndDate and status/errorCode eq 0"Write-Output "Fetching sign-in audit records from $StartDate to $EndDate..."# Get all successful sign-in audit records within the specified date rangeTry {[array]$AuditRecords = Get-MgBetaAuditLogSignIn -All -Filter $Filter -ErrorAction Stop} Catch {Write-Error "Failed to retrieve sign-in logs: $($_.Exception.Message)"Return}# Filter to find the set of external tenant accesses by guest users[array]$ExternalAccessRecords = $AuditRecords | Where-Object { $_.HomeTenantId -ne $_.ResourceTenantId -and $_.HomeTenantId -eq $ThisTenantId}[array]$InboundAccessRecords = $AuditRecords | Where-Object { $_.HomeTenantId -ne $_.ResourceTenantId -and $_.HomeTenantId -ne $ThisTenantId}# Summarize: which host tenants your users accessed# Find the unique tenant ids[array]$HostTenants = $ExternalAccessRecords | Select-Object -ExpandProperty ResourceTenantId -UniqueWrite-Output "Extracting information for host tenants..."$TenantData = [System.Collections.Generic.List[Object]]::new()ForEach ($Tenant in $HostTenants) {$TenantDetails = Find-MgTenantRelationshipTenantInformationByTenantId -TenantId $Tenant$TenantRecords = $ExternalAccessRecords | Where-Object { $_.ResourceTenantId -eq $Tenant }$TenantUsers = $TenantRecords | Select-Object -ExpandProperty UserPrincipalName -Unique$TenantApps = $TenantRecords | Select-Object -ExpandProperty AppDisplayName -Unique$TenantDataLine = [PSCustomObject]@{TenantId = $TenantTenantName = $TenantDetails.DisplayName'Access type' = 'Outbound''Number of Sign-ins' = $TenantRecords.Count'Unique Users' = $TenantUsers.Count'Users' = ($TenantUsers -join '; ')'Apps Accessed' = ($TenantApps -join '; ')}$TenantData.Add($TenantDataLine)}Write-Output "Extracting information for inbound tenants..."$TenantUsers = $InboundAccessRecords | Select-Object -ExpandProperty UserPrincipalName -Unique$TenantApps = $InboundAccessRecords | Select-Object -ExpandProperty AppDisplayName -Unique$TenantDataLine = [PSCustomObject]@{TenantId = $ThisTenantData.IdTenantName = $ThisTenantData.DisplayName'Access type' = 'Inbound''Number of Sign-ins' = $InboundAccessRecords.Count'Unique Users' = $TenantUsers.Count'Users' = ($TenantUsers -join '; ')'Apps Accessed' = ($TenantApps -join '; ')}$TenantData.Add($TenantDataLine)Write-Output ""Write-Output "Report of External Tenant Access by Guest Users"$TenantData | Select-Object TenantName, Type, 'Number of Sign-ins', 'Unique Users', Users, 'Apps Accessed' | Format-Table -AutoSize# Export the report as an Excel worksheet or CSV fileIf (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $trueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\ExternalTenantAccessReport.xlsx"If (Test-Path $ExcelOutputFile) {Remove-Item $ExcelOutputFile -ErrorAction SilentlyContinue}$TenantData | Export-Excel -Path $ExcelOutputFile -WorksheetName "External Tenant Access" -Title ("External Tenant Access Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "ExternalTenantAccess" -AutoSize -AutoFilter} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\ExternalTenantAccessReport.CSV"$TenantData | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8}If ($ExcelGenerated) {Write-Output ("Excel worksheet output written to {0}" -f $ExcelOutputFile)} Else {Write-Output ("CSV output file written to {0}" -f $CSVOutputFile)}Write-Output "All done - enjoy the External Tenant Access data!"
Parameters
ParameterDefaultNotes
-LookbackDays30Number of days back to include in the sign-in audit search.-StartDate(Get-Date).AddDays(-30).ToString('yyyy-MM-ddT00:00:00Z')Start of the reporting window.-EndDate(Get-Date).ToString('yyyy-MM-ddT23:59:59Z')End of the reporting window.Attribution
Author
Office365itpros